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

GO-5297 Merge branch 'main' of github.com:anyproto/anytype-heart into go-5297-push-service-1st-iteration

This commit is contained in:
AnastasiaShemyakinskaya 2025-04-02 11:48:04 +02:00
commit 601c118b3c
No known key found for this signature in database
GPG key ID: CCD60ED83B103281
95 changed files with 13519 additions and 24273 deletions

View file

@ -237,9 +237,11 @@ packages:
github.com/anyproto/anytype-heart/core/identity:
interfaces:
Service:
github.com/anyproto/anytype-heart/pb/service:
github.com/anyproto/anytype-heart/core/api/apicore:
interfaces:
ClientCommandsServer:
AccountService:
ExportService:
ClientCommands:
github.com/anyproto/anytype-heart/core/block/template:
interfaces:
Service:

View file

@ -9,6 +9,7 @@
<env name="ANYTYPE_GATEWAY_ADDR" value="127.0.0.1:31006" />
<env name="ANYTYPE_GRPC_ADDR" value="127.0.0.1:31007" />
<env name="ANYTYPE_GRPCWEB_ADDR" value="127.0.0.1:31008" />
<env name="ANYTYPE_API_DEBUG" value="1" />
<env name="CC" value="/usr/bin/cc" />
<env name="CXX" value="/usr/bin/c++" />
<env name="CGO_CFLAGS" value="-Wno-deprecated-declarations -Wno-deprecated-non-prototype -Wno-xor-used-as-pow" />

View file

@ -25,366 +25,365 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
func init() { proto.RegisterFile("pb/protos/service/service.proto", fileDescriptor_93a29dc403579097) }
var fileDescriptor_93a29dc403579097 = []byte{
// 5733 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x9d, 0x5f, 0x6f, 0x1d, 0x49,
0x56, 0xc0, 0xc7, 0x2f, 0x0c, 0xf4, 0xb2, 0x03, 0xdc, 0x81, 0x61, 0x76, 0xd8, 0x4d, 0x32, 0x99,
0xc4, 0x4e, 0xe2, 0xf8, 0x3a, 0x93, 0xcc, 0x3f, 0x76, 0x91, 0xe0, 0xc6, 0x8e, 0x3d, 0x77, 0xc7,
0x76, 0x8c, 0xef, 0x75, 0x22, 0x46, 0x42, 0xa2, 0x7d, 0xbb, 0x7c, 0xdd, 0xb8, 0x6f, 0x57, 0x6f,
0x77, 0x5d, 0x27, 0x77, 0x11, 0x08, 0x04, 0x02, 0x81, 0x16, 0xb1, 0xe2, 0x9f, 0xe0, 0x09, 0x89,
0x4f, 0xc0, 0xc7, 0xe0, 0x71, 0x1f, 0x79, 0x44, 0x33, 0xdf, 0x03, 0xa1, 0xae, 0xaa, 0xae, 0x3f,
0xa7, 0xcf, 0xa9, 0x6e, 0xcf, 0x3e, 0x25, 0xf2, 0xf9, 0x9d, 0x73, 0xaa, 0xba, 0x4e, 0x55, 0x9d,
0xaa, 0xae, 0xae, 0x1b, 0xdd, 0x2c, 0xce, 0xb6, 0x8b, 0x92, 0x0b, 0x5e, 0x6d, 0x57, 0xac, 0xbc,
0x4a, 0x67, 0xac, 0xf9, 0x77, 0x28, 0xff, 0x3c, 0x78, 0x33, 0xce, 0x57, 0x62, 0x55, 0xb0, 0xf7,
0xde, 0xb5, 0xe4, 0x8c, 0x2f, 0x16, 0x71, 0x9e, 0x54, 0x0a, 0x79, 0xef, 0x1d, 0x2b, 0x61, 0x57,
0x2c, 0x17, 0xfa, 0xef, 0x8f, 0x7f, 0xf2, 0x7f, 0x6b, 0xd1, 0x5b, 0x3b, 0x59, 0xca, 0x72, 0xb1,
0xa3, 0x35, 0x06, 0x5f, 0x46, 0xdf, 0x1e, 0x15, 0xc5, 0x3e, 0x13, 0x2f, 0x58, 0x59, 0xa5, 0x3c,
0x1f, 0x7c, 0x30, 0xd4, 0x0e, 0x86, 0x27, 0xc5, 0x6c, 0x38, 0x2a, 0x8a, 0xa1, 0x15, 0x0e, 0x4f,
0xd8, 0x8f, 0x96, 0xac, 0x12, 0xef, 0xdd, 0x09, 0x43, 0x55, 0xc1, 0xf3, 0x8a, 0x0d, 0xce, 0xa3,
0x5f, 0x1b, 0x15, 0xc5, 0x84, 0x89, 0x5d, 0x56, 0x57, 0x60, 0x22, 0x62, 0xc1, 0x06, 0x1b, 0x2d,
0x55, 0x1f, 0x30, 0x3e, 0xee, 0x75, 0x83, 0xda, 0xcf, 0x34, 0xfa, 0x56, 0xed, 0xe7, 0x62, 0x29,
0x12, 0xfe, 0x2a, 0x1f, 0xbc, 0xdf, 0x56, 0xd4, 0x22, 0x63, 0xfb, 0x76, 0x08, 0xd1, 0x56, 0x5f,
0x46, 0xbf, 0xfc, 0x32, 0xce, 0x32, 0x26, 0x76, 0x4a, 0x56, 0x17, 0xdc, 0xd7, 0x51, 0xa2, 0xa1,
0x92, 0x19, 0xbb, 0x1f, 0x04, 0x19, 0x6d, 0xf8, 0xcb, 0xe8, 0xdb, 0x4a, 0x72, 0xc2, 0x66, 0xfc,
0x8a, 0x95, 0x03, 0x54, 0x4b, 0x0b, 0x89, 0x47, 0xde, 0x82, 0xa0, 0xed, 0x1d, 0x9e, 0x5f, 0xb1,
0x52, 0xe0, 0xb6, 0xb5, 0x30, 0x6c, 0xdb, 0x42, 0xda, 0xf6, 0xdf, 0xae, 0x45, 0xdf, 0x1d, 0xcd,
0x66, 0x7c, 0x99, 0x8b, 0x03, 0x3e, 0x8b, 0xb3, 0x83, 0x34, 0xbf, 0x3c, 0x62, 0xaf, 0x76, 0x2e,
0x6a, 0x3e, 0x9f, 0xb3, 0xc1, 0x13, 0xff, 0xa9, 0x2a, 0x74, 0x68, 0xd8, 0xa1, 0x0b, 0x1b, 0xdf,
0x1f, 0x5d, 0x4f, 0x49, 0x97, 0xe5, 0x1f, 0xd6, 0xa2, 0x1b, 0xb0, 0x2c, 0x13, 0x9e, 0x5d, 0x31,
0x5b, 0x9a, 0x8f, 0x3b, 0x0c, 0xfb, 0xb8, 0x29, 0xcf, 0x27, 0xd7, 0x55, 0xd3, 0x25, 0xca, 0xa2,
0xb7, 0xdd, 0x70, 0x99, 0xb0, 0x4a, 0x76, 0xa7, 0xfb, 0x74, 0x44, 0x68, 0xc4, 0x78, 0x7e, 0xd0,
0x07, 0xd5, 0xde, 0xd2, 0x68, 0xa0, 0xbd, 0x65, 0xbc, 0x32, 0xce, 0xee, 0xa1, 0x16, 0x1c, 0xc2,
0xf8, 0xba, 0xdf, 0x83, 0xd4, 0xae, 0xfe, 0x28, 0xfa, 0x95, 0x97, 0xbc, 0xbc, 0xac, 0x8a, 0x78,
0xc6, 0x74, 0x57, 0xb8, 0xeb, 0x6b, 0x37, 0x52, 0xd8, 0x1b, 0xd6, 0xbb, 0x30, 0x27, 0x68, 0x1b,
0xe1, 0xf3, 0x82, 0xc1, 0x31, 0xc8, 0x2a, 0xd6, 0x42, 0x2a, 0x68, 0x21, 0xa4, 0x6d, 0x5f, 0x46,
0x03, 0x6b, 0xfb, 0xec, 0x8f, 0xd9, 0x4c, 0x8c, 0x92, 0x04, 0xb6, 0x8a, 0xd5, 0x95, 0xc4, 0x70,
0x94, 0x24, 0x54, 0xab, 0xe0, 0xa8, 0x76, 0xf6, 0x2a, 0x7a, 0x07, 0x38, 0x3b, 0x48, 0x2b, 0xe9,
0x70, 0x2b, 0x6c, 0x45, 0x63, 0xc6, 0xe9, 0xb0, 0x2f, 0xae, 0x1d, 0xff, 0xf9, 0x5a, 0xf4, 0x1d,
0xc4, 0xf3, 0x09, 0x5b, 0xf0, 0x2b, 0x36, 0x78, 0xd4, 0x6d, 0x4d, 0x91, 0xc6, 0xff, 0x87, 0xd7,
0xd0, 0x40, 0xc2, 0x64, 0xc2, 0x32, 0x36, 0x13, 0x64, 0x98, 0x28, 0x71, 0x67, 0x98, 0x18, 0xcc,
0xe9, 0x61, 0x8d, 0x70, 0x9f, 0x89, 0x9d, 0x65, 0x59, 0xb2, 0x5c, 0x90, 0x6d, 0x69, 0x91, 0xce,
0xb6, 0xf4, 0x50, 0xa4, 0x3e, 0xfb, 0x4c, 0x8c, 0xb2, 0x8c, 0xac, 0x8f, 0x12, 0x77, 0xd6, 0xc7,
0x60, 0xda, 0xc3, 0x2c, 0xfa, 0x55, 0xe7, 0x89, 0x89, 0x71, 0x7e, 0xce, 0x07, 0xf4, 0xb3, 0x90,
0x72, 0xe3, 0x63, 0xa3, 0x93, 0x43, 0xaa, 0xf1, 0xec, 0x75, 0xc1, 0x4b, 0xba, 0x59, 0x94, 0xb8,
0xb3, 0x1a, 0x06, 0xd3, 0x1e, 0xfe, 0x30, 0x7a, 0x4b, 0x8f, 0x92, 0xcd, 0x7c, 0x76, 0x07, 0x1d,
0x42, 0xe1, 0x84, 0x76, 0xb7, 0x83, 0x6a, 0x99, 0x3f, 0x4c, 0xe7, 0x65, 0x3d, 0xfa, 0xe0, 0xe6,
0xb5, 0xb4, 0xc3, 0xbc, 0xa5, 0xb4, 0x79, 0x1e, 0xfd, 0xba, 0x6f, 0x7e, 0x27, 0xce, 0x67, 0x2c,
0x1b, 0x3c, 0x08, 0xa9, 0x2b, 0xc6, 0xb8, 0xda, 0xec, 0xc5, 0xda, 0xc1, 0x4e, 0x13, 0x7a, 0x30,
0xfd, 0x00, 0xd5, 0x06, 0x43, 0xe9, 0x9d, 0x30, 0xd4, 0xb2, 0xbd, 0xcb, 0x32, 0x46, 0xda, 0x56,
0xc2, 0x0e, 0xdb, 0x06, 0xd2, 0xb6, 0xcb, 0xe8, 0x37, 0x4c, 0x33, 0xd7, 0x79, 0x81, 0x94, 0xd7,
0x93, 0xce, 0x26, 0xd1, 0x8e, 0x2e, 0x64, 0x7c, 0x3d, 0xec, 0x07, 0xb7, 0xea, 0xa3, 0x47, 0x14,
0xbc, 0x3e, 0x60, 0x3c, 0xb9, 0x13, 0x86, 0xb4, 0xed, 0xbf, 0x5b, 0x8b, 0xbe, 0xa7, 0x65, 0xcf,
0xf2, 0xf8, 0x2c, 0x63, 0x72, 0x8a, 0x3f, 0x62, 0xe2, 0x15, 0x2f, 0x2f, 0x27, 0xab, 0x7c, 0x46,
0xa4, 0x33, 0x38, 0xdc, 0x91, 0xce, 0x90, 0x4a, 0xba, 0x30, 0x7f, 0x12, 0xbd, 0xdb, 0x04, 0xc5,
0x45, 0x9c, 0xcf, 0xd9, 0x0f, 0x2b, 0x9e, 0x8f, 0x8a, 0x74, 0x94, 0x24, 0xe5, 0x60, 0x88, 0x37,
0x3d, 0xe4, 0x4c, 0x09, 0xb6, 0x7b, 0xf3, 0x4e, 0xfa, 0xac, 0x9f, 0xb2, 0xe0, 0x05, 0x4c, 0x9f,
0x9b, 0xc7, 0x27, 0x78, 0x41, 0xa5, 0xcf, 0x3e, 0xd2, 0xb2, 0x7a, 0x58, 0xcf, 0x41, 0xb8, 0xd5,
0x43, 0x77, 0xd2, 0xb9, 0x1d, 0x42, 0xec, 0x1c, 0xd0, 0x3c, 0x28, 0x9e, 0x9f, 0xa7, 0xf3, 0xd3,
0x22, 0xa9, 0xfb, 0xd0, 0x7d, 0xbc, 0xce, 0x0e, 0x42, 0xcc, 0x01, 0x04, 0xaa, 0xbd, 0xfd, 0xbd,
0xcd, 0x32, 0xf5, 0xb8, 0xb4, 0x57, 0xf2, 0xc5, 0x01, 0x9b, 0xc7, 0xb3, 0x95, 0x1e, 0x4c, 0x3f,
0x0a, 0x8d, 0x62, 0x90, 0x36, 0x85, 0xf8, 0xf8, 0x9a, 0x5a, 0xba, 0x3c, 0xff, 0xb1, 0x16, 0xdd,
0xf1, 0xe2, 0x44, 0x07, 0x93, 0x2a, 0xfd, 0x28, 0x4f, 0x4e, 0x58, 0x25, 0xe2, 0x52, 0x0c, 0xbe,
0x1f, 0x88, 0x01, 0x42, 0xc7, 0x94, 0xed, 0x07, 0xdf, 0x48, 0xd7, 0xb6, 0xfa, 0xa4, 0x9e, 0x25,
0xf4, 0xf8, 0xe3, 0xb7, 0xba, 0x94, 0xc0, 0xd1, 0xe7, 0x76, 0x08, 0xb1, 0xad, 0x2e, 0x05, 0xe3,
0xfc, 0x2a, 0x15, 0x6c, 0x9f, 0xe5, 0xac, 0x6c, 0xb7, 0xba, 0x52, 0xf5, 0x11, 0xa2, 0xd5, 0x09,
0xd4, 0x8e, 0x74, 0x9e, 0x37, 0x93, 0x69, 0x6c, 0x06, 0x8c, 0xb4, 0x72, 0x8d, 0x87, 0xfd, 0x60,
0xa2, 0x86, 0x62, 0xbf, 0x36, 0x12, 0xac, 0xa1, 0x42, 0x7a, 0xd5, 0xd0, 0xa0, 0x76, 0x61, 0xee,
0x78, 0x3b, 0x61, 0x57, 0xfc, 0x12, 0x2e, 0xcc, 0x5d, 0x03, 0x0a, 0x20, 0x16, 0xe6, 0x28, 0x68,
0x93, 0x0f, 0xc7, 0xcf, 0x8b, 0x94, 0xbd, 0x02, 0xc9, 0x87, 0xab, 0x5c, 0x8b, 0x89, 0xe4, 0x03,
0xc1, 0xb4, 0x87, 0xa3, 0xe8, 0x97, 0xa4, 0xf0, 0x87, 0x3c, 0xcd, 0x07, 0x37, 0x11, 0xa5, 0x5a,
0x60, 0xac, 0xde, 0xa2, 0x01, 0x50, 0xe2, 0xfa, 0xaf, 0x3a, 0x13, 0xb8, 0x4b, 0x28, 0x81, 0x24,
0x60, 0xbd, 0x0b, 0xb3, 0x59, 0x9f, 0x14, 0xd6, 0xa3, 0xe5, 0xe4, 0x22, 0x2e, 0xd3, 0x7c, 0x3e,
0xc0, 0x74, 0x1d, 0x39, 0x91, 0xf5, 0x61, 0x1c, 0x08, 0x27, 0xad, 0x38, 0x2a, 0x8a, 0xb2, 0x1e,
0x84, 0xb1, 0x70, 0xf2, 0x91, 0x60, 0x38, 0xb5, 0x50, 0xdc, 0xdb, 0x2e, 0x9b, 0x65, 0x69, 0x1e,
0xf4, 0xa6, 0x91, 0x3e, 0xde, 0x2c, 0x0a, 0x82, 0xf7, 0x80, 0xc5, 0x57, 0xac, 0xa9, 0x19, 0xf6,
0x64, 0x5c, 0x20, 0x18, 0xbc, 0x00, 0xb4, 0x4b, 0x6c, 0x29, 0x3e, 0x8c, 0x2f, 0x59, 0xfd, 0x80,
0x59, 0x3d, 0x85, 0x0f, 0x30, 0x7d, 0x8f, 0x20, 0x96, 0xd8, 0x38, 0xa9, 0x5d, 0x2d, 0xa3, 0x77,
0xa4, 0xfc, 0x38, 0x2e, 0x45, 0x3a, 0x4b, 0x8b, 0x38, 0x6f, 0x96, 0x6e, 0xd8, 0x28, 0xd2, 0xa2,
0x8c, 0xcb, 0xad, 0x9e, 0xb4, 0x76, 0xfb, 0xaf, 0x6b, 0xd1, 0xfb, 0xd0, 0xef, 0x31, 0x2b, 0x17,
0xa9, 0xdc, 0x01, 0xa8, 0xd4, 0x90, 0x3f, 0xf8, 0x34, 0x6c, 0xb4, 0xa5, 0x60, 0x4a, 0xf3, 0xd9,
0xf5, 0x15, 0x6d, 0xde, 0x37, 0xd1, 0xab, 0xa2, 0xe7, 0x65, 0xd2, 0xda, 0x21, 0x9b, 0x34, 0x4b,
0x1d, 0x29, 0x24, 0xf2, 0xbe, 0x16, 0x04, 0x7a, 0xf8, 0x69, 0x5e, 0x35, 0xd6, 0xb1, 0x1e, 0x6e,
0xc5, 0xc1, 0x1e, 0xee, 0x61, 0xb6, 0x87, 0x1f, 0x2f, 0xcf, 0xb2, 0xb4, 0xba, 0x48, 0xf3, 0xb9,
0x4e, 0xf2, 0x7d, 0x5d, 0x2b, 0x86, 0x79, 0xfe, 0x46, 0x27, 0x87, 0x39, 0xd1, 0xc1, 0x42, 0x3a,
0x01, 0x61, 0xb2, 0xd1, 0xc9, 0xd9, 0xb5, 0x97, 0x95, 0xd6, 0x8b, 0x7e, 0xb0, 0xf6, 0x72, 0x54,
0x6b, 0x29, 0xb1, 0xf6, 0x6a, 0x53, 0x76, 0xed, 0xe5, 0xd6, 0xa1, 0xe2, 0xd9, 0x15, 0x3b, 0x2d,
0x53, 0xb0, 0xf6, 0xf2, 0xca, 0xd7, 0x30, 0xc4, 0xda, 0x8b, 0x62, 0xed, 0x40, 0x65, 0x89, 0x7d,
0x26, 0x26, 0x22, 0x16, 0xcb, 0x0a, 0x0c, 0x54, 0x8e, 0x0d, 0x83, 0x10, 0x03, 0x15, 0x81, 0x6a,
0x6f, 0xbf, 0x1f, 0x45, 0x6a, 0xbf, 0x44, 0xee, 0x69, 0xf9, 0x73, 0x8f, 0xde, 0x48, 0xf1, 0x36,
0xb4, 0xde, 0x0f, 0x10, 0x36, 0xbd, 0x52, 0x7f, 0x97, 0x5b, 0x75, 0x03, 0x54, 0x43, 0x8a, 0x88,
0xf4, 0x0a, 0x20, 0xb0, 0xa0, 0x93, 0x0b, 0xfe, 0x0a, 0x2f, 0x68, 0x2d, 0x09, 0x17, 0x54, 0x13,
0x76, 0xf3, 0x5c, 0x17, 0x14, 0xdb, 0x3c, 0x6f, 0x8a, 0x11, 0xda, 0x3c, 0x87, 0x8c, 0x8d, 0x19,
0xd7, 0xf0, 0x53, 0xce, 0x2f, 0x17, 0x71, 0x79, 0x09, 0x62, 0xc6, 0x53, 0x6e, 0x18, 0x22, 0x66,
0x28, 0xd6, 0xc6, 0x8c, 0xeb, 0xb0, 0x4e, 0xce, 0x4f, 0xcb, 0x0c, 0xc4, 0x8c, 0x67, 0x43, 0x23,
0x44, 0xcc, 0x10, 0xa8, 0x1d, 0x9d, 0x5c, 0x6f, 0x13, 0x06, 0xb7, 0x6b, 0x3c, 0xf5, 0x09, 0xa3,
0xb6, 0x6b, 0x10, 0x0c, 0x86, 0xd0, 0x7e, 0x19, 0x17, 0x17, 0x78, 0x08, 0x49, 0x51, 0x38, 0x84,
0x1a, 0x04, 0xb6, 0xf7, 0x84, 0xc5, 0xe5, 0xec, 0x02, 0x6f, 0x6f, 0x25, 0x0b, 0xb7, 0xb7, 0x61,
0x60, 0x7b, 0x2b, 0xc1, 0xcb, 0x54, 0x5c, 0x1c, 0x32, 0x11, 0xe3, 0xed, 0xed, 0x33, 0xe1, 0xf6,
0x6e, 0xb1, 0x36, 0xfb, 0x77, 0x1d, 0x4e, 0x96, 0x67, 0xd5, 0xac, 0x4c, 0xcf, 0xd8, 0x20, 0x60,
0xc5, 0x40, 0x44, 0xf6, 0x4f, 0xc2, 0xda, 0xe7, 0x4f, 0xd7, 0xa2, 0x9b, 0x4d, 0xb3, 0xf3, 0xaa,
0xd2, 0x73, 0x9f, 0xef, 0xfe, 0x63, 0xbc, 0x7d, 0x09, 0x9c, 0x78, 0x9d, 0xd1, 0x43, 0xcd, 0xc9,
0x0d, 0xf0, 0x22, 0x9d, 0xe6, 0x95, 0x29, 0xd4, 0xa7, 0x7d, 0xac, 0x3b, 0x0a, 0x44, 0x6e, 0xd0,
0x4b, 0xd1, 0xa6, 0x65, 0xba, 0x7d, 0x1a, 0xd9, 0x38, 0xa9, 0x40, 0x5a, 0xd6, 0x3c, 0x6f, 0x87,
0x20, 0xd2, 0x32, 0x9c, 0x84, 0xa1, 0xb0, 0x5f, 0xf2, 0x65, 0x51, 0x75, 0x84, 0x02, 0x80, 0xc2,
0xa1, 0xd0, 0x86, 0xb5, 0xcf, 0xd7, 0xd1, 0x6f, 0xba, 0xe1, 0xe7, 0x3e, 0xec, 0x2d, 0x3a, 0xa6,
0xb0, 0x47, 0x3c, 0xec, 0x8b, 0xdb, 0x8c, 0xa2, 0xf1, 0x2c, 0x76, 0x99, 0x88, 0xd3, 0xac, 0x1a,
0xac, 0xe3, 0x36, 0x1a, 0x39, 0x91, 0x51, 0x60, 0x1c, 0x1c, 0xdf, 0x76, 0x97, 0x45, 0x96, 0xce,
0xda, 0x2f, 0x93, 0xb4, 0xae, 0x11, 0x87, 0xc7, 0x37, 0x17, 0x83, 0xe3, 0x75, 0x9d, 0xfa, 0xc9,
0xff, 0x4c, 0x57, 0x05, 0xc3, 0xc7, 0x6b, 0x0f, 0x09, 0x8f, 0xd7, 0x10, 0x85, 0xf5, 0x99, 0x30,
0x71, 0x10, 0xaf, 0xf8, 0x92, 0x18, 0xaf, 0x8d, 0x38, 0x5c, 0x1f, 0x17, 0xb3, 0x6b, 0x03, 0xe3,
0x61, 0x9c, 0x0b, 0x56, 0xe6, 0x71, 0xb6, 0x97, 0xc5, 0xf3, 0x6a, 0x40, 0x8c, 0x31, 0x3e, 0x45,
0xac, 0x0d, 0x68, 0x1a, 0x79, 0x8c, 0xe3, 0x6a, 0x2f, 0xbe, 0xe2, 0x65, 0x2a, 0xe8, 0xc7, 0x68,
0x91, 0xce, 0xc7, 0xe8, 0xa1, 0xa8, 0xb7, 0x51, 0x39, 0xbb, 0x48, 0xaf, 0x58, 0x12, 0xf0, 0xd6,
0x20, 0x3d, 0xbc, 0x39, 0x28, 0xd2, 0x68, 0x13, 0xbe, 0x2c, 0x67, 0x8c, 0x6c, 0x34, 0x25, 0xee,
0x6c, 0x34, 0x83, 0x69, 0x0f, 0x7f, 0xb5, 0x16, 0xfd, 0x96, 0x92, 0xba, 0x6f, 0x78, 0x76, 0xe3,
0xea, 0xe2, 0x8c, 0xc7, 0x65, 0x32, 0xf8, 0x10, 0xb3, 0x83, 0xa2, 0xc6, 0xf5, 0xe3, 0xeb, 0xa8,
0xc0, 0xc7, 0x5a, 0xe7, 0xdd, 0xb6, 0xc7, 0xa1, 0x8f, 0xd5, 0x43, 0xc2, 0x8f, 0x15, 0xa2, 0x70,
0x00, 0x91, 0x72, 0xb5, 0x01, 0xb8, 0x4e, 0xea, 0xfb, 0xbb, 0x80, 0x1b, 0x9d, 0x1c, 0x1c, 0x1f,
0x6b, 0xa1, 0x1f, 0x2d, 0x5b, 0x94, 0x0d, 0x3c, 0x62, 0x86, 0x7d, 0x71, 0xd2, 0xb3, 0xe9, 0x15,
0x61, 0xcf, 0xad, 0x9e, 0x31, 0xec, 0x8b, 0x13, 0x9e, 0x9d, 0x61, 0x2d, 0xe4, 0x19, 0x19, 0xda,
0x86, 0x7d, 0x71, 0x98, 0x7d, 0x69, 0xa6, 0x99, 0x17, 0x1e, 0x04, 0xec, 0xc0, 0xb9, 0x61, 0xb3,
0x17, 0xab, 0x1d, 0xfe, 0xcd, 0x5a, 0xf4, 0x5d, 0xeb, 0xf1, 0x90, 0x27, 0xe9, 0xf9, 0x4a, 0x41,
0x2f, 0xe2, 0x6c, 0xc9, 0xaa, 0xc1, 0x63, 0xca, 0x5a, 0x9b, 0x35, 0x25, 0x78, 0x72, 0x2d, 0x1d,
0xd8, 0x77, 0x46, 0x45, 0x91, 0xad, 0xa6, 0x6c, 0x51, 0x64, 0x64, 0xdf, 0xf1, 0x90, 0x70, 0xdf,
0x81, 0x28, 0xcc, 0xca, 0xa7, 0xbc, 0xce, 0xf9, 0xd1, 0xac, 0x5c, 0x8a, 0xc2, 0x59, 0x79, 0x83,
0xc0, 0x5c, 0x69, 0xca, 0x77, 0x78, 0x96, 0xb1, 0x99, 0x68, 0x9f, 0x12, 0x31, 0x9a, 0x96, 0x08,
0xe7, 0x4a, 0x80, 0xb4, 0xbb, 0x72, 0xcd, 0x1a, 0x32, 0x2e, 0xd9, 0xd3, 0xd5, 0x41, 0x9a, 0x5f,
0x0e, 0xf0, 0xb4, 0xc0, 0x02, 0xc4, 0xae, 0x1c, 0x0a, 0xc2, 0xb5, 0xea, 0x69, 0x9e, 0x70, 0x7c,
0xad, 0x5a, 0x4b, 0xc2, 0x6b, 0x55, 0x4d, 0x40, 0x93, 0x27, 0x8c, 0x32, 0x59, 0x4b, 0xc2, 0x26,
0x35, 0x81, 0x0d, 0x85, 0xfa, 0x4d, 0x11, 0x39, 0x14, 0x82, 0x77, 0x43, 0x1b, 0x9d, 0x1c, 0x8c,
0xd0, 0x66, 0xd1, 0xba, 0xc7, 0xc4, 0xec, 0x02, 0x8f, 0x50, 0x0f, 0x09, 0x47, 0x28, 0x44, 0x61,
0x95, 0xa6, 0xdc, 0x2c, 0xba, 0xd7, 0xf1, 0xf8, 0x68, 0x2d, 0xb8, 0x37, 0x3a, 0x39, 0xb8, 0x8c,
0x1c, 0x2f, 0xe4, 0x33, 0x43, 0x83, 0x5c, 0xc9, 0xc2, 0xcb, 0x48, 0xc3, 0xc0, 0xd2, 0x2b, 0x81,
0xdc, 0xcb, 0x5a, 0xa7, 0x15, 0xbd, 0xdd, 0xac, 0x8d, 0x4e, 0x4e, 0x3b, 0xf9, 0x67, 0xb3, 0x8c,
0x53, 0xd2, 0x23, 0x5e, 0xf7, 0x91, 0x17, 0x71, 0x96, 0x26, 0xb1, 0x60, 0x53, 0x7e, 0xc9, 0x72,
0x7c, 0xc5, 0xa4, 0x4b, 0xab, 0xf8, 0xa1, 0xa7, 0x10, 0x5e, 0x31, 0x85, 0x15, 0x61, 0x9c, 0x28,
0xfa, 0xb4, 0x62, 0x3b, 0x71, 0x45, 0x8c, 0x64, 0x1e, 0x12, 0x8e, 0x13, 0x88, 0xc2, 0x7c, 0x55,
0xc9, 0x9f, 0xbd, 0x2e, 0x58, 0x99, 0xb2, 0x7c, 0xc6, 0xf0, 0x7c, 0x15, 0x52, 0xe1, 0x7c, 0x15,
0xa1, 0xe1, 0x5a, 0x6d, 0x37, 0x16, 0xec, 0xe9, 0x6a, 0x9a, 0x2e, 0x58, 0x25, 0xe2, 0x45, 0x81,
0xaf, 0xd5, 0x00, 0x14, 0x5e, 0xab, 0xb5, 0xe1, 0xd6, 0xd6, 0x90, 0x19, 0x10, 0xdb, 0x87, 0xcb,
0x20, 0x11, 0x38, 0x5c, 0x46, 0xa0, 0xf0, 0xc1, 0x5a, 0x00, 0x7d, 0x49, 0xd0, 0xb2, 0x12, 0x7c,
0x49, 0x40, 0xd3, 0xad, 0x0d, 0x37, 0xc3, 0x4c, 0xea, 0xae, 0xd9, 0x51, 0xf4, 0x89, 0xdb, 0x45,
0x37, 0x7b, 0xb1, 0xf8, 0x0e, 0xdf, 0x09, 0xcb, 0x62, 0x39, 0x6d, 0x05, 0xb6, 0xd1, 0x1a, 0xa6,
0xcf, 0x0e, 0x9f, 0xc3, 0x6a, 0x87, 0x7f, 0xb1, 0x16, 0xbd, 0x87, 0x79, 0x7c, 0x5e, 0x48, 0xbf,
0x8f, 0xba, 0x6d, 0x29, 0x92, 0x38, 0x3d, 0x17, 0xd6, 0xb0, 0x07, 0x40, 0x1a, 0x91, 0x3d, 0x5c,
0xa7, 0x0b, 0xe0, 0x27, 0x6d, 0xa6, 0xfc, 0x90, 0x23, 0x0e, 0x80, 0x84, 0x78, 0xbb, 0x1e, 0xf2,
0xcb, 0x55, 0x81, 0xf5, 0x90, 0xb1, 0xa1, 0xc5, 0xc4, 0x7a, 0x08, 0xc1, 0x6c, 0xef, 0x74, 0xab,
0xf7, 0x32, 0x15, 0x17, 0x32, 0xdf, 0x02, 0xbd, 0xd3, 0x2b, 0xab, 0x81, 0x88, 0xde, 0x49, 0xc2,
0x30, 0x23, 0x69, 0xc0, 0xba, 0x6f, 0x62, 0x63, 0xb9, 0x31, 0xe4, 0xf6, 0xcc, 0x7b, 0xdd, 0x20,
0x8c, 0xd7, 0x46, 0xac, 0x97, 0x3e, 0x0f, 0x42, 0x16, 0xc0, 0xf2, 0x67, 0xb3, 0x17, 0xab, 0x1d,
0xfe, 0x59, 0xf4, 0x9d, 0x56, 0xc5, 0xf6, 0x58, 0x2c, 0x96, 0x25, 0x4b, 0x06, 0xdb, 0x1d, 0xe5,
0x6e, 0x40, 0xe3, 0xfa, 0x51, 0x7f, 0x85, 0x56, 0x8e, 0xde, 0x70, 0x2a, 0xac, 0x4c, 0x19, 0x1e,
0x87, 0x4c, 0xfa, 0x6c, 0x30, 0x47, 0xa7, 0x75, 0x5a, 0xcb, 0x6c, 0x37, 0xba, 0x46, 0x57, 0x71,
0x9a, 0xc9, 0x97, 0xb5, 0x1f, 0x86, 0x8c, 0x7a, 0x68, 0x70, 0x99, 0x4d, 0xaa, 0xb4, 0x46, 0x66,
0xd9, 0xc7, 0x9d, 0xe5, 0xd9, 0x43, 0x7a, 0x24, 0x40, 0x56, 0x67, 0x5b, 0x3d, 0x69, 0xed, 0x56,
0x34, 0x53, 0x5e, 0xfd, 0x67, 0x37, 0xc8, 0x31, 0xaf, 0x5a, 0x15, 0x89, 0xf4, 0xad, 0x9e, 0xb4,
0xf6, 0xfa, 0xa7, 0xd1, 0xbb, 0x6d, 0xaf, 0x7a, 0x22, 0xda, 0xee, 0x34, 0x05, 0xe6, 0xa2, 0x47,
0xfd, 0x15, 0xb4, 0xfb, 0x7f, 0x33, 0xfb, 0xd2, 0xca, 0xff, 0x8c, 0x2f, 0x16, 0x2c, 0x4f, 0x58,
0xd2, 0x68, 0x54, 0xf5, 0xfa, 0xe9, 0x33, 0xda, 0xae, 0x51, 0x18, 0xba, 0x1a, 0xa6, 0x44, 0xbf,
0xfd, 0x0d, 0x34, 0x75, 0xd1, 0xfe, 0x6b, 0x2d, 0xba, 0x8f, 0x16, 0xad, 0x09, 0x5c, 0xaf, 0x88,
0xbf, 0xd7, 0xc7, 0x11, 0xa6, 0x69, 0x8a, 0x3a, 0xfa, 0x39, 0x2c, 0xe8, 0x22, 0xff, 0xfb, 0x5a,
0x74, 0xdb, 0x2a, 0xd6, 0xe1, 0xbd, 0xc3, 0xf3, 0xf3, 0x2c, 0x9d, 0x09, 0xf9, 0x46, 0x56, 0xab,
0xd0, 0x8f, 0x93, 0xd2, 0xe8, 0x7e, 0x9c, 0x01, 0x4d, 0xbb, 0x78, 0xfd, 0x3c, 0xad, 0x04, 0x2f,
0x57, 0x93, 0x0b, 0xfe, 0xaa, 0xf9, 0x3c, 0xc9, 0x1f, 0x97, 0x35, 0x30, 0x74, 0x08, 0x62, 0xf1,
0x8a, 0x93, 0x2d, 0x57, 0xf6, 0x33, 0xa6, 0x8a, 0x70, 0xe5, 0x10, 0x1d, 0xae, 0x7c, 0xd2, 0xce,
0x4a, 0x4d, 0xad, 0xec, 0x37, 0x57, 0x1b, 0x78, 0x51, 0xdb, 0xdf, 0x5d, 0xdd, 0xeb, 0x06, 0x6d,
0x6e, 0xaa, 0xc5, 0xbb, 0xe9, 0xf9, 0xb9, 0xa9, 0x13, 0x5e, 0x52, 0x17, 0x21, 0x72, 0x53, 0x02,
0xb5, 0xcb, 0xab, 0xbd, 0x34, 0x63, 0xf2, 0xdd, 0xcd, 0xf3, 0xf3, 0xf3, 0x8c, 0xc7, 0x09, 0x58,
0x5e, 0xd5, 0xe2, 0xa1, 0x2b, 0x27, 0x96, 0x57, 0x18, 0x67, 0x4f, 0x85, 0xd4, 0xd2, 0x3a, 0xba,
0xf3, 0x59, 0x9a, 0xc1, 0xd3, 0xcd, 0x52, 0xd3, 0x08, 0x89, 0x53, 0x21, 0x2d, 0xc8, 0xa6, 0x40,
0xb5, 0xa8, 0x8e, 0xca, 0xa6, 0xfc, 0x77, 0xdb, 0x8a, 0x8e, 0x98, 0x48, 0x81, 0x10, 0xcc, 0xee,
0x32, 0xd4, 0xc2, 0xd3, 0x42, 0x1a, 0xbf, 0xd5, 0xd6, 0x52, 0x12, 0x62, 0x97, 0xc1, 0x27, 0xec,
0x6a, 0xb9, 0xfe, 0xfb, 0x2e, 0x7f, 0x95, 0x4b, 0xa3, 0xb7, 0xdb, 0x2a, 0x8d, 0x8c, 0x58, 0x2d,
0x43, 0x46, 0x1b, 0xfe, 0x22, 0xfa, 0x45, 0x69, 0xb8, 0xe4, 0xc5, 0xe0, 0x06, 0xa2, 0x50, 0x3a,
0x67, 0x81, 0x6f, 0x92, 0x72, 0x7b, 0x88, 0xc4, 0xc4, 0xc6, 0x69, 0x15, 0xcf, 0xe1, 0x01, 0x7e,
0xdb, 0xe2, 0x52, 0x4a, 0x1c, 0x22, 0x69, 0x53, 0x7e, 0x54, 0x1c, 0xf1, 0x44, 0x5b, 0x47, 0x6a,
0x68, 0x84, 0xa1, 0xa8, 0x70, 0x21, 0x9b, 0xb6, 0x1e, 0xc5, 0x57, 0xe9, 0xdc, 0xa4, 0x16, 0x6a,
0x00, 0xab, 0x40, 0xda, 0x6a, 0x99, 0xa1, 0x03, 0x11, 0x69, 0x2b, 0x09, 0x6b, 0x9f, 0xff, 0xb4,
0x16, 0xdd, 0xb2, 0xcc, 0x7e, 0xb3, 0x2f, 0x3b, 0xce, 0xcf, 0x79, 0x9d, 0xe4, 0x1e, 0xa4, 0xf9,
0x65, 0x35, 0xf8, 0x84, 0x32, 0x89, 0xf3, 0xa6, 0x28, 0x9f, 0x5e, 0x5b, 0xcf, 0xae, 0x4f, 0x9a,
0x4d, 0x4b, 0x7b, 0x72, 0x41, 0x69, 0x80, 0xf5, 0x89, 0xd9, 0xdb, 0x84, 0x1c, 0xb1, 0x3e, 0x09,
0xf1, 0xb6, 0x89, 0x8d, 0xf3, 0x8c, 0xe7, 0xb0, 0x89, 0xad, 0x85, 0x5a, 0x48, 0x34, 0x71, 0x0b,
0xb2, 0xe3, 0x71, 0x23, 0x52, 0xfb, 0x6b, 0xa3, 0x2c, 0x03, 0xe3, 0xb1, 0x51, 0x35, 0x00, 0x31,
0x1e, 0xa3, 0xa0, 0xf6, 0x73, 0x12, 0x7d, 0xab, 0x7e, 0xa4, 0xc7, 0x25, 0xbb, 0x4a, 0x19, 0x3c,
0x64, 0xe3, 0x48, 0x88, 0xfe, 0xef, 0x13, 0xb6, 0x67, 0x9d, 0xe6, 0x55, 0x91, 0xc5, 0xd5, 0x85,
0x3e, 0x76, 0xe1, 0xd7, 0xb9, 0x11, 0xc2, 0x83, 0x17, 0x77, 0x3b, 0x28, 0x3b, 0xa8, 0x37, 0x32,
0x33, 0xc4, 0xac, 0xe3, 0xaa, 0xad, 0x61, 0x66, 0xa3, 0x93, 0xb3, 0xef, 0x36, 0xf6, 0xe3, 0x2c,
0x63, 0xe5, 0xaa, 0x91, 0x1d, 0xc6, 0x79, 0x7a, 0xce, 0x2a, 0x01, 0xde, 0x6d, 0x68, 0x6a, 0x08,
0x31, 0xe2, 0xdd, 0x46, 0x00, 0xb7, 0xeb, 0x36, 0xe0, 0x79, 0x9c, 0x27, 0xec, 0x35, 0x58, 0xb7,
0x41, 0x3b, 0x92, 0x21, 0xd6, 0x6d, 0x14, 0x6b, 0xf7, 0xf8, 0x9f, 0x66, 0x7c, 0x76, 0xa9, 0xa7,
0x00, 0xbf, 0x81, 0xa5, 0x04, 0xce, 0x01, 0xb7, 0x43, 0x88, 0x9d, 0x04, 0xa4, 0xe0, 0x84, 0x15,
0x59, 0x3c, 0x83, 0x27, 0xad, 0x94, 0x8e, 0x96, 0x11, 0x93, 0x00, 0x64, 0x40, 0x71, 0xf5, 0x09,
0x2e, 0xac, 0xb8, 0xe0, 0x00, 0xd7, 0xed, 0x10, 0x62, 0xa7, 0x41, 0x29, 0x98, 0x14, 0x59, 0x2a,
0x40, 0x37, 0x50, 0x1a, 0x52, 0x42, 0x74, 0x03, 0x9f, 0x00, 0x26, 0x0f, 0x59, 0x39, 0x67, 0xa8,
0x49, 0x29, 0x09, 0x9a, 0x6c, 0x08, 0x7b, 0xac, 0x5c, 0xd5, 0x9d, 0x17, 0x2b, 0x70, 0xac, 0x5c,
0x57, 0x8b, 0x17, 0x2b, 0xe2, 0x58, 0xb9, 0x07, 0x80, 0x22, 0x1e, 0xc7, 0x95, 0xc0, 0x8b, 0x28,
0x25, 0xc1, 0x22, 0x36, 0x84, 0x9d, 0xa3, 0x55, 0x11, 0x97, 0x02, 0xcc, 0xd1, 0xba, 0x00, 0xce,
0x59, 0x83, 0x9b, 0xa4, 0xdc, 0x8e, 0x24, 0xaa, 0x55, 0x98, 0xd8, 0x4b, 0x59, 0x96, 0x54, 0x60,
0x24, 0xd1, 0xcf, 0xbd, 0x91, 0x12, 0x23, 0x49, 0x9b, 0x02, 0xa1, 0xa4, 0xdf, 0x84, 0x60, 0xb5,
0x03, 0x2f, 0x41, 0x6e, 0x87, 0x10, 0x3b, 0x3e, 0x35, 0x85, 0xde, 0x89, 0xcb, 0x32, 0xad, 0x27,
0xff, 0x75, 0xbc, 0x40, 0x8d, 0x9c, 0x18, 0x9f, 0x30, 0x0e, 0x74, 0xaf, 0x66, 0xe0, 0xc6, 0x0a,
0x06, 0x87, 0xee, 0x0f, 0x82, 0x8c, 0xcd, 0x38, 0xa5, 0xc4, 0x79, 0x59, 0x8e, 0x3d, 0x4d, 0xe4,
0x5d, 0xf9, 0x7a, 0x17, 0xe6, 0x7c, 0xe1, 0x66, 0x5c, 0x1c, 0xf2, 0x2b, 0x36, 0xe5, 0xcf, 0x5e,
0xa7, 0x55, 0xbd, 0xdc, 0xd2, 0x33, 0xf7, 0x13, 0xc2, 0x12, 0x06, 0x13, 0x5f, 0xb8, 0x75, 0x2a,
0xd9, 0x04, 0x02, 0x94, 0xe5, 0x88, 0xbd, 0x42, 0x13, 0x08, 0x68, 0xd1, 0x70, 0x44, 0x02, 0x11,
0xe2, 0xed, 0x8e, 0x99, 0x71, 0xae, 0xaf, 0x35, 0x98, 0xf2, 0x26, 0x97, 0xa3, 0xac, 0x41, 0x90,
0xd8, 0xb4, 0x08, 0x2a, 0xd8, 0xf5, 0xa5, 0xf1, 0x6f, 0xbb, 0xd8, 0x3d, 0xc2, 0x4e, 0xbb, 0x9b,
0xdd, 0xef, 0x41, 0x22, 0xae, 0xec, 0x89, 0x0f, 0xca, 0x55, 0xfb, 0xc0, 0xc7, 0xfd, 0x1e, 0xa4,
0xb3, 0xfb, 0xe6, 0x56, 0xeb, 0x69, 0x3c, 0xbb, 0x9c, 0x97, 0x7c, 0x99, 0x27, 0x3b, 0x3c, 0xe3,
0x25, 0xd8, 0x7d, 0xf3, 0x4a, 0x0d, 0x50, 0x62, 0xf7, 0xad, 0x43, 0xc5, 0x66, 0x70, 0x6e, 0x29,
0x46, 0x59, 0x3a, 0x87, 0x2b, 0x6a, 0xcf, 0x90, 0x04, 0x88, 0x0c, 0x0e, 0x05, 0x91, 0x20, 0x52,
0x2b, 0x6e, 0x91, 0xce, 0xe2, 0x4c, 0xf9, 0xdb, 0xa6, 0xcd, 0x78, 0x60, 0x67, 0x10, 0x21, 0x0a,
0x48, 0x3d, 0xa7, 0xcb, 0x32, 0x1f, 0xe7, 0x82, 0x93, 0xf5, 0x6c, 0x80, 0xce, 0x7a, 0x3a, 0x20,
0x18, 0x56, 0xa7, 0xec, 0x75, 0x5d, 0x9a, 0xfa, 0x1f, 0x6c, 0x58, 0xad, 0xff, 0x3e, 0xd4, 0xf2,
0xd0, 0xb0, 0x0a, 0x38, 0x50, 0x19, 0xed, 0x44, 0x05, 0x4c, 0x40, 0xdb, 0x0f, 0x93, 0x7b, 0xdd,
0x20, 0xee, 0x67, 0x22, 0x56, 0x19, 0x0b, 0xf9, 0x91, 0x40, 0x1f, 0x3f, 0x0d, 0x68, 0xb7, 0x5b,
0xbc, 0xfa, 0x5c, 0xb0, 0xd9, 0x65, 0xeb, 0x00, 0x9b, 0x5f, 0x50, 0x85, 0x10, 0xdb, 0x2d, 0x04,
0x8a, 0x37, 0xd1, 0x78, 0xc6, 0xf3, 0x50, 0x13, 0xd5, 0xf2, 0x3e, 0x4d, 0xa4, 0x39, 0xbb, 0xf8,
0x35, 0x52, 0x1d, 0x99, 0xaa, 0x99, 0x36, 0x09, 0x0b, 0x2e, 0x44, 0x2c, 0x7e, 0x49, 0xd8, 0xe6,
0xe4, 0xd0, 0xe7, 0x61, 0xfb, 0x74, 0x7f, 0xcb, 0xca, 0x21, 0x7d, 0xba, 0x9f, 0x62, 0xe9, 0x4a,
0xaa, 0x18, 0xe9, 0xb0, 0xe2, 0xc7, 0xc9, 0xc3, 0x7e, 0xb0, 0x5d, 0xf2, 0x78, 0x3e, 0x77, 0x32,
0x16, 0x97, 0xca, 0xeb, 0x56, 0xc0, 0x90, 0xc5, 0x88, 0x25, 0x4f, 0x00, 0x07, 0x43, 0x98, 0xe7,
0x79, 0x87, 0xe7, 0x82, 0xe5, 0x02, 0x1b, 0xc2, 0x7c, 0x63, 0x1a, 0x0c, 0x0d, 0x61, 0x94, 0x02,
0x88, 0x5b, 0xb9, 0x1f, 0xc4, 0xc4, 0x51, 0xbc, 0x40, 0x33, 0x36, 0xb5, 0xd7, 0xa3, 0xe4, 0xa1,
0xb8, 0x05, 0x9c, 0xf3, 0x3a, 0xd7, 0xf5, 0x32, 0x8d, 0xcb, 0xb9, 0xd9, 0xdd, 0x48, 0x06, 0x8f,
0x68, 0x3b, 0x3e, 0x49, 0xbc, 0xce, 0x0d, 0x6b, 0x80, 0x61, 0x67, 0xbc, 0x88, 0xe7, 0xa6, 0xa6,
0x48, 0x0d, 0xa4, 0xbc, 0x55, 0xd5, 0x7b, 0xdd, 0x20, 0xf0, 0xf3, 0x22, 0x4d, 0x18, 0x0f, 0xf8,
0x91, 0xf2, 0x3e, 0x7e, 0x20, 0x08, 0xb2, 0xb7, 0xba, 0xde, 0x6a, 0x45, 0x37, 0xca, 0x13, 0xbd,
0x8e, 0x1d, 0x12, 0x8f, 0x07, 0x70, 0xa1, 0xec, 0x8d, 0xe0, 0x41, 0x1f, 0x6d, 0x36, 0x68, 0x43,
0x7d, 0xd4, 0xec, 0xbf, 0xf6, 0xe9, 0xa3, 0x18, 0xac, 0x7d, 0xfe, 0x58, 0xf7, 0xd1, 0xdd, 0x58,
0xc4, 0x75, 0xde, 0xfe, 0x22, 0x65, 0xaf, 0xf4, 0x42, 0x18, 0xa9, 0x6f, 0x43, 0x0d, 0xe5, 0xc7,
0xc9, 0x60, 0x55, 0xbc, 0xdd, 0x9b, 0x0f, 0xf8, 0xd6, 0x2b, 0x84, 0x4e, 0xdf, 0x60, 0xa9, 0xb0,
0xdd, 0x9b, 0x0f, 0xf8, 0xd6, 0x77, 0x2c, 0x74, 0xfa, 0x06, 0x17, 0x2d, 0x6c, 0xf7, 0xe6, 0xb5,
0xef, 0xbf, 0x6c, 0x3a, 0xae, 0xeb, 0xbc, 0xce, 0xc3, 0x66, 0x22, 0xbd, 0x62, 0x58, 0x3a, 0xe9,
0xdb, 0x33, 0x68, 0x28, 0x9d, 0xa4, 0x55, 0x9c, 0x5b, 0xce, 0xb0, 0x52, 0x1c, 0xf3, 0x2a, 0x95,
0xc7, 0x31, 0x9e, 0xf4, 0x30, 0xda, 0xc0, 0xa1, 0x45, 0x53, 0x48, 0xc9, 0xbe, 0x58, 0xf6, 0x50,
0x7b, 0x5e, 0xfd, 0x61, 0xc0, 0x5e, 0xfb, 0xd8, 0xfa, 0x56, 0x4f, 0xda, 0xbe, 0xe2, 0xf5, 0x98,
0xe6, 0xe5, 0xdc, 0x84, 0xa1, 0xb3, 0x84, 0x31, 0x65, 0x5e, 0xda, 0xba, 0x6f, 0x29, 0x1f, 0xf5,
0x57, 0xe8, 0x70, 0x3f, 0x4a, 0x92, 0x7e, 0xee, 0xdd, 0xb7, 0xdb, 0x8f, 0xfa, 0x2b, 0x68, 0xf7,
0x7f, 0xdd, 0x2c, 0x6b, 0xa0, 0x7f, 0xdd, 0x07, 0x1f, 0xf7, 0xb1, 0x08, 0xfa, 0xe1, 0x93, 0x6b,
0xe9, 0xe8, 0x82, 0xfc, 0xa4, 0x59, 0xbf, 0x37, 0xa8, 0xfc, 0x68, 0x48, 0x7e, 0x6c, 0xac, 0xbb,
0x64, 0x28, 0xaa, 0x2c, 0x0c, 0x3b, 0xe6, 0xc7, 0xd7, 0xd4, 0x72, 0xae, 0xdc, 0xf3, 0x60, 0xfd,
0x71, 0xab, 0x53, 0x9e, 0x90, 0x65, 0x87, 0x86, 0x05, 0xfa, 0xe4, 0xba, 0x6a, 0x54, 0x57, 0x75,
0x60, 0x79, 0xe9, 0xcc, 0x93, 0x9e, 0x86, 0xbd, 0x6b, 0x68, 0x3e, 0xba, 0x9e, 0x92, 0x2e, 0xcb,
0x7f, 0xae, 0x45, 0x77, 0x3d, 0xd6, 0xbe, 0xce, 0x00, 0x9b, 0x2e, 0x3f, 0x08, 0xd8, 0xa7, 0x94,
0x4c, 0xe1, 0x7e, 0xe7, 0x9b, 0x29, 0xdb, 0xfb, 0xe9, 0x3c, 0x95, 0xbd, 0x34, 0x13, 0xac, 0x6c,
0xdf, 0x4f, 0xe7, 0xdb, 0x55, 0xd4, 0x90, 0xbe, 0x9f, 0x2e, 0x80, 0x3b, 0xf7, 0xd3, 0x21, 0x9e,
0xd1, 0xfb, 0xe9, 0x50, 0x6b, 0xc1, 0xfb, 0xe9, 0xc2, 0x1a, 0xd4, 0xec, 0xd2, 0x14, 0x41, 0x6d,
0x9b, 0xf7, 0xb2, 0xe8, 0xef, 0xa2, 0x3f, 0xbe, 0x8e, 0x0a, 0x31, 0xbf, 0x2a, 0x4e, 0x1e, 0xa8,
0xec, 0xf1, 0x4c, 0xbd, 0x43, 0x95, 0xdb, 0xbd, 0x79, 0xed, 0xfb, 0x47, 0x7a, 0x71, 0x65, 0x66,
0x13, 0x5e, 0xca, 0xbb, 0x09, 0x37, 0x43, 0xb3, 0x43, 0x6d, 0xc1, 0x6d, 0xf9, 0x87, 0xfd, 0x60,
0xa2, 0xba, 0x35, 0xa1, 0x1b, 0x7d, 0xd8, 0x65, 0x08, 0x34, 0xf9, 0x76, 0x6f, 0x9e, 0x98, 0x46,
0x94, 0x6f, 0xd5, 0xda, 0x3d, 0x8c, 0xf9, 0x6d, 0xfd, 0xa8, 0xbf, 0x82, 0x76, 0x7f, 0xa5, 0xb3,
0x56, 0xd7, 0xbd, 0x6c, 0xe7, 0xad, 0x2e, 0x53, 0x13, 0xaf, 0x99, 0x87, 0x7d, 0xf1, 0x50, 0xfe,
0xe2, 0x4e, 0xa1, 0x5d, 0xf9, 0x0b, 0x3a, 0x8d, 0x7e, 0x74, 0x3d, 0x25, 0x5d, 0x96, 0x7f, 0x5c,
0x8b, 0x6e, 0x92, 0x65, 0xd1, 0x71, 0xf0, 0x49, 0x5f, 0xcb, 0x20, 0x1e, 0x3e, 0xbd, 0xb6, 0x9e,
0x2e, 0xd4, 0xbf, 0xac, 0x45, 0xb7, 0x02, 0x85, 0x52, 0x01, 0x72, 0x0d, 0xeb, 0x7e, 0xa0, 0x7c,
0x76, 0x7d, 0x45, 0x6a, 0xba, 0x77, 0xf1, 0x49, 0xfb, 0xae, 0xb1, 0x80, 0xed, 0x09, 0x7d, 0xd7,
0x58, 0xb7, 0x16, 0xdc, 0x63, 0x8a, 0xcf, 0x9a, 0x35, 0x1f, 0xba, 0xc7, 0x24, 0x8f, 0x42, 0x06,
0x6f, 0x31, 0xc1, 0x38, 0xcc, 0xc9, 0xb3, 0xd7, 0x45, 0x9c, 0x27, 0xb4, 0x13, 0x25, 0xef, 0x76,
0x62, 0x38, 0xb8, 0x37, 0x57, 0x4b, 0x4f, 0x78, 0xb3, 0x8e, 0xbb, 0x4f, 0xe9, 0x1b, 0x24, 0xb8,
0x37, 0xd7, 0x42, 0x09, 0x6f, 0x3a, 0x6b, 0x0c, 0x79, 0x03, 0xc9, 0xe2, 0x83, 0x3e, 0x28, 0x58,
0x21, 0x18, 0x6f, 0x66, 0xcb, 0xff, 0x61, 0xc8, 0x4a, 0x6b, 0xdb, 0x7f, 0xab, 0x27, 0x4d, 0xb8,
0x9d, 0x30, 0xf1, 0x39, 0x8b, 0x13, 0x56, 0x06, 0xdd, 0x1a, 0xaa, 0x97, 0x5b, 0x97, 0xc6, 0xdc,
0xee, 0xf0, 0x6c, 0xb9, 0xc8, 0x75, 0x63, 0x92, 0x6e, 0x5d, 0xaa, 0xdb, 0x2d, 0xa0, 0xe1, 0xae,
0xa4, 0x75, 0x2b, 0xd3, 0xcb, 0x07, 0x61, 0x33, 0x5e, 0x56, 0xb9, 0xd9, 0x8b, 0xa5, 0xeb, 0xa9,
0xc3, 0xa8, 0xa3, 0x9e, 0x20, 0x92, 0xb6, 0x7a, 0xd2, 0x70, 0x7b, 0xd0, 0x71, 0x6b, 0xe2, 0x69,
0xbb, 0xc3, 0x56, 0x2b, 0xa4, 0x1e, 0xf5, 0x57, 0x80, 0x9b, 0xb1, 0x3a, 0xaa, 0x0e, 0xd2, 0x4a,
0xec, 0xa5, 0x59, 0x36, 0xd8, 0x0c, 0x84, 0x49, 0x03, 0x05, 0x37, 0x63, 0x11, 0x98, 0x88, 0xe4,
0x66, 0xf3, 0x32, 0x1f, 0x74, 0xd9, 0x91, 0x54, 0xaf, 0x48, 0x76, 0x69, 0xb0, 0xa1, 0xe6, 0x3c,
0x6a, 0x53, 0xdb, 0x61, 0xf8, 0xc1, 0xb5, 0x2a, 0xbc, 0xdd, 0x9b, 0x07, 0x6f, 0xfb, 0x25, 0x25,
0x67, 0x96, 0x3b, 0x94, 0x09, 0x6f, 0x26, 0xb9, 0xdb, 0x41, 0x81, 0x4d, 0x49, 0xd5, 0x8d, 0x5e,
0xa6, 0xc9, 0x9c, 0x09, 0xf4, 0x45, 0x95, 0x0b, 0x04, 0x5f, 0x54, 0x01, 0x10, 0x34, 0x9d, 0xfa,
0xbb, 0xd9, 0x8d, 0x1d, 0x27, 0x58, 0xd3, 0x69, 0x65, 0x87, 0x0a, 0x35, 0x1d, 0x4a, 0x83, 0xd1,
0xc0, 0xb8, 0xd5, 0xf7, 0x3e, 0x3c, 0x08, 0x99, 0x01, 0x97, 0x3f, 0x6c, 0xf6, 0x62, 0xc1, 0x8c,
0x62, 0x1d, 0xa6, 0x8b, 0x54, 0x60, 0x33, 0x8a, 0x63, 0xa3, 0x46, 0x42, 0x33, 0x4a, 0x1b, 0xa5,
0xaa, 0x57, 0xe7, 0x08, 0xe3, 0x24, 0x5c, 0x3d, 0xc5, 0xf4, 0xab, 0x9e, 0x61, 0x5b, 0xef, 0x55,
0x73, 0x13, 0x32, 0xe2, 0x42, 0x2f, 0x96, 0x91, 0xd8, 0x96, 0xdf, 0x03, 0x43, 0x30, 0x34, 0xea,
0x50, 0x0a, 0xf0, 0x7d, 0x41, 0xcd, 0x35, 0xaf, 0x7e, 0x8b, 0x82, 0xc5, 0x65, 0x9c, 0xcf, 0xd0,
0xc5, 0xa9, 0x34, 0xd8, 0x22, 0x43, 0x8b, 0x53, 0x52, 0x03, 0xbc, 0xb5, 0xf7, 0xbf, 0xe4, 0x45,
0xba, 0x82, 0xf9, 0x64, 0xd6, 0xff, 0x90, 0xf7, 0x7e, 0x0f, 0x12, 0xbe, 0xb5, 0x6f, 0x00, 0xb3,
0xef, 0xae, 0x9c, 0x7e, 0x18, 0x30, 0xe5, 0xa3, 0xa1, 0x85, 0x30, 0xad, 0x02, 0x82, 0xda, 0xd9,
0x5b, 0xfc, 0x82, 0xad, 0xb0, 0xa0, 0x76, 0x37, 0x09, 0xbf, 0x60, 0xab, 0x50, 0x50, 0xb7, 0x51,
0x90, 0x67, 0xba, 0xeb, 0xa0, 0xf5, 0x80, 0xbe, 0xbb, 0xf4, 0xd9, 0xe8, 0xe4, 0x40, 0xcf, 0xd9,
0x4d, 0xaf, 0xbc, 0xd7, 0x14, 0x48, 0x41, 0x77, 0xd3, 0x2b, 0xfc, 0x2d, 0xc5, 0x66, 0x2f, 0x16,
0x9e, 0x08, 0x88, 0x05, 0x7b, 0xdd, 0xbc, 0xaa, 0x47, 0x8a, 0x2b, 0xe5, 0xad, 0x77, 0xf5, 0xf7,
0xba, 0x41, 0x7b, 0xfe, 0xf6, 0xb8, 0xe4, 0x33, 0x56, 0x55, 0xfa, 0x4a, 0x54, 0xff, 0x80, 0x93,
0x96, 0x0d, 0xc1, 0x85, 0xa8, 0x77, 0xc2, 0x90, 0x73, 0x8f, 0xa1, 0x12, 0xd9, 0xeb, 0x95, 0xd6,
0x51, 0xcd, 0xf6, 0xcd, 0x4a, 0x1b, 0x9d, 0x9c, 0xed, 0x5e, 0x5a, 0xea, 0xde, 0xa7, 0x74, 0x0f,
0x55, 0xc7, 0xae, 0x52, 0xba, 0xdf, 0x83, 0xd4, 0xae, 0x3e, 0x8f, 0xde, 0x3c, 0xe0, 0xf3, 0x09,
0xcb, 0x93, 0xc1, 0xf7, 0xfc, 0x13, 0xbc, 0x7c, 0x3e, 0xac, 0xff, 0x6c, 0x8c, 0xde, 0xa0, 0xc4,
0xf6, 0x0c, 0xe2, 0x2e, 0x3b, 0x5b, 0xce, 0x27, 0x22, 0x16, 0xe0, 0x0c, 0xa2, 0xfc, 0xfb, 0xb0,
0x16, 0x10, 0x67, 0x10, 0x3d, 0x00, 0xd8, 0x9b, 0x96, 0x8c, 0xa1, 0xf6, 0x6a, 0x41, 0xd0, 0x9e,
0x06, 0x6c, 0x16, 0x61, 0xec, 0xd5, 0x89, 0x3a, 0x3c, 0x33, 0x68, 0x75, 0xa4, 0x94, 0xc8, 0x22,
0xda, 0x94, 0x0d, 0x6e, 0x55, 0x7d, 0x79, 0xbd, 0xcd, 0x72, 0xb1, 0x88, 0xcb, 0x15, 0x08, 0x6e,
0x5d, 0x4b, 0x07, 0x20, 0x82, 0x1b, 0x05, 0x6d, 0xaf, 0x6d, 0x1e, 0xf3, 0xec, 0x72, 0x9f, 0x97,
0x7c, 0x29, 0xd2, 0x9c, 0xc1, 0x2b, 0x4e, 0xcc, 0x03, 0x75, 0x19, 0xa2, 0xd7, 0x52, 0xac, 0xcd,
0x72, 0x25, 0xa1, 0x8e, 0x33, 0xca, 0x3b, 0xe1, 0x2b, 0xc1, 0x4b, 0xf8, 0x3a, 0x53, 0x59, 0x81,
0x10, 0x91, 0xe5, 0x92, 0x30, 0x68, 0xfb, 0xe3, 0x34, 0x9f, 0xa3, 0x6d, 0x7f, 0xec, 0x5e, 0x33,
0x7c, 0x8b, 0x06, 0x6c, 0x87, 0x52, 0x0f, 0x4d, 0x75, 0x00, 0xfd, 0xd1, 0x30, 0xfa, 0xd0, 0x5d,
0x82, 0xe8, 0x50, 0x38, 0x09, 0x5c, 0x3d, 0x2f, 0x58, 0xce, 0x92, 0xe6, 0xd0, 0x1e, 0xe6, 0xca,
0x23, 0x82, 0xae, 0x20, 0x69, 0xc7, 0x22, 0x29, 0x3f, 0x59, 0xe6, 0xc7, 0x25, 0x3f, 0x4f, 0x33,
0x56, 0x82, 0xb1, 0x48, 0xa9, 0x3b, 0x72, 0x62, 0x2c, 0xc2, 0x38, 0x7b, 0xfa, 0x43, 0x4a, 0xbd,
0x1f, 0x36, 0x98, 0x96, 0xf1, 0x0c, 0x9e, 0xfe, 0x50, 0x36, 0xda, 0x18, 0xb1, 0x33, 0x18, 0xc0,
0x9d, 0x44, 0x47, 0xb9, 0xce, 0x57, 0x32, 0x3e, 0xf4, 0x47, 0xab, 0xf2, 0xf2, 0xdd, 0x0a, 0x24,
0x3a, 0xda, 0x1c, 0x46, 0x12, 0x89, 0x4e, 0x58, 0xc3, 0x4e, 0x25, 0x92, 0x3b, 0xd2, 0xa7, 0x9a,
0xc0, 0x54, 0xa2, 0x6c, 0x34, 0x42, 0x62, 0x2a, 0x69, 0x41, 0x60, 0x40, 0x6a, 0xba, 0xc1, 0x1c,
0x1d, 0x90, 0x8c, 0x34, 0x38, 0x20, 0xb9, 0x94, 0x1d, 0x28, 0xc6, 0x79, 0x2a, 0xd2, 0x38, 0x9b,
0x30, 0x71, 0x1c, 0x97, 0xf1, 0x82, 0x09, 0x56, 0xc2, 0x81, 0x42, 0x23, 0x43, 0x8f, 0x21, 0x06,
0x0a, 0x8a, 0xd5, 0x0e, 0x7f, 0x37, 0x7a, 0xbb, 0x9e, 0xf7, 0x59, 0xae, 0x7f, 0x92, 0xe9, 0x99,
0xfc, 0x2d, 0xb7, 0xc1, 0x3b, 0xc6, 0xc6, 0x44, 0x94, 0x2c, 0x5e, 0x34, 0xb6, 0xdf, 0x32, 0x7f,
0x97, 0xe0, 0xa3, 0xb5, 0x3a, 0x9e, 0x8f, 0xb8, 0x48, 0xcf, 0xeb, 0x65, 0xb6, 0xfe, 0x80, 0x09,
0xc4, 0xb3, 0x2b, 0x1e, 0x06, 0x2e, 0x3d, 0xc1, 0x38, 0x3b, 0x4e, 0xbb, 0xd2, 0x13, 0x56, 0x64,
0x70, 0x9c, 0xf6, 0xb4, 0x25, 0x40, 0x8c, 0xd3, 0x28, 0x68, 0x3b, 0xa7, 0x2b, 0x9e, 0xb2, 0x70,
0x65, 0xa6, 0xac, 0x5f, 0x65, 0xa6, 0xde, 0x37, 0x21, 0x59, 0xf4, 0xf6, 0x21, 0x5b, 0x9c, 0xb1,
0xb2, 0xba, 0x48, 0x0b, 0xea, 0x82, 0x60, 0x4b, 0x74, 0x5e, 0x10, 0x4c, 0xa0, 0x76, 0x26, 0xb0,
0xc0, 0xb8, 0x3a, 0x8a, 0x17, 0x4c, 0x5e, 0xe1, 0x02, 0x66, 0x02, 0xc7, 0x88, 0x03, 0x11, 0x33,
0x01, 0x09, 0x3b, 0x9f, 0x97, 0x59, 0xe6, 0x84, 0xcd, 0xeb, 0x08, 0x2b, 0x8f, 0xe3, 0xd5, 0x82,
0xe5, 0x42, 0x9b, 0x04, 0x7b, 0xf2, 0x8e, 0x49, 0x9c, 0x27, 0xf6, 0xe4, 0xfb, 0xe8, 0x39, 0x43,
0x93, 0xf7, 0xe0, 0x8f, 0x79, 0x29, 0xd4, 0x0f, 0xae, 0x9d, 0x96, 0x19, 0x18, 0x9a, 0xfc, 0x87,
0xea, 0x91, 0xc4, 0xd0, 0x14, 0xd6, 0x70, 0x7e, 0x5c, 0xc3, 0x2b, 0xc3, 0x0b, 0x56, 0x9a, 0x38,
0x79, 0xb6, 0x88, 0xd3, 0x4c, 0x47, 0xc3, 0xf7, 0x03, 0xb6, 0x09, 0x1d, 0xe2, 0xc7, 0x35, 0xfa,
0xea, 0x3a, 0x3f, 0x47, 0x12, 0x2e, 0x21, 0x78, 0x45, 0xd0, 0x61, 0x9f, 0x78, 0x45, 0xd0, 0xad,
0x65, 0x57, 0xee, 0x96, 0x95, 0xdc, 0x4a, 0x12, 0x3b, 0x3c, 0x81, 0xfb, 0x85, 0x8e, 0x4d, 0x00,
0x12, 0x2b, 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, 0xa1,
0xff, 0x5e, 0xe0, 0xb9, 0x49, 0xa2, 0xdb, 0x95, 0x43, 0x3a, 0x97, 0x01, 0xc3, 0xc7, 0x3a, 0x2a,
0x8a, 0x49, 0x3d, 0xab, 0x9e, 0xb0, 0x19, 0x4b, 0x0b, 0x31, 0xf8, 0x38, 0xfc, 0xac, 0x00, 0x4e,
0x1c, 0xb4, 0xe8, 0xa1, 0xe6, 0xbc, 0xbe, 0xaf, 0xc7, 0x92, 0x89, 0xfa, 0x25, 0xd2, 0xd3, 0x8a,
0x95, 0x3a, 0xd1, 0xd8, 0x67, 0x02, 0xf4, 0x4e, 0x87, 0x1b, 0x3a, 0x60, 0x5d, 0x51, 0xa2, 0x77,
0x86, 0x35, 0xec, 0x66, 0x9f, 0xc3, 0xe9, 0xcb, 0xdd, 0xe5, 0x71, 0xc7, 0x87, 0xa4, 0x31, 0x87,
0x22, 0x36, 0xfb, 0x68, 0xda, 0x66, 0x6b, 0x6d, 0xb7, 0xa3, 0x7c, 0x35, 0x86, 0x47, 0x26, 0x10,
0x4b, 0x12, 0x23, 0xb2, 0xb5, 0x00, 0xee, 0x6c, 0x86, 0x97, 0x3c, 0x4e, 0x66, 0x71, 0x25, 0x8e,
0xe3, 0x55, 0xc6, 0xe3, 0x44, 0xce, 0xeb, 0x70, 0x33, 0xbc, 0x61, 0x86, 0x2e, 0x44, 0x6d, 0x86,
0x53, 0xb0, 0x9b, 0x9d, 0xc9, 0x1f, 0x58, 0xd5, 0x47, 0x49, 0x61, 0x76, 0x26, 0xcb, 0x0b, 0x8f,
0x91, 0xde, 0x09, 0x43, 0xf6, 0x13, 0x38, 0x25, 0x92, 0x69, 0xc8, 0x2d, 0x4c, 0xc7, 0x4b, 0x40,
0xde, 0x0f, 0x10, 0xf6, 0x02, 0x14, 0xf5, 0xf7, 0xe6, 0x37, 0xb5, 0x84, 0xbe, 0x32, 0xfd, 0x21,
0xa6, 0xeb, 0x42, 0xde, 0x09, 0xb5, 0xad, 0x9e, 0xb4, 0x4d, 0x33, 0x77, 0x2e, 0x62, 0x31, 0x4a,
0x92, 0x43, 0x56, 0x21, 0xdf, 0xb3, 0xd7, 0xc2, 0xa1, 0x95, 0x12, 0x69, 0x66, 0x9b, 0xb2, 0x81,
0x5e, 0xcb, 0x9e, 0x25, 0xa9, 0xd0, 0xb2, 0xe6, 0x80, 0xf6, 0xc3, 0xb6, 0x81, 0x36, 0x45, 0xd4,
0x8a, 0xa6, 0xed, 0x58, 0x5e, 0x33, 0x53, 0x3e, 0x9f, 0x67, 0x4c, 0x43, 0x27, 0x2c, 0x56, 0x37,
0x46, 0x6e, 0xb7, 0x6d, 0xa1, 0x20, 0x31, 0x96, 0x07, 0x15, 0x6c, 0x1a, 0x59, 0x63, 0xea, 0x95,
0x54, 0xf3, 0x60, 0x37, 0xda, 0x66, 0x3c, 0x80, 0x48, 0x23, 0x51, 0xd0, 0x7e, 0x76, 0x57, 0x8b,
0xf7, 0x59, 0xf3, 0x24, 0xe0, 0x5d, 0x57, 0x52, 0xd9, 0x11, 0x13, 0x9f, 0xdd, 0x21, 0x98, 0x5d,
0x27, 0x00, 0x0f, 0x4f, 0x57, 0xe3, 0x04, 0xae, 0x13, 0xa0, 0xbe, 0x64, 0x88, 0x75, 0x02, 0xc5,
0xfa, 0x4d, 0x67, 0xf6, 0xbd, 0x0e, 0xe2, 0xca, 0x56, 0x0e, 0x69, 0x3a, 0x14, 0x0c, 0x35, 0x1d,
0xa5, 0xe0, 0x3f, 0x52, 0x77, 0x6b, 0x0d, 0x79, 0xa4, 0xd8, 0xbe, 0xda, 0x7a, 0x17, 0x66, 0x73,
0xff, 0x5a, 0x78, 0xc2, 0xe2, 0xc4, 0x54, 0x0c, 0xd1, 0x75, 0xe5, 0x44, 0xee, 0x8f, 0x71, 0xda,
0xc9, 0x1f, 0x44, 0x03, 0x55, 0x8d, 0xd2, 0x75, 0x73, 0x0b, 0x2b, 0x62, 0x4d, 0x10, 0x03, 0x95,
0x4f, 0x38, 0x89, 0x9b, 0xd7, 0x44, 0x53, 0xae, 0x1d, 0xe8, 0xcf, 0x42, 0x2b, 0x90, 0xb8, 0xf9,
0x8f, 0xbd, 0x45, 0x13, 0x89, 0x5b, 0xb7, 0x96, 0x73, 0xed, 0x0f, 0x68, 0xb2, 0xbd, 0x92, 0x2f,
0x60, 0x99, 0x3e, 0x0b, 0x36, 0x0f, 0xa2, 0x41, 0x5c, 0xfb, 0xd3, 0x4f, 0xd3, 0xce, 0x41, 0x66,
0xef, 0x40, 0x1e, 0x4f, 0xc3, 0x7f, 0x16, 0x44, 0x09, 0x89, 0x39, 0xa8, 0x05, 0x39, 0xbf, 0x25,
0x3a, 0x7e, 0x59, 0xa6, 0x22, 0xcd, 0xe7, 0x53, 0xce, 0x33, 0xb8, 0x65, 0x39, 0x1a, 0x0f, 0x5d,
0x29, 0xf5, 0x5b, 0xa2, 0x2d, 0xca, 0x4e, 0x71, 0xa3, 0xf1, 0x68, 0x29, 0xf8, 0x79, 0x9a, 0x65,
0x20, 0x72, 0x46, 0xe3, 0x61, 0x23, 0x21, 0x22, 0xc7, 0x27, 0x9c, 0x5f, 0xc0, 0x1c, 0xcb, 0xdd,
0x7f, 0xbd, 0x03, 0xfa, 0x01, 0xd4, 0x71, 0x84, 0xd4, 0x2f, 0x60, 0x42, 0xc8, 0xf9, 0x45, 0xcf,
0x31, 0xf6, 0xdb, 0x26, 0x9b, 0x50, 0x1d, 0x81, 0xa8, 0x5f, 0xf4, 0xa4, 0x60, 0xe7, 0x9b, 0xe4,
0xe3, 0x65, 0x75, 0xe1, 0x6f, 0x19, 0xa8, 0xc5, 0xa1, 0xba, 0x20, 0xf5, 0x09, 0xf8, 0x85, 0x1d,
0x9f, 0x1d, 0x7a, 0x30, 0x71, 0x3c, 0xad, 0x53, 0x49, 0x15, 0xe6, 0xe9, 0xfb, 0xff, 0xfd, 0xd5,
0x8d, 0xb5, 0x9f, 0x7d, 0x75, 0x63, 0xed, 0x7f, 0xbf, 0xba, 0xb1, 0xf6, 0xd3, 0xaf, 0x6f, 0xbc,
0xf1, 0xb3, 0xaf, 0x6f, 0xbc, 0xf1, 0x3f, 0x5f, 0xdf, 0x78, 0xe3, 0xcb, 0x37, 0xf5, 0x6f, 0xee,
0x9f, 0xfd, 0x82, 0xfc, 0xe5, 0xfc, 0x27, 0xff, 0x1f, 0x00, 0x00, 0xff, 0xff, 0x89, 0x4b, 0xe1,
0x93, 0x97, 0x7f, 0x00, 0x00,
// 5713 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, 0x78, 0x8f, 0x68, 0xf7, 0xef, 0x40, 0x42, 0x95, 0x95, 0x9f,
0x51, 0x11, 0x59, 0xe5, 0xbd, 0xa7, 0x19, 0x39, 0x7e, 0x11, 0x91, 0x1f, 0x91, 0x99, 0x91, 0x59,
0x59, 0xd5, 0xd1, 0xcd, 0xe2, 0x6c, 0xbb, 0x28, 0xb9, 0xe0, 0xd5, 0x76, 0xc5, 0xca, 0xab, 0x74,
0xc6, 0xf4, 0xbf, 0x43, 0xf9, 0xe7, 0xc1, 0x9b, 0x71, 0xbe, 0x12, 0xab, 0x82, 0xbd, 0xf7, 0xae,
0x25, 0x67, 0x7c, 0xb1, 0x88, 0xf3, 0xa4, 0x6a, 0x90, 0xf7, 0xde, 0xb1, 0x12, 0x76, 0xc5, 0x72,
0xa1, 0xfe, 0xfe, 0xe4, 0x67, 0xff, 0xb7, 0x16, 0xbd, 0xb5, 0x93, 0xa5, 0x2c, 0x17, 0x3b, 0x4a,
0x63, 0xf0, 0x65, 0xf4, 0xed, 0x51, 0x51, 0xec, 0x33, 0xf1, 0x82, 0x95, 0x55, 0xca, 0xf3, 0xc1,
0x9d, 0xa1, 0x72, 0x30, 0x3c, 0x29, 0x66, 0xc3, 0x51, 0x51, 0x0c, 0xad, 0x70, 0x78, 0xc2, 0x7e,
0xb4, 0x64, 0x95, 0x78, 0xef, 0x6e, 0x18, 0xaa, 0x0a, 0x9e, 0x57, 0x6c, 0x70, 0x1e, 0xfd, 0xda,
0xa8, 0x28, 0x26, 0x4c, 0xec, 0xb2, 0xba, 0x02, 0x13, 0x11, 0x0b, 0x36, 0xd8, 0x68, 0xa9, 0xfa,
0x80, 0xf1, 0x71, 0xbf, 0x1b, 0x54, 0x7e, 0xa6, 0xd1, 0xb7, 0x6a, 0x3f, 0x17, 0x4b, 0x91, 0xf0,
0x57, 0xf9, 0xe0, 0xfd, 0xb6, 0xa2, 0x12, 0x19, 0xdb, 0xb7, 0x43, 0x88, 0xb2, 0xfa, 0x32, 0xfa,
0xe5, 0x97, 0x71, 0x96, 0x31, 0xb1, 0x53, 0xb2, 0xba, 0xe0, 0xbe, 0x4e, 0x23, 0x1a, 0x36, 0x32,
0x63, 0xf7, 0x4e, 0x90, 0x51, 0x86, 0xbf, 0x8c, 0xbe, 0xdd, 0x48, 0x4e, 0xd8, 0x8c, 0x5f, 0xb1,
0x72, 0x80, 0x6a, 0x29, 0x21, 0xd1, 0xe4, 0x2d, 0x08, 0xda, 0xde, 0xe1, 0xf9, 0x15, 0x2b, 0x05,
0x6e, 0x5b, 0x09, 0xc3, 0xb6, 0x2d, 0xa4, 0x6c, 0xff, 0xed, 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, 0xb5, 0x41, 0x87, 0x86, 0x1d, 0xba, 0xb0, 0xf1, 0xfd, 0xe1,
0xf5, 0x94, 0x54, 0x59, 0xfe, 0x71, 0x2d, 0xba, 0x01, 0xcb, 0x32, 0xe1, 0xd9, 0x15, 0xb3, 0xa5,
0xf9, 0xa8, 0xc3, 0xb0, 0x8f, 0x9b, 0xf2, 0x7c, 0x7c, 0x5d, 0x35, 0x55, 0xa2, 0x2c, 0x7a, 0xdb,
0x0d, 0x97, 0x09, 0xab, 0xe4, 0x70, 0x7a, 0x40, 0x47, 0x84, 0x42, 0x8c, 0xe7, 0x87, 0x7d, 0x50,
0xe5, 0x2d, 0x8d, 0x06, 0xca, 0x5b, 0xc6, 0x2b, 0xe3, 0xec, 0x3e, 0x6a, 0xc1, 0x21, 0x8c, 0xaf,
0x07, 0x3d, 0x48, 0xe5, 0xea, 0x8f, 0xa2, 0x5f, 0x79, 0xc9, 0xcb, 0xcb, 0xaa, 0x88, 0x67, 0x4c,
0x0d, 0x85, 0x7b, 0xbe, 0xb6, 0x96, 0xc2, 0xd1, 0xb0, 0xde, 0x85, 0x39, 0x41, 0xab, 0x85, 0x5f,
0x14, 0x0c, 0xce, 0x41, 0x56, 0xb1, 0x16, 0x52, 0x41, 0x0b, 0x21, 0x65, 0xfb, 0x32, 0x1a, 0x58,
0xdb, 0x67, 0x7f, 0xcc, 0x66, 0x62, 0x94, 0x24, 0xb0, 0x57, 0xac, 0xae, 0x24, 0x86, 0xa3, 0x24,
0xa1, 0x7a, 0x05, 0x47, 0x95, 0xb3, 0x57, 0xd1, 0x3b, 0xc0, 0xd9, 0x41, 0x5a, 0x49, 0x87, 0x5b,
0x61, 0x2b, 0x0a, 0x33, 0x4e, 0x87, 0x7d, 0x71, 0xe5, 0xf8, 0xcf, 0xd7, 0xa2, 0xef, 0x20, 0x9e,
0x4f, 0xd8, 0x82, 0x5f, 0xb1, 0xc1, 0xe3, 0x6e, 0x6b, 0x0d, 0x69, 0xfc, 0x7f, 0x70, 0x0d, 0x0d,
0x24, 0x4c, 0x26, 0x2c, 0x63, 0x33, 0x41, 0x86, 0x49, 0x23, 0xee, 0x0c, 0x13, 0x83, 0x39, 0x23,
0x4c, 0x0b, 0xf7, 0x99, 0xd8, 0x59, 0x96, 0x25, 0xcb, 0x05, 0xd9, 0x97, 0x16, 0xe9, 0xec, 0x4b,
0x0f, 0x45, 0xea, 0xb3, 0xcf, 0xc4, 0x28, 0xcb, 0xc8, 0xfa, 0x34, 0xe2, 0xce, 0xfa, 0x18, 0x4c,
0x79, 0x98, 0x45, 0xbf, 0xea, 0xb4, 0x98, 0x18, 0xe7, 0xe7, 0x7c, 0x40, 0xb7, 0x85, 0x94, 0x1b,
0x1f, 0x1b, 0x9d, 0x1c, 0x52, 0x8d, 0xe7, 0xaf, 0x0b, 0x5e, 0xd2, 0xdd, 0xd2, 0x88, 0x3b, 0xab,
0x61, 0x30, 0xe5, 0xe1, 0x0f, 0xa3, 0xb7, 0xd4, 0x2c, 0xa9, 0xd7, 0xb3, 0xbb, 0xe8, 0x14, 0x0a,
0x17, 0xb4, 0x7b, 0x1d, 0x54, 0xcb, 0xfc, 0x61, 0x3a, 0x2f, 0xeb, 0xd9, 0x07, 0x37, 0xaf, 0xa4,
0x1d, 0xe6, 0x2d, 0xa5, 0xcc, 0xf3, 0xe8, 0xd7, 0x7d, 0xf3, 0x3b, 0x71, 0x3e, 0x63, 0xd9, 0xe0,
0x61, 0x48, 0xbd, 0x61, 0x8c, 0xab, 0xcd, 0x5e, 0xac, 0x9d, 0xec, 0x14, 0xa1, 0x26, 0xd3, 0x3b,
0xa8, 0x36, 0x98, 0x4a, 0xef, 0x86, 0xa1, 0x96, 0xed, 0x5d, 0x96, 0x31, 0xd2, 0x76, 0x23, 0xec,
0xb0, 0x6d, 0x20, 0x65, 0xbb, 0x8c, 0x7e, 0xc3, 0x74, 0x73, 0x9d, 0x17, 0x48, 0x79, 0xbd, 0xe8,
0x6c, 0x12, 0xfd, 0xe8, 0x42, 0xc6, 0xd7, 0xa3, 0x7e, 0x70, 0xab, 0x3e, 0x6a, 0x46, 0xc1, 0xeb,
0x03, 0xe6, 0x93, 0xbb, 0x61, 0x48, 0xd9, 0xfe, 0xbb, 0xb5, 0xe8, 0x7b, 0x4a, 0xf6, 0x3c, 0x8f,
0xcf, 0x32, 0x26, 0x97, 0xf8, 0x23, 0x26, 0x5e, 0xf1, 0xf2, 0x72, 0xb2, 0xca, 0x67, 0x44, 0x3a,
0x83, 0xc3, 0x1d, 0xe9, 0x0c, 0xa9, 0xa4, 0x0a, 0xf3, 0x27, 0xd1, 0xbb, 0x3a, 0x28, 0x2e, 0xe2,
0x7c, 0xce, 0x7e, 0x58, 0xf1, 0x7c, 0x54, 0xa4, 0xa3, 0x24, 0x29, 0x07, 0x43, 0xbc, 0xeb, 0x21,
0x67, 0x4a, 0xb0, 0xdd, 0x9b, 0x77, 0xd2, 0x67, 0xd5, 0xca, 0x82, 0x17, 0x30, 0x7d, 0xd6, 0xcd,
0x27, 0x78, 0x41, 0xa5, 0xcf, 0x3e, 0xd2, 0xb2, 0x7a, 0x58, 0xaf, 0x41, 0xb8, 0xd5, 0x43, 0x77,
0xd1, 0xb9, 0x1d, 0x42, 0xec, 0x1a, 0xa0, 0x1b, 0x8a, 0xe7, 0xe7, 0xe9, 0xfc, 0xb4, 0x48, 0xea,
0x31, 0xf4, 0x00, 0xaf, 0xb3, 0x83, 0x10, 0x6b, 0x00, 0x81, 0x2a, 0x6f, 0xff, 0x60, 0xb3, 0x4c,
0x35, 0x2f, 0xed, 0x95, 0x7c, 0x71, 0xc0, 0xe6, 0xf1, 0x6c, 0xa5, 0x26, 0xd3, 0x0f, 0x43, 0xb3,
0x18, 0xa4, 0x4d, 0x21, 0x3e, 0xba, 0xa6, 0x96, 0x2a, 0xcf, 0x7f, 0xae, 0x45, 0x77, 0xbd, 0x38,
0x51, 0xc1, 0xd4, 0x94, 0x7e, 0x94, 0x27, 0x27, 0xac, 0x12, 0x71, 0x29, 0x06, 0xdf, 0x0f, 0xc4,
0x00, 0xa1, 0x63, 0xca, 0xf6, 0x83, 0x6f, 0xa4, 0x6b, 0x7b, 0x7d, 0x52, 0xaf, 0x12, 0x6a, 0xfe,
0xf1, 0x7b, 0x5d, 0x4a, 0xe0, 0xec, 0x73, 0x3b, 0x84, 0xd8, 0x5e, 0x97, 0x82, 0x71, 0x7e, 0x95,
0x0a, 0xb6, 0xcf, 0x72, 0x56, 0xb6, 0x7b, 0xbd, 0x51, 0xf5, 0x11, 0xa2, 0xd7, 0x09, 0xd4, 0xce,
0x74, 0x9e, 0x37, 0x93, 0x69, 0x6c, 0x06, 0x8c, 0xb4, 0x72, 0x8d, 0x47, 0xfd, 0x60, 0xa2, 0x86,
0x62, 0xbf, 0x36, 0x12, 0xac, 0x61, 0x83, 0xf4, 0xaa, 0xa1, 0x41, 0xed, 0xc6, 0xdc, 0xf1, 0x76,
0xc2, 0xae, 0xf8, 0x25, 0xdc, 0x98, 0xbb, 0x06, 0x1a, 0x80, 0xd8, 0x98, 0xa3, 0xa0, 0x4d, 0x3e,
0x1c, 0x3f, 0x2f, 0x52, 0xf6, 0x0a, 0x24, 0x1f, 0xae, 0x72, 0x2d, 0x26, 0x92, 0x0f, 0x04, 0x53,
0x1e, 0x8e, 0xa2, 0x5f, 0x92, 0xc2, 0x1f, 0xf2, 0x34, 0x1f, 0xdc, 0x44, 0x94, 0x6a, 0x81, 0xb1,
0x7a, 0x8b, 0x06, 0x40, 0x89, 0xeb, 0xbf, 0xaa, 0x4c, 0xe0, 0x1e, 0xa1, 0x04, 0x92, 0x80, 0xf5,
0x2e, 0xcc, 0x66, 0x7d, 0x52, 0x58, 0xcf, 0x96, 0x93, 0x8b, 0xb8, 0x4c, 0xf3, 0xf9, 0x00, 0xd3,
0x75, 0xe4, 0x44, 0xd6, 0x87, 0x71, 0x20, 0x9c, 0x94, 0xe2, 0xa8, 0x28, 0xca, 0x7a, 0x12, 0xc6,
0xc2, 0xc9, 0x47, 0x82, 0xe1, 0xd4, 0x42, 0x71, 0x6f, 0xbb, 0x6c, 0x96, 0xa5, 0x79, 0xd0, 0x9b,
0x42, 0xfa, 0x78, 0xb3, 0x28, 0x08, 0xde, 0x03, 0x16, 0x5f, 0x31, 0x5d, 0x33, 0xac, 0x65, 0x5c,
0x20, 0x18, 0xbc, 0x00, 0xb4, 0x5b, 0x6c, 0x29, 0x3e, 0x8c, 0x2f, 0x59, 0xdd, 0xc0, 0xac, 0x5e,
0xc2, 0x07, 0x98, 0xbe, 0x47, 0x10, 0x5b, 0x6c, 0x9c, 0x54, 0xae, 0x96, 0xd1, 0x3b, 0x52, 0x7e,
0x1c, 0x97, 0x22, 0x9d, 0xa5, 0x45, 0x9c, 0xeb, 0xad, 0x1b, 0x36, 0x8b, 0xb4, 0x28, 0xe3, 0x72,
0xab, 0x27, 0xad, 0xdc, 0xfe, 0xdb, 0x5a, 0xf4, 0x3e, 0xf4, 0x7b, 0xcc, 0xca, 0x45, 0x2a, 0x4f,
0x00, 0xaa, 0x66, 0xca, 0x1f, 0x7c, 0x12, 0x36, 0xda, 0x52, 0x30, 0xa5, 0xf9, 0xf4, 0xfa, 0x8a,
0x36, 0xef, 0x9b, 0xa8, 0x5d, 0xd1, 0x17, 0x65, 0xd2, 0x3a, 0x21, 0x9b, 0xe8, 0xad, 0x8e, 0x14,
0x12, 0x79, 0x5f, 0x0b, 0x02, 0x23, 0xfc, 0x34, 0xaf, 0xb4, 0x75, 0x6c, 0x84, 0x5b, 0x71, 0x70,
0x84, 0x7b, 0x98, 0x1d, 0xe1, 0xc7, 0xcb, 0xb3, 0x2c, 0xad, 0x2e, 0xd2, 0x7c, 0xae, 0x92, 0x7c,
0x5f, 0xd7, 0x8a, 0x61, 0x9e, 0xbf, 0xd1, 0xc9, 0x61, 0x4e, 0x54, 0xb0, 0x90, 0x4e, 0x40, 0x98,
0x6c, 0x74, 0x72, 0x76, 0xef, 0x65, 0xa5, 0xf5, 0xa6, 0x1f, 0xec, 0xbd, 0x1c, 0xd5, 0x5a, 0x4a,
0xec, 0xbd, 0xda, 0x94, 0xdd, 0x7b, 0xb9, 0x75, 0xa8, 0x78, 0x76, 0xc5, 0x4e, 0xcb, 0x14, 0xec,
0xbd, 0xbc, 0xf2, 0x69, 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, 0xe5, 0xed, 0xf7,
0xa3, 0xa8, 0x39, 0x2f, 0x91, 0x67, 0x5a, 0xfe, 0xda, 0xa3, 0x0e, 0x52, 0xbc, 0x03, 0xad, 0xf7,
0x03, 0x84, 0x4d, 0xaf, 0x9a, 0xbf, 0xcb, 0xa3, 0xba, 0x01, 0xaa, 0x21, 0x45, 0x44, 0x7a, 0x05,
0x10, 0x58, 0xd0, 0xc9, 0x05, 0x7f, 0x85, 0x17, 0xb4, 0x96, 0x84, 0x0b, 0xaa, 0x08, 0x7b, 0x78,
0xae, 0x0a, 0x8a, 0x1d, 0x9e, 0xeb, 0x62, 0x84, 0x0e, 0xcf, 0x21, 0x63, 0x63, 0xc6, 0x35, 0xfc,
0x8c, 0xf3, 0xcb, 0x45, 0x5c, 0x5e, 0x82, 0x98, 0xf1, 0x94, 0x35, 0x43, 0xc4, 0x0c, 0xc5, 0xda,
0x98, 0x71, 0x1d, 0xd6, 0xc9, 0xf9, 0x69, 0x99, 0x81, 0x98, 0xf1, 0x6c, 0x28, 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, 0x90, 0x46, 0x60,
0x7f, 0x4f, 0x58, 0x5c, 0xce, 0x2e, 0xf0, 0xfe, 0x6e, 0x64, 0xe1, 0xfe, 0x36, 0x0c, 0xec, 0xef,
0x46, 0xf0, 0x32, 0x15, 0x17, 0x87, 0x4c, 0xc4, 0x78, 0x7f, 0xfb, 0x4c, 0xb8, 0xbf, 0x5b, 0xac,
0xcd, 0xfe, 0x5d, 0x87, 0x93, 0xe5, 0x59, 0x35, 0x2b, 0xd3, 0x33, 0x36, 0x08, 0x58, 0x31, 0x10,
0x91, 0xfd, 0x93, 0xb0, 0xf2, 0xf9, 0x93, 0xb5, 0xe8, 0xa6, 0xee, 0x76, 0x5e, 0x55, 0x6a, 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, 0xf5, 0x8f, 0x96, 0x8d, 0x93, 0x0a, 0xa4, 0x65, 0xba, 0xbd, 0x1d, 0x82, 0x48, 0xcb,
0x70, 0x12, 0x86, 0xc2, 0x7e, 0xc9, 0x97, 0x45, 0xd5, 0x11, 0x0a, 0x00, 0x0a, 0x87, 0x42, 0x1b,
0x56, 0x3e, 0x5f, 0x47, 0xbf, 0xe9, 0x86, 0x9f, 0xdb, 0xd8, 0x5b, 0x74, 0x4c, 0x61, 0x4d, 0x3c,
0xec, 0x8b, 0xdb, 0x8c, 0x42, 0x7b, 0x16, 0xbb, 0x4c, 0xc4, 0x69, 0x56, 0x0d, 0xd6, 0x71, 0x1b,
0x5a, 0x4e, 0x64, 0x14, 0x18, 0x07, 0xe7, 0xb7, 0xdd, 0x65, 0x91, 0xa5, 0xb3, 0xf6, 0xc3, 0x24,
0xa5, 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, 0x37, 0x8d, 0xf4, 0xf0, 0xe6,
0xa0, 0x48, 0xa7, 0x4d, 0xf8, 0xb2, 0x9c, 0x31, 0xb2, 0xd3, 0x1a, 0x71, 0x67, 0xa7, 0x19, 0x4c,
0x79, 0xf8, 0xab, 0xb5, 0xe8, 0xb7, 0x1a, 0xa9, 0xfb, 0x84, 0x67, 0x37, 0xae, 0x2e, 0xce, 0x78,
0x5c, 0x26, 0x83, 0x0f, 0x30, 0x3b, 0x28, 0x6a, 0x5c, 0x3f, 0xb9, 0x8e, 0x0a, 0x6c, 0xd6, 0x3a,
0xef, 0xb6, 0x23, 0x0e, 0x6d, 0x56, 0x0f, 0x09, 0x37, 0x2b, 0x44, 0xe1, 0x04, 0x22, 0xe5, 0xcd,
0x01, 0xe0, 0x3a, 0xa9, 0xef, 0x9f, 0x02, 0x6e, 0x74, 0x72, 0x70, 0x7e, 0xac, 0x85, 0x7e, 0xb4,
0x6c, 0x51, 0x36, 0xf0, 0x88, 0x19, 0xf6, 0xc5, 0x49, 0xcf, 0x66, 0x54, 0x84, 0x3d, 0xb7, 0x46,
0xc6, 0xb0, 0x2f, 0x4e, 0x78, 0x76, 0xa6, 0xb5, 0x90, 0x67, 0x64, 0x6a, 0x1b, 0xf6, 0xc5, 0x61,
0xf6, 0xa5, 0x18, 0xbd, 0x2e, 0x3c, 0x0c, 0xd8, 0x81, 0x6b, 0xc3, 0x66, 0x2f, 0x56, 0x39, 0xfc,
0x9b, 0xb5, 0xe8, 0xbb, 0xd6, 0xe3, 0x21, 0x4f, 0xd2, 0xf3, 0x55, 0x03, 0xbd, 0x88, 0xb3, 0x25,
0xab, 0x06, 0x4f, 0x28, 0x6b, 0x6d, 0xd6, 0x94, 0xe0, 0xe9, 0xb5, 0x74, 0xe0, 0xd8, 0x19, 0x15,
0x45, 0xb6, 0x9a, 0xb2, 0x45, 0x91, 0x91, 0x63, 0xc7, 0x43, 0xc2, 0x63, 0x07, 0xa2, 0x30, 0x2b,
0x9f, 0xf2, 0x3a, 0xe7, 0x47, 0xb3, 0x72, 0x29, 0x0a, 0x67, 0xe5, 0x1a, 0x81, 0xb9, 0xd2, 0x94,
0xef, 0xf0, 0x2c, 0x63, 0x33, 0xd1, 0xbe, 0x25, 0x62, 0x34, 0x2d, 0x11, 0xce, 0x95, 0x00, 0x69,
0x4f, 0xe5, 0xf4, 0x1e, 0x32, 0x2e, 0xd9, 0xb3, 0xd5, 0x41, 0x9a, 0x5f, 0x0e, 0xf0, 0xb4, 0xc0,
0x02, 0xc4, 0xa9, 0x1c, 0x0a, 0xc2, 0xbd, 0xea, 0x69, 0x9e, 0x70, 0x7c, 0xaf, 0x5a, 0x4b, 0xc2,
0x7b, 0x55, 0x45, 0x40, 0x93, 0x27, 0x8c, 0x32, 0x59, 0x4b, 0xc2, 0x26, 0x15, 0x81, 0x4d, 0x85,
0xea, 0x49, 0x11, 0x39, 0x15, 0x82, 0x67, 0x43, 0x1b, 0x9d, 0x1c, 0xdc, 0x73, 0x29, 0x07, 0x68,
0x44, 0x00, 0xe3, 0x77, 0x82, 0x0c, 0x0c, 0x7d, 0xbd, 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, 0x8d, 0x2c, 0xdc, 0x56, 0x86,
0x81, 0xa5, 0x6f, 0x04, 0xf2, 0x90, 0x6c, 0x9d, 0x56, 0xf4, 0x8e, 0xc9, 0x36, 0x3a, 0x39, 0xe5,
0xe4, 0x5f, 0xcc, 0xfe, 0xb0, 0x91, 0x1e, 0xf1, 0x7a, 0xf0, 0xbd, 0x88, 0xb3, 0x34, 0x89, 0x05,
0x9b, 0xf2, 0x4b, 0x96, 0xe3, 0x5b, 0x31, 0x55, 0xda, 0x86, 0x1f, 0x7a, 0x0a, 0xe1, 0xad, 0x58,
0x58, 0x11, 0xc6, 0x49, 0x43, 0x9f, 0x56, 0x6c, 0x27, 0xae, 0x88, 0x29, 0xd2, 0x43, 0xc2, 0x71,
0x02, 0x51, 0x98, 0x08, 0x37, 0xf2, 0xe7, 0xaf, 0x0b, 0x56, 0xa6, 0x2c, 0x9f, 0x31, 0x3c, 0x11,
0x86, 0x54, 0x38, 0x11, 0x46, 0x68, 0xb8, 0x09, 0xdc, 0x8d, 0x05, 0x7b, 0xb6, 0x9a, 0xa6, 0x0b,
0x56, 0x89, 0x78, 0x51, 0xe0, 0x9b, 0x40, 0x00, 0x85, 0x37, 0x81, 0x6d, 0xb8, 0x75, 0xe6, 0x64,
0x66, 0xda, 0xf6, 0xad, 0x35, 0x48, 0x04, 0x6e, 0xad, 0x11, 0x28, 0x6c, 0x58, 0x0b, 0xa0, 0x4f,
0x1f, 0x5a, 0x56, 0x82, 0x4f, 0x1f, 0x68, 0xba, 0x75, 0x92, 0x67, 0x98, 0x49, 0x3d, 0x34, 0x3b,
0x8a, 0x3e, 0x71, 0x87, 0xe8, 0x66, 0x2f, 0x16, 0x3f, 0x3a, 0x3c, 0x61, 0x59, 0x2c, 0xd7, 0xc3,
0xc0, 0xf9, 0x9c, 0x66, 0xfa, 0x1c, 0x1d, 0x3a, 0xac, 0x72, 0xf8, 0x17, 0x6b, 0xd1, 0x7b, 0x98,
0xc7, 0x2f, 0x0a, 0xe9, 0xf7, 0x71, 0xb7, 0xad, 0x86, 0x24, 0xae, 0xe5, 0x85, 0x35, 0xec, 0xcd,
0x12, 0x2d, 0xb2, 0xb7, 0xf6, 0x54, 0x01, 0xfc, 0x6c, 0xd0, 0x94, 0x1f, 0x72, 0xc4, 0xcd, 0x92,
0x10, 0x6f, 0x37, 0x5a, 0x7e, 0xb9, 0x2a, 0xb0, 0xd1, 0x32, 0x36, 0x94, 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, 0x1a, 0xac, 0xc7, 0x26, 0x36, 0x97, 0x1b, 0x43, 0xee,
0xc8, 0xbc, 0xdf, 0x0d, 0xc2, 0x78, 0xd5, 0x62, 0xb5, 0xa7, 0x7a, 0x18, 0xb2, 0x00, 0xf6, 0x55,
0x9b, 0xbd, 0x58, 0xe5, 0xf0, 0xcf, 0xa2, 0xef, 0xb4, 0x2a, 0xb6, 0xc7, 0x62, 0xb1, 0x2c, 0x59,
0x32, 0xd8, 0xee, 0x28, 0xb7, 0x06, 0x8d, 0xeb, 0xc7, 0xfd, 0x15, 0x5a, 0xc9, 0xbf, 0xe6, 0x9a,
0xb0, 0x32, 0x65, 0x78, 0x12, 0x32, 0xe9, 0xb3, 0xc1, 0xe4, 0x9f, 0xd6, 0x69, 0xed, 0xdf, 0xdd,
0xe8, 0x1a, 0x5d, 0xc5, 0x69, 0x26, 0x9f, 0x02, 0x7f, 0x10, 0x32, 0xea, 0xa1, 0xc1, 0xfd, 0x3b,
0xa9, 0xd2, 0x9a, 0x99, 0xe5, 0x18, 0x77, 0xf6, 0x7d, 0x8f, 0xe8, 0x99, 0x00, 0xd9, 0xf6, 0x6d,
0xf5, 0xa4, 0x95, 0x5b, 0xa1, 0x97, 0xbc, 0xfa, 0xcf, 0x6e, 0x90, 0x63, 0x5e, 0x95, 0x2a, 0x12,
0xe9, 0x5b, 0x3d, 0x69, 0xe5, 0xf5, 0x4f, 0xa3, 0x77, 0xdb, 0x5e, 0xd5, 0x42, 0xb4, 0xdd, 0x69,
0x0a, 0xac, 0x45, 0x8f, 0xfb, 0x2b, 0x28, 0xf7, 0xff, 0x6e, 0x0e, 0xbc, 0x1b, 0xff, 0x33, 0xbe,
0x58, 0xb0, 0x3c, 0x61, 0x89, 0xd6, 0xa8, 0xea, 0x8d, 0xd9, 0xa7, 0xb4, 0x5d, 0xa3, 0x30, 0x74,
0x35, 0x4c, 0x89, 0x7e, 0xfb, 0x1b, 0x68, 0xaa, 0xa2, 0xfd, 0xf7, 0x5a, 0xf4, 0x00, 0x2d, 0x9a,
0x0e, 0x5c, 0xaf, 0x88, 0xbf, 0xd7, 0xc7, 0x11, 0xa6, 0x69, 0x8a, 0x3a, 0xfa, 0x39, 0x2c, 0xa8,
0x22, 0xff, 0xc7, 0x5a, 0x74, 0xdb, 0x2a, 0xd6, 0xe1, 0xbd, 0xc3, 0xf3, 0xf3, 0x2c, 0x9d, 0x09,
0xf9, 0xa8, 0x57, 0xa9, 0xd0, 0xcd, 0x49, 0x69, 0x74, 0x37, 0x67, 0x40, 0xd3, 0xee, 0x8a, 0x3f,
0x4b, 0x2b, 0xc1, 0xcb, 0xd5, 0xe4, 0x82, 0xbf, 0xd2, 0xef, 0x3d, 0xf9, 0xf3, 0xb2, 0x02, 0x86,
0x0e, 0x41, 0xec, 0x8a, 0x71, 0xb2, 0xe5, 0xca, 0xbe, 0x1f, 0x55, 0x11, 0xae, 0x1c, 0xa2, 0xc3,
0x95, 0x4f, 0xda, 0x55, 0x49, 0xd7, 0xca, 0xbe, 0xcc, 0xb5, 0x81, 0x17, 0xb5, 0xfd, 0x42, 0xd7,
0xfd, 0x6e, 0xd0, 0xe6, 0xa6, 0x4a, 0xbc, 0x9b, 0x9e, 0x9f, 0x9b, 0x3a, 0xe1, 0x25, 0x75, 0x11,
0x22, 0x37, 0x25, 0x50, 0xbb, 0xbd, 0xda, 0x4b, 0x33, 0x26, 0x1f, 0x0a, 0x7d, 0x71, 0x7e, 0x9e,
0xf1, 0x38, 0x01, 0xdb, 0xab, 0x5a, 0x3c, 0x74, 0xe5, 0xc4, 0xf6, 0x0a, 0xe3, 0xec, 0x75, 0x93,
0x5a, 0x5a, 0x47, 0x77, 0x3e, 0x4b, 0x33, 0x78, 0x6d, 0x5a, 0x6a, 0x1a, 0x21, 0x71, 0xdd, 0xa4,
0x05, 0xd9, 0x14, 0xa8, 0x16, 0xd5, 0x51, 0xa9, 0xcb, 0x7f, 0xaf, 0xad, 0xe8, 0x88, 0x89, 0x14,
0x08, 0xc1, 0xec, 0xf1, 0x45, 0x2d, 0x3c, 0x2d, 0xa4, 0xf1, 0x5b, 0x6d, 0xad, 0x46, 0x42, 0x1c,
0x5f, 0xf8, 0x84, 0xdd, 0x2d, 0xd7, 0x7f, 0xdf, 0xe5, 0xaf, 0x72, 0x69, 0xf4, 0x76, 0x5b, 0x45,
0xcb, 0x88, 0xdd, 0x32, 0x64, 0x94, 0xe1, 0xcf, 0xa3, 0x5f, 0x94, 0x86, 0x4b, 0x5e, 0x0c, 0x6e,
0x20, 0x0a, 0xa5, 0x73, 0xc9, 0xf8, 0x26, 0x29, 0xb7, 0xb7, 0x53, 0x4c, 0x6c, 0x9c, 0x56, 0xf1,
0x1c, 0xbe, 0x19, 0x60, 0x7b, 0x5c, 0x4a, 0x89, 0xdb, 0x29, 0x6d, 0xca, 0x8f, 0x8a, 0x23, 0x9e,
0x28, 0xeb, 0x48, 0x0d, 0x8d, 0x30, 0x14, 0x15, 0x2e, 0x64, 0xd3, 0xd6, 0xa3, 0xf8, 0x2a, 0x9d,
0x9b, 0xd4, 0xa2, 0x99, 0xc0, 0x2a, 0x90, 0xb6, 0x5a, 0x66, 0xe8, 0x40, 0x44, 0xda, 0x4a, 0xc2,
0xca, 0xe7, 0x3f, 0xaf, 0x45, 0xb7, 0x2c, 0xb3, 0xaf, 0x0f, 0x7c, 0xc7, 0xf9, 0x39, 0xaf, 0x93,
0xdc, 0x83, 0x34, 0xbf, 0xac, 0x06, 0x1f, 0x53, 0x26, 0x71, 0xde, 0x14, 0xe5, 0x93, 0x6b, 0xeb,
0xd9, 0xfd, 0x89, 0x3e, 0x0d, 0xb5, 0x57, 0x22, 0x1a, 0x0d, 0xb0, 0x3f, 0x31, 0x87, 0xa6, 0x90,
0x23, 0xf6, 0x27, 0x21, 0xde, 0x76, 0xb1, 0x71, 0x9e, 0xf1, 0x1c, 0x76, 0xb1, 0xb5, 0x50, 0x0b,
0x89, 0x2e, 0x6e, 0x41, 0x76, 0x3e, 0xd6, 0xa2, 0xe6, 0x7c, 0x6d, 0x94, 0x65, 0x60, 0x3e, 0x36,
0xaa, 0x06, 0x20, 0xe6, 0x63, 0x14, 0x54, 0x7e, 0x4e, 0xa2, 0x6f, 0xd5, 0x4d, 0x7a, 0x5c, 0xb2,
0xab, 0x94, 0xc1, 0xdb, 0x3b, 0x8e, 0x84, 0x18, 0xff, 0x3e, 0x61, 0x47, 0xd6, 0x69, 0x5e, 0x15,
0x59, 0x5c, 0x5d, 0xa8, 0xfb, 0x1c, 0x7e, 0x9d, 0xb5, 0x10, 0xde, 0xe8, 0xb8, 0xd7, 0x41, 0xd9,
0x49, 0x5d, 0xcb, 0xcc, 0x14, 0xb3, 0x8e, 0xab, 0xb6, 0xa6, 0x99, 0x8d, 0x4e, 0xce, 0x3e, 0x34,
0xd9, 0x8f, 0xb3, 0x8c, 0x95, 0x2b, 0x2d, 0x3b, 0x8c, 0xf3, 0xf4, 0x9c, 0x55, 0x02, 0x3c, 0x34,
0x51, 0xd4, 0x10, 0x62, 0xc4, 0x43, 0x93, 0x00, 0x6e, 0xf7, 0x6d, 0xc0, 0xf3, 0x38, 0x4f, 0xd8,
0x6b, 0xb0, 0x6f, 0x83, 0x76, 0x24, 0x43, 0xec, 0xdb, 0x28, 0xd6, 0x3e, 0x3c, 0x78, 0x96, 0xf1,
0xd9, 0xa5, 0x5a, 0x02, 0xfc, 0x0e, 0x96, 0x12, 0xb8, 0x06, 0xdc, 0x0e, 0x21, 0x76, 0x11, 0x90,
0x82, 0x13, 0x56, 0x64, 0xf1, 0x0c, 0x5e, 0xe1, 0x6a, 0x74, 0x94, 0x8c, 0x58, 0x04, 0x20, 0x03,
0x8a, 0xab, 0xae, 0x86, 0x61, 0xc5, 0x05, 0x37, 0xc3, 0x6e, 0x87, 0x10, 0xbb, 0x0c, 0x4a, 0xc1,
0xa4, 0xc8, 0x52, 0x01, 0x86, 0x41, 0xa3, 0x21, 0x25, 0xc4, 0x30, 0xf0, 0x09, 0x60, 0xf2, 0x90,
0x95, 0x73, 0x86, 0x9a, 0x94, 0x92, 0xa0, 0x49, 0x4d, 0xd8, 0xfb, 0xea, 0x4d, 0xdd, 0x79, 0xb1,
0x02, 0xf7, 0xd5, 0x55, 0xb5, 0x78, 0xb1, 0x22, 0xee, 0xab, 0x7b, 0x00, 0x28, 0xe2, 0x71, 0x5c,
0x09, 0xbc, 0x88, 0x52, 0x12, 0x2c, 0xa2, 0x26, 0xec, 0x1a, 0xdd, 0x14, 0x71, 0x29, 0xc0, 0x1a,
0xad, 0x0a, 0xe0, 0x5c, 0x62, 0xb8, 0x49, 0xca, 0xed, 0x4c, 0xd2, 0xf4, 0x0a, 0x13, 0x7b, 0x29,
0xcb, 0x92, 0x0a, 0xcc, 0x24, 0xaa, 0xdd, 0xb5, 0x94, 0x98, 0x49, 0xda, 0x14, 0x08, 0x25, 0xf5,
0x04, 0x04, 0xab, 0x1d, 0x78, 0x00, 0x72, 0x3b, 0x84, 0xd8, 0xf9, 0x49, 0x17, 0x7a, 0x27, 0x2e,
0xcb, 0xb4, 0x5e, 0xfc, 0xd7, 0xf1, 0x02, 0x69, 0x39, 0x31, 0x3f, 0x61, 0x1c, 0x18, 0x5e, 0x7a,
0xe2, 0xc6, 0x0a, 0x06, 0xa7, 0xee, 0x3b, 0x41, 0xc6, 0x66, 0x9c, 0x52, 0xe2, 0x3c, 0x85, 0xc7,
0x5a, 0x13, 0x79, 0x08, 0xbf, 0xde, 0x85, 0x39, 0xaf, 0xce, 0x19, 0x17, 0x87, 0xfc, 0x8a, 0x4d,
0xf9, 0xf3, 0xd7, 0x69, 0x55, 0x6f, 0xb7, 0xd4, 0xca, 0xfd, 0x94, 0xb0, 0x84, 0xc1, 0xc4, 0xab,
0x73, 0x9d, 0x4a, 0x36, 0x81, 0x00, 0x65, 0x39, 0x62, 0xaf, 0xd0, 0x04, 0x02, 0x5a, 0x34, 0x1c,
0x91, 0x40, 0x84, 0x78, 0x7b, 0x62, 0x66, 0x9c, 0xab, 0xef, 0x25, 0x4c, 0xb9, 0xce, 0xe5, 0x28,
0x6b, 0x10, 0x24, 0x0e, 0x2d, 0x82, 0x0a, 0x76, 0x7f, 0x69, 0xfc, 0xdb, 0x21, 0x76, 0x9f, 0xb0,
0xd3, 0x1e, 0x66, 0x0f, 0x7a, 0x90, 0x88, 0x2b, 0x7b, 0x95, 0x84, 0x72, 0xd5, 0xbe, 0x49, 0xf2,
0xa0, 0x07, 0xe9, 0x9c, 0xbe, 0xb9, 0xd5, 0x7a, 0x16, 0xcf, 0x2e, 0xe7, 0x25, 0x5f, 0xe6, 0xc9,
0x0e, 0xcf, 0x78, 0x09, 0x4e, 0xdf, 0xbc, 0x52, 0x03, 0x94, 0x38, 0x7d, 0xeb, 0x50, 0xb1, 0x19,
0x9c, 0x5b, 0x8a, 0x51, 0x96, 0xce, 0xe1, 0x8e, 0xda, 0x33, 0x24, 0x01, 0x22, 0x83, 0x43, 0x41,
0x24, 0x88, 0x9a, 0x1d, 0xb7, 0x48, 0x67, 0x71, 0xd6, 0xf8, 0xdb, 0xa6, 0xcd, 0x78, 0x60, 0x67,
0x10, 0x21, 0x0a, 0x48, 0x3d, 0xa7, 0xcb, 0x32, 0x1f, 0xe7, 0x82, 0x93, 0xf5, 0xd4, 0x40, 0x67,
0x3d, 0x1d, 0x10, 0x4c, 0xab, 0x53, 0xf6, 0xba, 0x2e, 0x4d, 0xfd, 0x0f, 0x36, 0xad, 0xd6, 0x7f,
0x1f, 0x2a, 0x79, 0x68, 0x5a, 0x05, 0x1c, 0xa8, 0x8c, 0x72, 0xd2, 0x04, 0x4c, 0x40, 0xdb, 0x0f,
0x93, 0xfb, 0xdd, 0x20, 0xee, 0x67, 0x22, 0x56, 0x19, 0x0b, 0xf9, 0x91, 0x40, 0x1f, 0x3f, 0x1a,
0xb4, 0xc7, 0x2d, 0x5e, 0x7d, 0x2e, 0xd8, 0xec, 0xb2, 0x75, 0x33, 0xce, 0x2f, 0x68, 0x83, 0x10,
0xc7, 0x2d, 0x04, 0x8a, 0x77, 0xd1, 0x78, 0xc6, 0xf3, 0x50, 0x17, 0xd5, 0xf2, 0x3e, 0x5d, 0xa4,
0x38, 0xbb, 0xf9, 0x35, 0x52, 0x15, 0x99, 0x4d, 0x37, 0x6d, 0x12, 0x16, 0x5c, 0x88, 0xd8, 0xfc,
0x92, 0xb0, 0xcd, 0xc9, 0xa1, 0xcf, 0xc3, 0xf6, 0x6b, 0x03, 0x2d, 0x2b, 0x87, 0xf4, 0x6b, 0x03,
0x14, 0x4b, 0x57, 0xb2, 0x89, 0x91, 0x0e, 0x2b, 0x7e, 0x9c, 0x3c, 0xea, 0x07, 0xdb, 0x2d, 0x8f,
0xe7, 0x73, 0x27, 0x63, 0x71, 0xd9, 0x78, 0xdd, 0x0a, 0x18, 0xb2, 0x18, 0xb1, 0xe5, 0x09, 0xe0,
0x60, 0x0a, 0xf3, 0x3c, 0xef, 0xf0, 0x5c, 0xb0, 0x5c, 0x60, 0x53, 0x98, 0x6f, 0x4c, 0x81, 0xa1,
0x29, 0x8c, 0x52, 0x00, 0x71, 0x2b, 0xcf, 0x83, 0x98, 0x38, 0x8a, 0x17, 0x68, 0xc6, 0xd6, 0x9c,
0xf5, 0x34, 0xf2, 0x50, 0xdc, 0x02, 0xce, 0x79, 0x9c, 0xeb, 0x7a, 0x99, 0xc6, 0xe5, 0xdc, 0x9c,
0x6e, 0x24, 0x83, 0xc7, 0xb4, 0x1d, 0x9f, 0x24, 0x1e, 0xe7, 0x86, 0x35, 0xc0, 0xb4, 0x33, 0x5e,
0xc4, 0x73, 0x53, 0x53, 0xa4, 0x06, 0x52, 0xde, 0xaa, 0xea, 0xfd, 0x6e, 0x10, 0xf8, 0x79, 0x91,
0x26, 0x8c, 0x07, 0xfc, 0x48, 0x79, 0x1f, 0x3f, 0x10, 0x04, 0xd9, 0x5b, 0x5d, 0xef, 0x66, 0x47,
0x37, 0xca, 0x13, 0xb5, 0x8f, 0x1d, 0x12, 0xcd, 0x03, 0xb8, 0x50, 0xf6, 0x46, 0xf0, 0x60, 0x8c,
0xea, 0x03, 0xda, 0xd0, 0x18, 0x35, 0xe7, 0xaf, 0x7d, 0xc6, 0x28, 0x06, 0x2b, 0x9f, 0x3f, 0x56,
0x63, 0x74, 0x37, 0x16, 0x71, 0x9d, 0xb7, 0xbf, 0x48, 0xd9, 0x2b, 0xb5, 0x11, 0x46, 0xea, 0xab,
0xa9, 0xa1, 0x7c, 0xeb, 0x19, 0xec, 0x8a, 0xb7, 0x7b, 0xf3, 0x01, 0xdf, 0x6a, 0x87, 0xd0, 0xe9,
0x1b, 0x6c, 0x15, 0xb6, 0x7b, 0xf3, 0x01, 0xdf, 0xea, 0xe3, 0x0d, 0x9d, 0xbe, 0xc1, 0x17, 0x1c,
0xb6, 0x7b, 0xf3, 0xca, 0xf7, 0x5f, 0xea, 0x81, 0xeb, 0x3a, 0xaf, 0xf3, 0xb0, 0x99, 0x48, 0xaf,
0x18, 0x96, 0x4e, 0xfa, 0xf6, 0x0c, 0x1a, 0x4a, 0x27, 0x69, 0x15, 0xe7, 0xf3, 0x69, 0x58, 0x29,
0x8e, 0x79, 0x95, 0xca, 0xeb, 0x18, 0x4f, 0x7b, 0x18, 0xd5, 0x70, 0x68, 0xd3, 0x14, 0x52, 0xb2,
0x0f, 0x96, 0x3d, 0xd4, 0x5e, 0x84, 0x7f, 0x14, 0xb0, 0xd7, 0xbe, 0x0f, 0xbf, 0xd5, 0x93, 0xb6,
0x8f, 0x78, 0x3d, 0x46, 0x3f, 0x9c, 0x9b, 0x30, 0x74, 0x95, 0x30, 0xa6, 0xcc, 0x43, 0x5b, 0xf7,
0x29, 0xe5, 0xe3, 0xfe, 0x0a, 0x1d, 0xee, 0x47, 0x49, 0xd2, 0xcf, 0xbd, 0xfb, 0x74, 0xfb, 0x71,
0x7f, 0x05, 0xe5, 0xfe, 0xaf, 0xf5, 0xb6, 0x06, 0xfa, 0x57, 0x63, 0xf0, 0x49, 0x1f, 0x8b, 0x60,
0x1c, 0x3e, 0xbd, 0x96, 0x8e, 0x2a, 0xc8, 0xdf, 0xeb, 0xfd, 0xbb, 0x46, 0xe5, 0xdb, 0x48, 0xf2,
0x2d, 0x66, 0x35, 0x24, 0x43, 0x51, 0x65, 0x61, 0x38, 0x30, 0x3f, 0xba, 0xa6, 0x96, 0xf3, 0x2d,
0x3f, 0x0f, 0x56, 0x6f, 0xcd, 0x3a, 0xe5, 0x09, 0x59, 0x76, 0x68, 0x58, 0xa0, 0x8f, 0xaf, 0xab,
0x46, 0x0d, 0x55, 0x07, 0x96, 0x5f, 0xb3, 0x79, 0xda, 0xd3, 0xb0, 0xf7, 0x7d, 0x9b, 0x0f, 0xaf,
0xa7, 0xa4, 0xca, 0xf2, 0x5f, 0x6b, 0xd1, 0x3d, 0x8f, 0xb5, 0x8f, 0x33, 0xc0, 0xa1, 0xcb, 0x0f,
0x02, 0xf6, 0x29, 0x25, 0x53, 0xb8, 0xdf, 0xf9, 0x66, 0xca, 0xf6, 0xc3, 0x77, 0x9e, 0xca, 0x5e,
0x9a, 0x09, 0x56, 0xb6, 0x3f, 0x7c, 0xe7, 0xdb, 0x6d, 0xa8, 0x21, 0xfd, 0xe1, 0xbb, 0x00, 0xee,
0x7c, 0xf8, 0x0e, 0xf1, 0x8c, 0x7e, 0xf8, 0x0e, 0xb5, 0x16, 0xfc, 0xf0, 0x5d, 0x58, 0x83, 0x5a,
0x5d, 0x74, 0x11, 0x9a, 0x63, 0xf3, 0x5e, 0x16, 0xfd, 0x53, 0xf4, 0x27, 0xd7, 0x51, 0x21, 0xd6,
0xd7, 0x86, 0x93, 0x17, 0x2a, 0x7b, 0xb4, 0xa9, 0x77, 0xa9, 0x72, 0xbb, 0x37, 0xaf, 0x7c, 0xff,
0x48, 0x6d, 0xae, 0xcc, 0x6a, 0xc2, 0x4b, 0xf9, 0xd1, 0xc3, 0xcd, 0xd0, 0xea, 0x50, 0x5b, 0x70,
0x7b, 0xfe, 0x51, 0x3f, 0x98, 0xa8, 0x6e, 0x4d, 0xa8, 0x4e, 0x1f, 0x76, 0x19, 0x02, 0x5d, 0xbe,
0xdd, 0x9b, 0x27, 0x96, 0x91, 0xc6, 0x77, 0xd3, 0xdb, 0x3d, 0x8c, 0xf9, 0x7d, 0xfd, 0xb8, 0xbf,
0x82, 0x72, 0x7f, 0xa5, 0xb2, 0x56, 0xd7, 0xbd, 0xec, 0xe7, 0xad, 0x2e, 0x53, 0x13, 0xaf, 0x9b,
0x87, 0x7d, 0xf1, 0x50, 0xfe, 0xe2, 0x2e, 0xa1, 0x5d, 0xf9, 0x0b, 0xba, 0x8c, 0x7e, 0x78, 0x3d,
0x25, 0x55, 0x96, 0x7f, 0x5a, 0x8b, 0x6e, 0x92, 0x65, 0x51, 0x71, 0xf0, 0x71, 0x5f, 0xcb, 0x20,
0x1e, 0x3e, 0xb9, 0xb6, 0x9e, 0x2a, 0xd4, 0xbf, 0xae, 0x45, 0xb7, 0x02, 0x85, 0x6a, 0x02, 0xe4,
0x1a, 0xd6, 0xfd, 0x40, 0xf9, 0xf4, 0xfa, 0x8a, 0xd4, 0x72, 0xef, 0xe2, 0x93, 0xf6, 0x47, 0xcc,
0x02, 0xb6, 0x27, 0xf4, 0x47, 0xcc, 0xba, 0xb5, 0xe0, 0x19, 0x53, 0x7c, 0xa6, 0xf7, 0x7c, 0xe8,
0x19, 0x93, 0xbc, 0x0a, 0x19, 0xfc, 0x3c, 0x0a, 0xc6, 0x61, 0x4e, 0x9e, 0xbf, 0x2e, 0xe2, 0x3c,
0xa1, 0x9d, 0x34, 0xf2, 0x6e, 0x27, 0x86, 0x83, 0x67, 0x73, 0xb5, 0xf4, 0x84, 0xeb, 0x7d, 0xdc,
0x03, 0x4a, 0xdf, 0x20, 0xc1, 0xb3, 0xb9, 0x16, 0x4a, 0x78, 0x53, 0x59, 0x63, 0xc8, 0x1b, 0x48,
0x16, 0x1f, 0xf6, 0x41, 0xc1, 0x0e, 0xc1, 0x78, 0x33, 0x47, 0xfe, 0x8f, 0x42, 0x56, 0x5a, 0xc7,
0xfe, 0x5b, 0x3d, 0x69, 0xc2, 0xed, 0x84, 0x89, 0xcf, 0x58, 0x9c, 0xb0, 0x32, 0xe8, 0xd6, 0x50,
0xbd, 0xdc, 0xba, 0x34, 0xe6, 0x76, 0x87, 0x67, 0xcb, 0x45, 0xae, 0x3a, 0x93, 0x74, 0xeb, 0x52,
0xdd, 0x6e, 0x01, 0x0d, 0x4f, 0x25, 0xad, 0x5b, 0x99, 0x5e, 0x3e, 0x0c, 0x9b, 0xf1, 0xb2, 0xca,
0xcd, 0x5e, 0x2c, 0x5d, 0x4f, 0x15, 0x46, 0x1d, 0xf5, 0x04, 0x91, 0xb4, 0xd5, 0x93, 0x86, 0xc7,
0x83, 0x8e, 0x5b, 0x13, 0x4f, 0xdb, 0x1d, 0xb6, 0x5a, 0x21, 0xf5, 0xb8, 0xbf, 0x02, 0x3c, 0x8c,
0x55, 0x51, 0x75, 0x90, 0x56, 0x62, 0x2f, 0xcd, 0xb2, 0xc1, 0x66, 0x20, 0x4c, 0x34, 0x14, 0x3c,
0x8c, 0x45, 0x60, 0x22, 0x92, 0xf5, 0xe1, 0x65, 0x3e, 0xe8, 0xb2, 0x23, 0xa9, 0x5e, 0x91, 0xec,
0xd2, 0xe0, 0x40, 0xcd, 0x69, 0x6a, 0x53, 0xdb, 0x61, 0xb8, 0xe1, 0x5a, 0x15, 0xde, 0xee, 0xcd,
0x83, 0xa7, 0xfd, 0x92, 0x92, 0x2b, 0xcb, 0x5d, 0xca, 0x84, 0xb7, 0x92, 0xdc, 0xeb, 0xa0, 0xc0,
0xa1, 0x64, 0x33, 0x8c, 0x5e, 0xa6, 0xc9, 0x9c, 0x09, 0xf4, 0x41, 0x95, 0x0b, 0x04, 0x1f, 0x54,
0x01, 0x10, 0x74, 0x5d, 0xf3, 0x77, 0x73, 0x1a, 0x3b, 0x4e, 0xb0, 0xae, 0x53, 0xca, 0x0e, 0x15,
0xea, 0x3a, 0x94, 0x06, 0xb3, 0x81, 0x71, 0xab, 0x3e, 0x28, 0xf1, 0x30, 0x64, 0x06, 0x7c, 0x55,
0x62, 0xb3, 0x17, 0x0b, 0x56, 0x14, 0xeb, 0x30, 0x5d, 0xa4, 0x02, 0x5b, 0x51, 0x1c, 0x1b, 0x35,
0x12, 0x5a, 0x51, 0xda, 0x28, 0x55, 0xbd, 0x3a, 0x47, 0x18, 0x27, 0xe1, 0xea, 0x35, 0x4c, 0xbf,
0xea, 0x19, 0xb6, 0xf5, 0x5c, 0x35, 0x37, 0x21, 0x23, 0x2e, 0xd4, 0x66, 0x19, 0x89, 0x6d, 0xf9,
0xa2, 0x31, 0x04, 0x43, 0xb3, 0x0e, 0xa5, 0x00, 0x9f, 0x17, 0xd4, 0x9c, 0x7e, 0xf4, 0x5b, 0x14,
0x2c, 0x2e, 0xe3, 0x7c, 0x86, 0x6e, 0x4e, 0xa5, 0xc1, 0x16, 0x19, 0xda, 0x9c, 0x92, 0x1a, 0xe0,
0xa9, 0xbd, 0xff, 0x26, 0x2f, 0x32, 0x14, 0xcc, 0x2b, 0xb3, 0xfe, 0x8b, 0xbc, 0x0f, 0x7a, 0x90,
0xf0, 0xa9, 0xbd, 0x06, 0xcc, 0xb9, 0x7b, 0xe3, 0xf4, 0x83, 0x80, 0x29, 0x1f, 0x0d, 0x6d, 0x84,
0x69, 0x15, 0x10, 0xd4, 0xce, 0xd9, 0xe2, 0xe7, 0x6c, 0x85, 0x05, 0xb5, 0x7b, 0x48, 0xf8, 0x39,
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, 0x6b, 0xfd, 0xa8, 0x1e, 0x29, 0xae,
0x94, 0xb7, 0x9e, 0xd5, 0xdf, 0xef, 0x06, 0xed, 0xfd, 0xdb, 0xe3, 0x92, 0xcf, 0x58, 0x55, 0xa9,
0x6f, 0xad, 0xfa, 0x17, 0x9c, 0x94, 0x6c, 0x08, 0xbe, 0xb4, 0x7a, 0x37, 0x0c, 0x39, 0x1f, 0x48,
0x6c, 0x44, 0xf6, 0xbb, 0x4d, 0xeb, 0xa8, 0x66, 0xfb, 0x93, 0x4d, 0x1b, 0x9d, 0x9c, 0x1d, 0x5e,
0x4a, 0xea, 0x7e, 0xa8, 0xe9, 0x3e, 0xaa, 0x8e, 0x7d, 0xa3, 0xe9, 0x41, 0x0f, 0x52, 0xb9, 0xfa,
0x2c, 0x7a, 0xf3, 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, 0x0a, 0xb0, 0x59, 0x84, 0xb1, 0x57, 0x27, 0xea, 0xf0, 0xce,
0xa0, 0xd5, 0x91, 0x52, 0x22, 0x8b, 0x68, 0x53, 0x36, 0xb8, 0x9b, 0xea, 0xcb, 0xef, 0xe6, 0x2c,
0x17, 0x8b, 0xb8, 0x5c, 0x81, 0xe0, 0x56, 0xb5, 0x74, 0x00, 0x22, 0xb8, 0x51, 0xd0, 0x8e, 0x5a,
0xdd, 0xcc, 0xb3, 0xcb, 0x7d, 0x5e, 0xf2, 0xa5, 0x48, 0x73, 0x06, 0xbf, 0x9d, 0x62, 0x1a, 0xd4,
0x65, 0x88, 0x51, 0x4b, 0xb1, 0x36, 0xcb, 0x95, 0x44, 0x73, 0x9d, 0x51, 0x7e, 0x6c, 0xbe, 0x12,
0xbc, 0x84, 0x8f, 0x33, 0x1b, 0x2b, 0x10, 0x22, 0xb2, 0x5c, 0x12, 0x06, 0x7d, 0x7f, 0x9c, 0xe6,
0x73, 0xb4, 0xef, 0x8f, 0xdd, 0xef, 0x17, 0xdf, 0xa2, 0x01, 0x3b, 0xa0, 0x9a, 0x46, 0x6b, 0x06,
0x80, 0x7a, 0x69, 0x18, 0x6d, 0x74, 0x97, 0x20, 0x06, 0x14, 0x4e, 0x02, 0x57, 0x5f, 0x14, 0x2c,
0x67, 0x89, 0xbe, 0xb4, 0x87, 0xb9, 0xf2, 0x88, 0xa0, 0x2b, 0x48, 0xda, 0xb9, 0x48, 0xca, 0x4f,
0x96, 0xf9, 0x71, 0xc9, 0xcf, 0xd3, 0x8c, 0x95, 0x60, 0x2e, 0x6a, 0xd4, 0x1d, 0x39, 0x31, 0x17,
0x61, 0x9c, 0xbd, 0xfd, 0x21, 0xa5, 0xde, 0x2f, 0x26, 0x4c, 0xcb, 0x78, 0x06, 0x6f, 0x7f, 0x34,
0x36, 0xda, 0x18, 0x71, 0x32, 0x18, 0xc0, 0x9d, 0x44, 0xa7, 0x71, 0x9d, 0xaf, 0x64, 0x7c, 0xa8,
0x97, 0x56, 0xe5, 0x57, 0x7d, 0x2b, 0x90, 0xe8, 0x28, 0x73, 0x18, 0x49, 0x24, 0x3a, 0x61, 0x0d,
0xbb, 0x94, 0x48, 0xee, 0x48, 0xdd, 0x6a, 0x02, 0x4b, 0x49, 0x63, 0x43, 0x0b, 0x89, 0xa5, 0xa4,
0x05, 0x81, 0x09, 0x49, 0x0f, 0x83, 0x39, 0x3a, 0x21, 0x19, 0x69, 0x70, 0x42, 0x72, 0x29, 0x3b,
0x51, 0x8c, 0xf3, 0x54, 0xa4, 0x71, 0x36, 0x61, 0xe2, 0x38, 0x2e, 0xe3, 0x05, 0x13, 0xac, 0x84,
0x13, 0x85, 0x42, 0x86, 0x1e, 0x43, 0x4c, 0x14, 0x14, 0xab, 0x1c, 0xfe, 0x6e, 0xf4, 0x76, 0xbd,
0xee, 0xb3, 0x5c, 0xfd, 0xd6, 0xd3, 0x73, 0xf9, 0x23, 0x71, 0x83, 0x77, 0x8c, 0x8d, 0x89, 0x28,
0x59, 0xbc, 0xd0, 0xb6, 0xdf, 0x32, 0x7f, 0x97, 0xe0, 0xe3, 0xb5, 0x3a, 0x9e, 0x8f, 0xb8, 0x48,
0xcf, 0xeb, 0x6d, 0xb6, 0x7a, 0x81, 0x09, 0xc4, 0xb3, 0x2b, 0x1e, 0x06, 0x3e, 0x7a, 0x82, 0x71,
0x76, 0x9e, 0x76, 0xa5, 0x27, 0xac, 0xc8, 0xe0, 0x3c, 0xed, 0x69, 0x4b, 0x80, 0x98, 0xa7, 0x51,
0xd0, 0x0e, 0x4e, 0x57, 0x3c, 0x65, 0xe1, 0xca, 0x4c, 0x59, 0xbf, 0xca, 0x4c, 0xbd, 0x77, 0x42,
0xb2, 0xe8, 0xed, 0x43, 0xb6, 0x38, 0x63, 0x65, 0x75, 0x91, 0x16, 0xd4, 0x97, 0x87, 0x2d, 0xd1,
0xf9, 0xe5, 0x61, 0x02, 0xb5, 0x2b, 0x81, 0x05, 0xc6, 0xd5, 0x51, 0xbc, 0x60, 0xf2, 0x13, 0x2e,
0x60, 0x25, 0x70, 0x8c, 0x38, 0x10, 0xb1, 0x12, 0x90, 0xb0, 0xf3, 0x7a, 0x99, 0x65, 0x4e, 0xd8,
0xbc, 0x8e, 0xb0, 0xf2, 0x38, 0x5e, 0x2d, 0x58, 0x2e, 0x94, 0x49, 0x70, 0x26, 0xef, 0x98, 0xc4,
0x79, 0xe2, 0x4c, 0xbe, 0x8f, 0x9e, 0x33, 0x35, 0x79, 0x0d, 0x7f, 0xcc, 0x4b, 0xd1, 0xfc, 0x92,
0xdb, 0x69, 0x99, 0x81, 0xa9, 0xc9, 0x6f, 0x54, 0x8f, 0x24, 0xa6, 0xa6, 0xb0, 0x86, 0xf3, 0xab,
0x1d, 0x5e, 0x19, 0x5e, 0xb0, 0xd2, 0xc4, 0xc9, 0xf3, 0x45, 0x9c, 0x66, 0x2a, 0x1a, 0xbe, 0x1f,
0xb0, 0x4d, 0xe8, 0x10, 0xbf, 0xda, 0xd1, 0x57, 0xd7, 0xf9, 0x9d, 0x93, 0x70, 0x09, 0xc1, 0x23,
0x82, 0x0e, 0xfb, 0xc4, 0x23, 0x82, 0x6e, 0x2d, 0xbb, 0x73, 0xb7, 0xac, 0xe4, 0x56, 0x92, 0xd8,
0xe1, 0x09, 0x3c, 0x2f, 0x74, 0x6c, 0x02, 0x90, 0xd8, 0xb9, 0x07, 0x15, 0x6c, 0x6a, 0x60, 0xb1,
0xbd, 0x34, 0x8f, 0xb3, 0xf4, 0xc7, 0x30, 0xad, 0x77, 0xec, 0x68, 0x82, 0x48, 0x0d, 0x70, 0x12,
0x73, 0xb5, 0xcf, 0xc4, 0x34, 0xad, 0xa7, 0xfe, 0xfb, 0x81, 0x76, 0x93, 0x44, 0xb7, 0x2b, 0x87,
0x74, 0xbe, 0x32, 0x0c, 0x9b, 0x75, 0x54, 0x14, 0x93, 0x7a, 0x55, 0x3d, 0x61, 0x33, 0x96, 0x16,
0x62, 0xf0, 0x51, 0xb8, 0xad, 0x00, 0x4e, 0x5c, 0xb4, 0xe8, 0xa1, 0xe6, 0x3c, 0xbe, 0xaf, 0xe7,
0x92, 0x49, 0xf3, 0x13, 0xa7, 0xa7, 0x15, 0x2b, 0x55, 0xa2, 0xb1, 0xcf, 0x04, 0x18, 0x9d, 0x0e,
0x37, 0x74, 0xc0, 0xba, 0xa2, 0xc4, 0xe8, 0x0c, 0x6b, 0xd8, 0xc3, 0x3e, 0x87, 0x53, 0x5f, 0x8d,
0x97, 0xd7, 0x1d, 0x1f, 0x91, 0xc6, 0x1c, 0x8a, 0x38, 0xec, 0xa3, 0x69, 0x9b, 0xad, 0xb5, 0xdd,
0x8e, 0xf2, 0xd5, 0x18, 0x5e, 0x99, 0x40, 0x2c, 0x49, 0x8c, 0xc8, 0xd6, 0x02, 0xb8, 0x73, 0x18,
0x5e, 0xf2, 0x38, 0x99, 0xc5, 0x95, 0x38, 0x8e, 0x57, 0x19, 0x8f, 0x13, 0xb9, 0xae, 0xc3, 0xc3,
0x70, 0xcd, 0x0c, 0x5d, 0x88, 0x3a, 0x0c, 0xa7, 0x60, 0x37, 0x3b, 0x93, 0xbf, 0xdc, 0xaa, 0xae,
0x92, 0xc2, 0xec, 0x4c, 0x96, 0x17, 0x5e, 0x23, 0xbd, 0x1b, 0x86, 0xec, 0x2b, 0x70, 0x8d, 0x48,
0xa6, 0x21, 0xb7, 0x30, 0x1d, 0x2f, 0x01, 0x79, 0x3f, 0x40, 0xd8, 0x0f, 0xa0, 0x34, 0x7f, 0xd7,
0x3f, 0xd6, 0x25, 0xd4, 0xb7, 0xd8, 0x1f, 0x61, 0xba, 0x2e, 0xe4, 0xdd, 0x50, 0xdb, 0xea, 0x49,
0xdb, 0x34, 0x73, 0xe7, 0x22, 0x16, 0xa3, 0x24, 0x39, 0x64, 0x15, 0xf2, 0x3e, 0x7b, 0x2d, 0x1c,
0x5a, 0x29, 0x91, 0x66, 0xb6, 0x29, 0x1b, 0xe8, 0xb5, 0xec, 0x79, 0x92, 0x0a, 0x25, 0xd3, 0x17,
0xb4, 0x1f, 0xb5, 0x0d, 0xb4, 0x29, 0xa2, 0x56, 0x34, 0x6d, 0xe7, 0xf2, 0x9a, 0x99, 0xf2, 0xf9,
0x3c, 0x63, 0x0a, 0x3a, 0x61, 0x71, 0xf3, 0x29, 0xca, 0xed, 0xb6, 0x2d, 0x14, 0x24, 0xe6, 0xf2,
0xa0, 0x82, 0x4d, 0x23, 0x6b, 0xac, 0x79, 0x24, 0xa5, 0x1b, 0x76, 0xa3, 0x6d, 0xc6, 0x03, 0x88,
0x34, 0x12, 0x05, 0xed, 0x6b, 0x77, 0xb5, 0x78, 0x9f, 0xe9, 0x96, 0x80, 0xdf, 0xba, 0x92, 0xca,
0x8e, 0x98, 0x78, 0xed, 0x0e, 0xc1, 0xec, 0x3e, 0x01, 0x78, 0x78, 0xb6, 0x1a, 0x27, 0x70, 0x9f,
0x00, 0xf5, 0x25, 0x43, 0xec, 0x13, 0x28, 0xd6, 0xef, 0x3a, 0x73, 0xee, 0x75, 0x10, 0x57, 0xb6,
0x72, 0x48, 0xd7, 0xa1, 0x60, 0xa8, 0xeb, 0x28, 0x05, 0xbf, 0x49, 0xdd, 0xa3, 0x35, 0xa4, 0x49,
0xb1, 0x73, 0xb5, 0xf5, 0x2e, 0xcc, 0xe6, 0xfe, 0xb5, 0xf0, 0x84, 0xc5, 0x89, 0xa9, 0x18, 0xa2,
0xeb, 0xca, 0x89, 0xdc, 0x1f, 0xe3, 0x94, 0x93, 0x3f, 0x88, 0x06, 0x4d, 0x35, 0x4a, 0xd7, 0xcd,
0x2d, 0xac, 0x88, 0x35, 0x41, 0x4c, 0x54, 0x3e, 0xe1, 0x24, 0x6e, 0x5e, 0x17, 0x4d, 0xb9, 0x72,
0xa0, 0x5e, 0x0b, 0xad, 0x40, 0xe2, 0xe6, 0x37, 0x7b, 0x8b, 0x26, 0x12, 0xb7, 0x6e, 0x2d, 0xe7,
0xb3, 0x3f, 0xa0, 0xcb, 0xf6, 0x4a, 0xbe, 0x80, 0x65, 0xfa, 0x34, 0xd8, 0x3d, 0x88, 0x06, 0xf1,
0xd9, 0x9f, 0x7e, 0x9a, 0x76, 0x0d, 0x32, 0x67, 0x07, 0xf2, 0x7a, 0x1a, 0xfe, 0x7b, 0x23, 0x8d,
0x90, 0x58, 0x83, 0x5a, 0x90, 0xf3, 0x23, 0xa5, 0xe3, 0x97, 0x65, 0x2a, 0xd2, 0x7c, 0x3e, 0xe5,
0x3c, 0x83, 0x47, 0x96, 0xa3, 0xf1, 0xd0, 0x95, 0x52, 0x3f, 0x52, 0xda, 0xa2, 0xec, 0x12, 0x37,
0x1a, 0x8f, 0x96, 0x82, 0x9f, 0xa7, 0x59, 0x06, 0x22, 0x67, 0x34, 0x1e, 0x6a, 0x09, 0x11, 0x39,
0x3e, 0xe1, 0xfc, 0xb4, 0xe6, 0x58, 0x9e, 0xfe, 0xab, 0x13, 0xd0, 0x3b, 0x50, 0xc7, 0x11, 0x52,
0x3f, 0xad, 0x09, 0x21, 0xe7, 0xa7, 0x42, 0xc7, 0xd8, 0x8f, 0xa6, 0x6c, 0x42, 0x75, 0x04, 0xa2,
0x7e, 0x2a, 0x94, 0x82, 0x1b, 0x9f, 0xcf, 0xde, 0xff, 0x9f, 0xaf, 0x6e, 0xac, 0xfd, 0xf4, 0xab,
0x1b, 0x6b, 0xff, 0xfb, 0xd5, 0x8d, 0xb5, 0x9f, 0x7c, 0x7d, 0xe3, 0x8d, 0x9f, 0x7e, 0x7d, 0xe3,
0x8d, 0x9f, 0x7d, 0x7d, 0xe3, 0x8d, 0x2f, 0xdf, 0x54, 0xbf, 0x9f, 0x7f, 0xf6, 0x0b, 0xf2, 0x57,
0xf0, 0x9f, 0xfe, 0x7f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x64, 0x71, 0xe6, 0xb7, 0x63, 0x7f, 0x00,
0x00,
}
// This is a compile-time assertion to ensure that this generated file
@ -505,6 +504,7 @@ type ClientCommandsHandler interface {
ObjectUndo(context.Context, *pb.RpcObjectUndoRequest) *pb.RpcObjectUndoResponse
ObjectRedo(context.Context, *pb.RpcObjectRedoRequest) *pb.RpcObjectRedoResponse
ObjectListExport(context.Context, *pb.RpcObjectListExportRequest) *pb.RpcObjectListExportResponse
ObjectExport(context.Context, *pb.RpcObjectExportRequest) *pb.RpcObjectExportResponse
ObjectBookmarkFetch(context.Context, *pb.RpcObjectBookmarkFetchRequest) *pb.RpcObjectBookmarkFetchResponse
ObjectToBookmark(context.Context, *pb.RpcObjectToBookmarkRequest) *pb.RpcObjectToBookmarkResponse
ObjectImport(context.Context, *pb.RpcObjectImportRequest) *pb.RpcObjectImportResponse
@ -749,8 +749,6 @@ type ClientCommandsHandler interface {
AIAutofill(context.Context, *pb.RpcAIAutofillRequest) *pb.RpcAIAutofillResponse
AIListSummary(context.Context, *pb.RpcAIListSummaryRequest) *pb.RpcAIListSummaryResponse
AIObjectCreateFromUrl(context.Context, *pb.RpcAIObjectCreateFromUrlRequest) *pb.RpcAIObjectCreateFromUrlResponse
// Push
PushNotificationRegisterToken(context.Context, *pb.RpcPushNotificationRegisterTokenRequest) *pb.RpcPushNotificationRegisterTokenResponse
}
func registerClientCommandsHandler(srv ClientCommandsHandler) {
@ -2657,6 +2655,26 @@ func ObjectListExport(b []byte) (resp []byte) {
return resp
}
func ObjectExport(b []byte) (resp []byte) {
defer func() {
if PanicHandler != nil {
if r := recover(); r != nil {
resp, _ = (&pb.RpcObjectExportResponse{Error: &pb.RpcObjectExportResponseError{Code: pb.RpcObjectExportResponseError_UNKNOWN_ERROR, Description: "panic recovered"}}).Marshal()
PanicHandler(r)
}
}
}()
in := new(pb.RpcObjectExportRequest)
if err := in.Unmarshal(b); err != nil {
resp, _ = (&pb.RpcObjectExportResponse{Error: &pb.RpcObjectExportResponseError{Code: pb.RpcObjectExportResponseError_BAD_INPUT, Description: err.Error()}}).Marshal()
return resp
}
resp, _ = clientCommandsHandler.ObjectExport(context.Background(), in).Marshal()
return resp
}
func ObjectBookmarkFetch(b []byte) (resp []byte) {
defer func() {
if PanicHandler != nil {
@ -6557,26 +6575,6 @@ func AIObjectCreateFromUrl(b []byte) (resp []byte) {
return resp
}
func PushNotificationRegisterToken(b []byte) (resp []byte) {
defer func() {
if PanicHandler != nil {
if r := recover(); r != nil {
resp, _ = (&pb.RpcPushNotificationRegisterTokenResponse{Error: &pb.RpcPushNotificationRegisterTokenResponseError{Code: pb.RpcPushNotificationRegisterTokenResponseError_UNKNOWN_ERROR, Description: "panic recovered"}}).Marshal()
PanicHandler(r)
}
}
}()
in := new(pb.RpcPushNotificationRegisterTokenRequest)
if err := in.Unmarshal(b); err != nil {
resp, _ = (&pb.RpcPushNotificationRegisterTokenResponse{Error: &pb.RpcPushNotificationRegisterTokenResponseError{Code: pb.RpcPushNotificationRegisterTokenResponseError_BAD_INPUT, Description: err.Error()}}).Marshal()
return resp
}
resp, _ = clientCommandsHandler.PushNotificationRegisterToken(context.Background(), in).Marshal()
return resp
}
var PanicHandler func(v interface{})
func CommandAsync(cmd string, data []byte, callback func(data []byte)) {
@ -6773,6 +6771,8 @@ func CommandAsync(cmd string, data []byte, callback func(data []byte)) {
cd = ObjectRedo(data)
case "ObjectListExport":
cd = ObjectListExport(data)
case "ObjectExport":
cd = ObjectExport(data)
case "ObjectBookmarkFetch":
cd = ObjectBookmarkFetch(data)
case "ObjectToBookmark":
@ -7163,8 +7163,6 @@ func CommandAsync(cmd string, data []byte, callback func(data []byte)) {
cd = AIListSummary(data)
case "AIObjectCreateFromUrl":
cd = AIObjectCreateFromUrl(data)
case "PushNotificationRegisterToken":
cd = PushNotificationRegisterToken(data)
default:
log.Errorf("unknown command type: %s\n", cmd)
}
@ -8517,6 +8515,20 @@ func (h *ClientCommandsHandlerProxy) ObjectListExport(ctx context.Context, req *
call, _ := actualCall(ctx, req)
return call.(*pb.RpcObjectListExportResponse)
}
func (h *ClientCommandsHandlerProxy) ObjectExport(ctx context.Context, req *pb.RpcObjectExportRequest) *pb.RpcObjectExportResponse {
actualCall := func(ctx context.Context, req any) (any, error) {
return h.client.ObjectExport(ctx, req.(*pb.RpcObjectExportRequest)), nil
}
for _, interceptor := range h.interceptors {
toCall := actualCall
currentInterceptor := interceptor
actualCall = func(ctx context.Context, req any) (any, error) {
return currentInterceptor(ctx, req, "ObjectExport", toCall)
}
}
call, _ := actualCall(ctx, req)
return call.(*pb.RpcObjectExportResponse)
}
func (h *ClientCommandsHandlerProxy) ObjectBookmarkFetch(ctx context.Context, req *pb.RpcObjectBookmarkFetchRequest) *pb.RpcObjectBookmarkFetchResponse {
actualCall := func(ctx context.Context, req any) (any, error) {
return h.client.ObjectBookmarkFetch(ctx, req.(*pb.RpcObjectBookmarkFetchRequest)), nil
@ -11247,17 +11259,3 @@ func (h *ClientCommandsHandlerProxy) AIObjectCreateFromUrl(ctx context.Context,
call, _ := actualCall(ctx, req)
return call.(*pb.RpcAIObjectCreateFromUrlResponse)
}
func (h *ClientCommandsHandlerProxy) PushNotificationRegisterToken(ctx context.Context, req *pb.RpcPushNotificationRegisterTokenRequest) *pb.RpcPushNotificationRegisterTokenResponse {
actualCall := func(ctx context.Context, req any) (any, error) {
return h.client.PushNotificationRegisterToken(ctx, req.(*pb.RpcPushNotificationRegisterTokenRequest)), nil
}
for _, interceptor := range h.interceptors {
toCall := actualCall
currentInterceptor := interceptor
actualCall = func(ctx context.Context, req any) (any, error) {
return currentInterceptor(ctx, req, "PushNotificationRegisterToken", toCall)
}
}
call, _ := actualCall(ctx, req)
return call.(*pb.RpcPushNotificationRegisterTokenResponse)
}

View file

@ -0,0 +1,53 @@
package apicore
import (
"context"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
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 ClientCommands interface {
// Wallet
AccountLocalLinkNewChallenge(context.Context, *pb.RpcAccountLocalLinkNewChallengeRequest) *pb.RpcAccountLocalLinkNewChallengeResponse
AccountLocalLinkSolveChallenge(context.Context, *pb.RpcAccountLocalLinkSolveChallengeRequest) *pb.RpcAccountLocalLinkSolveChallengeResponse
WalletCreateSession(context.Context, *pb.RpcWalletCreateSessionRequest) *pb.RpcWalletCreateSessionResponse
// Space
WorkspaceCreate(context.Context, *pb.RpcWorkspaceCreateRequest) *pb.RpcWorkspaceCreateResponse
WorkspaceOpen(context.Context, *pb.RpcWorkspaceOpenRequest) *pb.RpcWorkspaceOpenResponse
WorkspaceSetInfo(context.Context, *pb.RpcWorkspaceSetInfoRequest) *pb.RpcWorkspaceSetInfoResponse
SpaceRequestApprove(context.Context, *pb.RpcSpaceRequestApproveRequest) *pb.RpcSpaceRequestApproveResponse
SpaceRequestDecline(context.Context, *pb.RpcSpaceRequestDeclineRequest) *pb.RpcSpaceRequestDeclineResponse
SpaceParticipantRemove(context.Context, *pb.RpcSpaceParticipantRemoveRequest) *pb.RpcSpaceParticipantRemoveResponse
SpaceParticipantPermissionsChange(context.Context, *pb.RpcSpaceParticipantPermissionsChangeRequest) *pb.RpcSpaceParticipantPermissionsChangeResponse
// Object
ObjectShow(context.Context, *pb.RpcObjectShowRequest) *pb.RpcObjectShowResponse
ObjectCreate(context.Context, *pb.RpcObjectCreateRequest) *pb.RpcObjectCreateResponse
ObjectCreateBookmark(context.Context, *pb.RpcObjectCreateBookmarkRequest) *pb.RpcObjectCreateBookmarkResponse
ObjectSearch(context.Context, *pb.RpcObjectSearchRequest) *pb.RpcObjectSearchResponse
ObjectSearchSubscribe(context.Context, *pb.RpcObjectSearchSubscribeRequest) *pb.RpcObjectSearchSubscribeResponse
ObjectSetDetails(context.Context, *pb.RpcObjectSetDetailsRequest) *pb.RpcObjectSetDetailsResponse
ObjectSetIsArchived(context.Context, *pb.RpcObjectSetIsArchivedRequest) *pb.RpcObjectSetIsArchivedResponse
ObjectListExport(context.Context, *pb.RpcObjectListExportRequest) *pb.RpcObjectListExportResponse
// List
ObjectCollectionAdd(context.Context, *pb.RpcObjectCollectionAddRequest) *pb.RpcObjectCollectionAddResponse
ObjectCollectionRemove(context.Context, *pb.RpcObjectCollectionRemoveRequest) *pb.RpcObjectCollectionRemoveResponse
// Property
ObjectRelationAddFeatured(context.Context, *pb.RpcObjectRelationAddFeaturedRequest) *pb.RpcObjectRelationAddFeaturedResponse
// Block
BlockCreate(context.Context, *pb.RpcBlockCreateRequest) *pb.RpcBlockCreateResponse
BlockPaste(context.Context, *pb.RpcBlockPasteRequest) *pb.RpcBlockPasteResponse
}

View file

@ -0,0 +1,95 @@
// 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"
)
// MockAccountService is an autogenerated mock type for the AccountService type
type MockAccountService struct {
mock.Mock
}
type MockAccountService_Expecter struct {
mock *mock.Mock
}
func (_m *MockAccountService) EXPECT() *MockAccountService_Expecter {
return &MockAccountService_Expecter{mock: &_m.Mock}
}
// GetInfo provides a mock function with given fields: ctx
func (_m *MockAccountService) GetInfo(ctx context.Context) (*model.AccountInfo, error) {
ret := _m.Called(ctx)
if len(ret) == 0 {
panic("no return value specified for GetInfo")
}
var r0 *model.AccountInfo
var r1 error
if rf, ok := ret.Get(0).(func(context.Context) (*model.AccountInfo, error)); ok {
return rf(ctx)
}
if rf, ok := ret.Get(0).(func(context.Context) *model.AccountInfo); ok {
r0 = rf(ctx)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AccountInfo)
}
}
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockAccountService_GetInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetInfo'
type MockAccountService_GetInfo_Call struct {
*mock.Call
}
// GetInfo is a helper method to define mock.On call
// - ctx context.Context
func (_e *MockAccountService_Expecter) GetInfo(ctx interface{}) *MockAccountService_GetInfo_Call {
return &MockAccountService_GetInfo_Call{Call: _e.mock.On("GetInfo", ctx)}
}
func (_c *MockAccountService_GetInfo_Call) Run(run func(ctx context.Context)) *MockAccountService_GetInfo_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context))
})
return _c
}
func (_c *MockAccountService_GetInfo_Call) Return(_a0 *model.AccountInfo, _a1 error) *MockAccountService_GetInfo_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockAccountService_GetInfo_Call) RunAndReturn(run func(context.Context) (*model.AccountInfo, error)) *MockAccountService_GetInfo_Call {
_c.Call.Return(run)
return _c
}
// NewMockAccountService creates a new instance of MockAccountService. 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 NewMockAccountService(t interface {
mock.TestingT
Cleanup(func())
}) *MockAccountService {
mock := &MockAccountService{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,96 @@
// 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

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -10,15 +10,16 @@ import (
// DisplayCodeHandler starts a new challenge and returns the challenge ID
//
// @Summary Start new challenge
// @Tags auth
// @Accept json
// @Produce json
// @Param app_name query string true "App name requesting the challenge"
// @Success 200 {object} DisplayCodeResponse "Challenge ID"
// @Failure 400 {object} util.ValidationError "Invalid input"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /auth/display_code [post]
// @Summary Start new challenge
// @Description This endpoint initiates a secure authentication flow by generating a new challenge. Clients must supply the name of the application (via a query parameter) that is requesting authentication. 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. In essence, this endpoint “boots up” the login process and is the first step in a multi-phase authentication sequence.
// @Tags auth
// @Accept json
// @Produce json
// @Param app_name query string true "App name requesting the challenge"
// @Success 200 {object} DisplayCodeResponse "Challenge ID"
// @Failure 400 {object} util.ValidationError "Invalid input"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /auth/display_code [post]
func DisplayCodeHandler(s *AuthService) gin.HandlerFunc {
return func(c *gin.Context) {
appName := c.Query("app_name")
@ -40,16 +41,17 @@ func DisplayCodeHandler(s *AuthService) gin.HandlerFunc {
// TokenHandler retrieves an authentication token using a code and challenge ID
//
// @Summary Retrieve token
// @Tags auth
// @Accept json
// @Produce json
// @Param challenge_id query string true "Challenge ID"
// @Param code query string true "4-digit code retrieved from Anytype Desktop app"
// @Success 200 {object} TokenResponse "Authentication token"
// @Failure 400 {object} util.ValidationError "Invalid input"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /auth/token [post]
// @Summary Retrieve token
// @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 an ephemeral session token together with a permanent app key. These tokens are then used in subsequent API requests to authorize access. This endpoint is central to ensuring that only properly authenticated sessions can access further resources.
// @Tags auth
// @Accept json
// @Produce json
// @Param challenge_id query string true "Challenge ID"
// @Param code query string true "4-digit code retrieved from Anytype Desktop app"
// @Success 200 {object} TokenResponse "Authentication token"
// @Failure 400 {object} util.ValidationError "Invalid input"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /auth/token [post]
func TokenHandler(s *AuthService) gin.HandlerFunc {
return func(c *gin.Context) {
challengeId := c.Query("challenge_id")

View file

@ -1,10 +1,10 @@
package auth
type DisplayCodeResponse struct {
ChallengeId string `json:"challenge_id" example:"67647f5ecda913e9a2e11b26"`
ChallengeId string `json:"challenge_id" example:"67647f5ecda913e9a2e11b26"` // The challenge id associated with the displayed code and needed to solve the challenge for token
}
type TokenResponse struct {
SessionToken string `json:"session_token" example:"eyJhbGciOeJIRzI1NiIsInR5cCI6IkpXVCJ1.eyJzZWVkIjaiY0dmVndlUnAifQ.Y1EZecYnwmvMkrXKOa2XJnAbaRt34urBabe06tmDQII"`
AppKey string `json:"app_key" example:"zhSG/zQRmgADyilWPtgdnfo1qD60oK02/SVgi1GaFt6="`
SessionToken string `json:"session_token" example:"eyJhbGciOeJIRzI1NiIsInR5cCI6IkpXVCJ1.eyJzZWVkIjaiY0dmVndlUnAifQ.Y1EZecYnwmvMkrXKOa2XJnAbaRt34urBabe06tmDQII"` // The ephemeral session token
AppKey string `json:"app_key" example:"zhSG/zQRmgADyilWPtgdnfo1qD60oK02/SVgi1GaFt6="` // The permanent app key
}

View file

@ -4,8 +4,8 @@ import (
"context"
"errors"
"github.com/anyproto/anytype-heart/core/api/apicore"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pb/service"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
@ -22,10 +22,10 @@ type Service interface {
}
type AuthService struct {
mw service.ClientCommandsServer
mw apicore.ClientCommands
}
func NewService(mw service.ClientCommandsServer) *AuthService {
func NewService(mw apicore.ClientCommands) *AuthService {
return &AuthService{mw: mw}
}

View file

@ -7,8 +7,8 @@ import (
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/anyproto/anytype-heart/core/api/apicore/mock_apicore"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pb/service/mock_service"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
@ -22,16 +22,16 @@ const (
type fixture struct {
*AuthService
mwMock *mock_service.MockClientCommandsServer
mwMock *mock_apicore.MockClientCommands
}
func newFixture(t *testing.T) *fixture {
mw := mock_service.NewMockClientCommandsServer(t)
authService := NewService(mw)
mwMock := mock_apicore.NewMockClientCommands(t)
authService := NewService(mwMock)
return &fixture{
AuthService: authService,
mwMock: mw,
mwMock: mwMock,
}
}

View file

@ -10,33 +10,29 @@ import (
// GetObjectExportHandler exports an object in specified format
//
// @Summary Export object
// @Tags export
// @Accept json
// @Produce json
// @Param space_id path string true "Space ID"
// @Param object_id path string true "Object ID"
// @Param format path string true "Export format" Enums(markdown,protobuf)
// @Success 200 {object} ObjectExportResponse "Object exported successfully"
// @Failure 400 {object} util.ValidationError "Bad request"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /spaces/{space_id}/objects/{object_id}/export/{format} [post]
// @Summary Export object
// @Description This endpoint exports a single object from the specified space into a desired format. The export format is provided as a path parameter (currently supporting “markdown” only). The endpoint calls the export service which converts the objects content into the requested format. It is useful for sharing, or displaying the markdown representation of the objecte externally.
// @Tags export
// @Accept json
// @Produce json
// @Param space_id path string true "Space ID"
// @Param object_id path string true "Object ID"
// @Param format path string true "Export format" Enums(markdown)
// @Success 200 {object} ObjectExportResponse "Object exported successfully"
// @Failure 400 {object} util.ValidationError "Bad request"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/objects/{object_id}/{format} [get]
func GetObjectExportHandler(s *ExportService) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
objectId := c.Param("object_id")
format := c.Query("format")
format := c.Param("format")
objectAsRequest := ObjectExportRequest{}
if err := c.ShouldBindJSON(&objectAsRequest); err != nil {
apiErr := util.CodeToAPIError(http.StatusBadRequest, ErrBadInput.Error())
c.JSON(http.StatusBadRequest, apiErr)
return
}
outputPath, err := s.GetObjectExport(c.Request.Context(), spaceId, objectId, format, objectAsRequest.Path)
code := util.MapErrorCode(err, util.ErrToCode(ErrFailedExportObjectAsMarkdown, http.StatusInternalServerError))
markdown, err := s.GetObjectExport(c.Request.Context(), spaceId, objectId, format)
code := util.MapErrorCode(err,
util.ErrToCode(ErrInvalidExportFormat, http.StatusInternalServerError))
if code != http.StatusOK {
apiErr := util.CodeToAPIError(code, err.Error())
@ -44,6 +40,6 @@ func GetObjectExportHandler(s *ExportService) gin.HandlerFunc {
return
}
c.JSON(http.StatusOK, ObjectExportResponse{Path: outputPath})
c.JSON(http.StatusOK, ObjectExportResponse{Markdown: markdown})
}
}

View file

@ -1,9 +1,5 @@
package export
type ObjectExportRequest struct {
Path string `json:"path" example:"/path/to/export"`
}
type ObjectExportResponse struct {
Path string `json:"path" example:"/path/to/export"`
Markdown string `json:"markdown" example:"# This is the title\n..."`
}

View file

@ -4,14 +4,12 @@ import (
"context"
"errors"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pb/service"
"github.com/anyproto/anytype-heart/core/api/apicore"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
var (
ErrFailedExportObjectAsMarkdown = errors.New("failed to export object as markdown")
ErrBadInput = errors.New("bad input")
ErrInvalidExportFormat = errors.New("format is not supported")
)
type Service interface {
@ -19,34 +17,26 @@ type Service interface {
}
type ExportService struct {
mw service.ClientCommandsServer
AccountInfo *model.AccountInfo
mw apicore.ClientCommands
exportService apicore.ExportService
}
func NewService(mw service.ClientCommandsServer) *ExportService {
return &ExportService{mw: mw}
func NewService(mw apicore.ClientCommands, exportService apicore.ExportService) *ExportService {
return &ExportService{mw: mw, exportService: exportService}
}
// GetObjectExport retrieves an object from a space and exports it as a specific format.
func (s *ExportService) GetObjectExport(ctx context.Context, spaceId string, objectId string, format string, path string) (string, error) {
resp := s.mw.ObjectListExport(ctx, &pb.RpcObjectListExportRequest{
SpaceId: spaceId,
Path: path,
ObjectIds: []string{objectId},
Format: s.mapStringToFormat(format),
Zip: false,
IncludeNested: false,
IncludeFiles: true,
IsJson: false,
IncludeArchived: false,
NoProgress: true,
})
if resp.Error.Code != pb.RpcObjectListExportResponseError_NULL {
return "", ErrFailedExportObjectAsMarkdown
func (s *ExportService) GetObjectExport(ctx context.Context, spaceId string, objectId string, format string) (string, error) {
if format != "markdown" {
return "", ErrInvalidExportFormat
}
return resp.Path, nil
result, err := s.exportService.ExportSingleInMemory(ctx, spaceId, objectId, s.mapStringToFormat(format))
if err != nil {
return "", err
}
return result, nil
}
// mapStringToFormat maps a format string to an ExportFormat enum.

View file

@ -1,15 +1,9 @@
package export
import (
"context"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pb/service/mock_service"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/core/api/apicore/mock_apicore"
)
const (
@ -22,113 +16,117 @@ const (
type fixture struct {
*ExportService
mwMock *mock_service.MockClientCommandsServer
exportMock *mock_apicore.MockExportService
mwMock *mock_apicore.MockClientCommands
}
func newFixture(t *testing.T) *fixture {
mw := mock_service.NewMockClientCommandsServer(t)
exportService := NewService(mw)
mwMock := mock_apicore.NewMockClientCommands(t)
exportMock := mock_apicore.NewMockExportService(t)
exportService := NewService(mwMock, exportMock)
return &fixture{
ExportService: exportService,
mwMock: mw,
exportMock: exportMock,
mwMock: mwMock,
}
}
func TestExportService_GetObjectExport(t *testing.T) {
t.Run("successful export to markdown", func(t *testing.T) {
// Given
ctx := context.Background()
fx := newFixture(t)
// Mock the ObjectListExport call
fx.mwMock.
On("ObjectListExport", mock.Anything, &pb.RpcObjectListExportRequest{
SpaceId: spaceID,
Path: exportPath,
ObjectIds: []string{objectID},
Format: model.Export_Markdown,
Zip: false,
IncludeNested: false,
IncludeFiles: true,
IsJson: false,
IncludeArchived: false,
NoProgress: true,
}).
Return(&pb.RpcObjectListExportResponse{
Path: exportPath,
Error: &pb.RpcObjectListExportResponseError{
Code: pb.RpcObjectListExportResponseError_NULL,
},
}).
Once()
// When
gotPath, err := fx.GetObjectExport(ctx, spaceID, objectID, exportFormat, exportPath)
// Then
require.NoError(t, err)
require.Equal(t, exportPath, gotPath)
fx.mwMock.AssertExpectations(t)
})
t.Run("failed export returns error", func(t *testing.T) {
// Given
ctx := context.Background()
fx := newFixture(t)
// Mock the ObjectListExport call to return an error code
fx.mwMock.
On("ObjectListExport", mock.Anything, mock.Anything).
Return(&pb.RpcObjectListExportResponse{
Path: "",
Error: &pb.RpcObjectListExportResponseError{
Code: pb.RpcObjectListExportResponseError_UNKNOWN_ERROR,
},
}).
Once()
// When
gotPath, err := fx.GetObjectExport(ctx, spaceID, objectID, exportFormat, exportPath)
// Then
require.Error(t, err)
require.Empty(t, gotPath)
require.ErrorIs(t, err, ErrFailedExportObjectAsMarkdown)
fx.mwMock.AssertExpectations(t)
})
t.Run("unrecognized format defaults to markdown", func(t *testing.T) {
ctx := context.Background()
fx := newFixture(t)
fx.mwMock.
On("ObjectListExport", mock.Anything, &pb.RpcObjectListExportRequest{
SpaceId: spaceID,
Path: exportPath,
ObjectIds: []string{objectID},
Format: model.Export_Markdown, // fallback
Zip: false,
IncludeNested: false,
IncludeFiles: true,
IsJson: false,
IncludeArchived: false,
NoProgress: true,
}).
Return(&pb.RpcObjectListExportResponse{
Path: exportPath,
Error: &pb.RpcObjectListExportResponseError{
Code: pb.RpcObjectListExportResponseError_NULL,
},
}).
Once()
// When
gotPath, err := fx.GetObjectExport(ctx, spaceID, objectID, unrecognizedFormat, exportPath) //
// Then
require.NoError(t, err)
require.Equal(t, exportPath, gotPath)
fx.mwMock.AssertExpectations(t)
})
// TODO: revive tests once export is finalized
// t.Run("successful export to markdown", func(t *testing.T) {
// // Given
// ctx := context.Background()
// fx := newFixture(t)
//
// // Mock the ObjectListExport call
// fx.mwMock.
// On("ObjectListExport", mock.Anything, &pb.RpcObjectListExportRequest{
// SpaceId: spaceID,
// Path: exportPath,
// ObjectIds: []string{objectID},
// Format: model.Export_Markdown,
// Zip: false,
// IncludeNested: false,
// IncludeFiles: true,
// IsJson: false,
// IncludeArchived: false,
// NoProgress: true,
// }).
// Return(&pb.RpcObjectListExportResponse{
// Path: exportPath,
// Error: &pb.RpcObjectListExportResponseError{
// Code: pb.RpcObjectListExportResponseError_NULL,
// },
// }).
// Once()
//
// // When
// gotPath, err := fx.GetObjectExport(ctx, spaceID, objectID, exportFormat)
//
// // Then
// require.NoError(t, err)
// require.Equal(t, exportPath, gotPath)
// fx.mwMock.AssertExpectations(t)
// })
//
// t.Run("failed export returns error", func(t *testing.T) {
// // Given
// ctx := context.Background()
// fx := newFixture(t)
//
// // Mock the ObjectListExport call to return an error code
// fx.mwMock.
// On("ObjectListExport", mock.Anything, mock.Anything).
// Return(&pb.RpcObjectListExportResponse{
// Path: "",
// Error: &pb.RpcObjectListExportResponseError{
// Code: pb.RpcObjectListExportResponseError_UNKNOWN_ERROR,
// },
// }).
// Once()
//
// // When
// gotPath, err := fx.GetObjectExport(ctx, spaceID, objectID, exportFormat)
//
// // Then
// require.Error(t, err)
// require.Empty(t, gotPath)
// require.ErrorIs(t, err, ErrFailedExportObjectAsMarkdown)
// fx.mwMock.AssertExpectations(t)
// })
//
// t.Run("unrecognized format defaults to markdown", func(t *testing.T) {
// ctx := context.Background()
// fx := newFixture(t)
//
// fx.mwMock.
// On("ObjectListExport", mock.Anything, &pb.RpcObjectListExportRequest{
// SpaceId: spaceID,
// Path: exportPath,
// ObjectIds: []string{objectID},
// Format: model.Export_Markdown, // fallback
// Zip: false,
// IncludeNested: false,
// IncludeFiles: true,
// IsJson: false,
// IncludeArchived: false,
// NoProgress: true,
// }).
// Return(&pb.RpcObjectListExportResponse{
// Path: exportPath,
// Error: &pb.RpcObjectListExportResponseError{
// Code: pb.RpcObjectListExportResponseError_NULL,
// },
// }).
// Once()
//
// // When
// gotPath, err := fx.GetObjectExport(ctx, spaceID, objectID, unrecognizedFormat)
//
// // Then
// require.NoError(t, err)
// require.Equal(t, exportPath, gotPath)
// fx.mwMock.AssertExpectations(t)
// })
}

View file

@ -0,0 +1,175 @@
package list
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/anyproto/anytype-heart/core/api/pagination"
"github.com/anyproto/anytype-heart/core/api/util"
)
// GetListViewsHandler
//
// @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 configuration details such as layout, applied filters, and sorting options, enabling clients to render the list according to user preferences and context. This endpoint supports pagination parameters to control the number of views returned and the starting point of the result set.
// @Tags lists
// @Produce json
// @Param space_id path string true "Space ID"
// @Param list_id path string true "List ID"
// @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[View] "List of views"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @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]
func GetListViewsHandler(s *ListService) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
listId := c.Param("list_id")
offset := c.GetInt("offset")
limit := c.GetInt("limit")
views, total, hasMore, err := s.GetListViews(c, spaceId, listId, offset, limit)
code := util.MapErrorCode(err,
util.ErrToCode(ErrFailedGetList, http.StatusNotFound),
util.ErrToCode(ErrFailedGetListDataview, http.StatusInternalServerError),
)
if code != http.StatusOK {
apiErr := util.CodeToAPIError(code, err.Error())
c.JSON(code, apiErr)
return
}
pagination.RespondWithPagination(c, http.StatusOK, views, total, offset, limit, hasMore)
}
}
// GetObjectsInListHandler
//
// @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.
// @Tags lists
// @Produce json
// @Param space_id path string true "Space ID"
// @Param list_id path string true "List ID"
// @Param view_id path string true "View ID"
// @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[object.Object] "List of objects"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @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]
func GetObjectsInListHandler(s *ListService) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
listId := c.Param("list_id")
viewId := c.Param("view_id")
offset := c.GetInt("offset")
limit := c.GetInt("limit")
objects, total, hasMore, err := s.GetObjectsInList(c, spaceId, listId, viewId, offset, limit)
code := util.MapErrorCode(err,
util.ErrToCode(ErrFailedGetList, http.StatusNotFound),
util.ErrToCode(ErrFailedGetListDataview, http.StatusInternalServerError),
util.ErrToCode(ErrFailedGetListDataviewView, http.StatusInternalServerError),
util.ErrToCode(ErrUnsupportedListType, http.StatusInternalServerError),
util.ErrToCode(ErrFailedGetObjectsInList, http.StatusInternalServerError),
util.ErrToCode(util.ErrorTypeNotFound, http.StatusInternalServerError),
)
if code != http.StatusOK {
apiErr := util.CodeToAPIError(code, err.Error())
c.JSON(code, apiErr)
return
}
pagination.RespondWithPagination(c, http.StatusOK, objects, total, offset, limit, hasMore)
}
}
// AddObjectsToListHandler
//
// @Summary Add objects to list
// @Description Enables clients to add 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.
// @Tags lists
// @Accept json
// @Produce json
// @Param space_id path string true "Space ID"
// @Param list_id path string true "List ID"
// @Param objects body []string true "List of object IDs"
// @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 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/lists/{list_id}/objects [post]
func AddObjectsToListHandler(s *ListService) 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 {
apiErr := util.CodeToAPIError(http.StatusBadRequest, err.Error())
c.JSON(http.StatusBadRequest, apiErr)
return
}
err := s.AddObjectsToList(c, spaceId, listId, objects)
code := util.MapErrorCode(err,
util.ErrToCode(ErrFailedAddObjectsToList, http.StatusInternalServerError),
)
if code != http.StatusOK {
apiErr := util.CodeToAPIError(code, err.Error())
c.JSON(code, apiErr)
return
}
c.JSON(http.StatusOK, "Objects added successfully")
}
}
// RemoveObjectFromListHandler
//
// @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. It's subject to rate limiting and returns a success message on completion. It is used for dynamically managing collections without affecting the underlying object data.
// @Tags lists
// @Produce json
// @Param space_id path string true "Space ID"
// @Param list_id path string true "List ID"
// @Param object_id path string true "Object ID"
// @Success 200 {object} string "Objects removed successfully"
// @Failure 400 {object} util.ValidationError "Bad request"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 404 {object} util.NotFoundError "Not found"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/lists/{list_id}/objects/{object_id} [delete]
func RemoveObjectFromListHandler(s *ListService) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
listId := c.Param("list_id")
objectId := c.Param("object_id")
err := s.RemoveObjectsFromList(c, spaceId, listId, []string{objectId})
code := util.MapErrorCode(err,
util.ErrToCode(ErrFailedRemoveObjectsFromList, http.StatusInternalServerError),
)
if code != http.StatusOK {
apiErr := util.CodeToAPIError(code, err.Error())
c.JSON(code, apiErr)
return
}
c.JSON(http.StatusOK, "Objects removed successfully")
}
}

View file

@ -0,0 +1,24 @@
package 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
Layout string `json:"layout" example:"grid" enums:"grid,table"` // The layout of the view
Filters []Filter `json:"filters"` // The list of filters
Sorts []Sort `json:"sorts"` // The list of sorts
}
type Filter struct {
Id string `json:"id" example:"67bf3f21cda9134102e2422c"` // The id of the filter
PropertyKey string `json:"property_key" example:"name"` // The property key used for filtering
Format string `json:"format" example:"text" enum:"text,number,select,multi_select,date,file,checkbox,url,email,phone,object"` // The format of the property used for filtering
Condition string `json:"condition" example:"contains" enum:"equal,not_equal,greater,less,greater_or_equal,less_or_equal,like,not_like,in,not_in,empty,not_empty,all_in,not_all_in,exact_in,not_exact_in,exists"` // The filter condition
Value string `json:"value" example:"Some value..."` // The value used for filtering
}
type Sort struct {
Id string `json:"id" example:"67bf3f21cda9134102e2422c"` // The id of the sort
PropertyKey string `json:"property_key" example:"name"` // The property key used for sorting
Format string `json:"format" example:"text" enum:"text,number,select,multi_select,date,file,checkbox,url,email,phone,object"` // The format of the property used for sorting
SortType string `json:"sort_type" example:"asc" enum:"asc,desc,custom"` // The sort direction
}

View file

@ -0,0 +1,244 @@
package list
import (
"context"
"errors"
"github.com/iancoleman/strcase"
"github.com/anyproto/anytype-heart/core/api/apicore"
"github.com/anyproto/anytype-heart/core/api/internal/object"
"github.com/anyproto/anytype-heart/core/api/pagination"
"github.com/anyproto/anytype-heart/core/api/util"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
var (
ErrFailedGetList = errors.New("failed to get list")
ErrFailedGetListDataview = errors.New("failed to get list dataview")
ErrFailedGetListDataviewView = errors.New("failed to get list dataview view")
ErrUnsupportedListType = errors.New("unsupported list type")
ErrFailedGetObjectsInList = errors.New("failed to get objects in list")
ErrFailedAddObjectsToList = errors.New("failed to add objects to list")
ErrFailedRemoveObjectsFromList = errors.New("failed to remove objects from list")
)
type Service interface {
GetListViews(ctx context.Context, spaceId string, listId string, offset, limit int) ([]View, int, bool, error)
GetObjectsInList(ctx context.Context, spaceId string, listId string, viewId string, offset, limit int) ([]object.Object, int, bool, error)
AddObjectsToList(ctx context.Context, spaceId string, listId string, objectIds []string) error
RemoveObjectsFromList(ctx context.Context, spaceId string, listId string, objectIds []string) error
}
type ListService struct {
mw apicore.ClientCommands
objectService *object.ObjectService
}
func NewService(mw apicore.ClientCommands, objectService *object.ObjectService) *ListService {
return &ListService{mw: mw, objectService: objectService}
}
// GetListViews retrieves views of a list
func (s *ListService) GetListViews(ctx context.Context, spaceId string, listId string, offset, limit int) ([]View, int, bool, error) {
resp := s.mw.ObjectShow(ctx, &pb.RpcObjectShowRequest{
SpaceId: spaceId,
ObjectId: listId,
})
if resp.Error != nil && resp.Error.Code != pb.RpcObjectShowResponseError_NULL {
return nil, 0, false, ErrFailedGetList
}
var dataviewBlock *model.Block
for _, block := range resp.ObjectView.Blocks {
if block.Id == "dataview" {
dataviewBlock = block
break
}
}
if dataviewBlock == nil {
return nil, 0, false, ErrFailedGetListDataview
}
var views []View
switch content := dataviewBlock.Content.(type) {
case *model.BlockContentOfDataview:
for _, view := range content.Dataview.Views {
var filters []Filter
for _, f := range view.Filters {
filters = append(filters, Filter{
Id: f.Id,
PropertyKey: f.RelationKey,
Format: s.objectService.MapRelationFormat(f.Format),
Condition: strcase.ToSnake(model.BlockContentDataviewFilterCondition_name[int32(f.Condition)]),
Value: f.Value.GetStringValue(),
})
}
var sorts []Sort
for _, srt := range view.Sorts {
sorts = append(sorts, Sort{
Id: srt.Id,
PropertyKey: srt.RelationKey,
Format: s.objectService.MapRelationFormat(srt.Format),
SortType: strcase.ToSnake(model.BlockContentDataviewSortType_name[int32(srt.Type)]),
})
}
views = append(views, View{
Id: view.Id,
Name: view.Name,
Layout: s.mapDataviewTypeName(view.Type),
Filters: filters,
Sorts: sorts,
})
}
default:
return nil, 0, false, ErrFailedGetListDataview
}
total := len(views)
paginatedViews, hasMore := pagination.Paginate(views, offset, limit)
return paginatedViews, total, hasMore, nil
}
// GetObjectsInList retrieves objects in a list
func (s *ListService) GetObjectsInList(ctx context.Context, spaceId string, listId string, viewId string, offset, limit int) ([]object.Object, int, bool, error) {
resp := s.mw.ObjectShow(ctx, &pb.RpcObjectShowRequest{
SpaceId: spaceId,
ObjectId: listId,
})
if resp.Error != nil && resp.Error.Code != pb.RpcObjectShowResponseError_NULL {
return nil, 0, false, ErrFailedGetList
}
var dataviewBlock *model.Block
for _, block := range resp.ObjectView.Blocks {
if block.Id == "dataview" {
dataviewBlock = block
break
}
}
if dataviewBlock == nil {
return nil, 0, false, ErrFailedGetListDataview
}
var sorts []*model.BlockContentDataviewSort
var filters []*model.BlockContentDataviewFilter
switch content := dataviewBlock.Content.(type) {
case *model.BlockContentOfDataview:
// if view not specified: return all objects without filtering and sorting
if viewId != "" {
var targetView *model.BlockContentDataviewView
for _, view := range content.Dataview.Views {
if view.Id == viewId {
targetView = view
break
}
}
if targetView == nil {
return nil, 0, false, ErrFailedGetListDataviewView
}
sorts = targetView.Sorts
filters = targetView.Filters
}
default:
return nil, 0, false, ErrFailedGetListDataview
}
var collectionId string
var source []string
listType := s.objectService.GetTypeFromDetails(resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyType.String()].GetStringValue(), resp.ObjectView.Details)
if listType.RecommendedLayout == "set" {
// 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 {
typeKey, err := util.ResolveIdtoUniqueKey(s.mw, spaceId, value.GetStringValue())
if err != nil {
return nil, 0, false, err
}
source = append(source, typeKey)
}
} else if listType.RecommendedLayout == "collection" {
// for collections, we need to search within that collection
collectionId = listId
} else {
return nil, 0, false, ErrUnsupportedListType
}
// TODO: use subscription service with internal: 'true' to not send events to clients
searchResp := s.mw.ObjectSearchSubscribe(ctx, &pb.RpcObjectSearchSubscribeRequest{
SpaceId: spaceId,
Limit: int64(limit), // nolint: gosec
Offset: int64(offset), // nolint: gosec
Keys: []string{bundle.RelationKeyId.String()},
Sorts: sorts,
Filters: filters,
Source: source,
CollectionId: collectionId,
})
// TODO: returned error from ObjectSearchSubscribe is inconsistent with other RPCs: Error is nil instead of Code being NULL
if searchResp.Error != nil && searchResp.Error.Code != pb.RpcObjectSearchSubscribeResponseError_NULL {
return nil, 0, false, ErrFailedGetObjectsInList
}
total := int(searchResp.Counters.Total)
hasMore := searchResp.Counters.Total > int64(offset+limit)
objects := make([]object.Object, 0, len(searchResp.Records))
for _, record := range searchResp.Records {
object, err := s.objectService.GetObject(ctx, spaceId, record.Fields[bundle.RelationKeyId.String()].GetStringValue())
if err != nil {
return nil, 0, false, err
}
objects = append(objects, object)
}
return objects, total, hasMore, nil
}
// AddObjectsToList adds objects to a list
func (s *ListService) AddObjectsToList(ctx context.Context, spaceId string, listId string, objectIds []string) error {
resp := s.mw.ObjectCollectionAdd(ctx, &pb.RpcObjectCollectionAddRequest{
ContextId: listId,
ObjectIds: objectIds,
})
if resp.Error.Code != pb.RpcObjectCollectionAddResponseError_NULL {
return ErrFailedAddObjectsToList
}
return nil
}
// RemoveObjectsFromList removes objects from a list
func (s *ListService) RemoveObjectsFromList(ctx context.Context, spaceId string, listId string, objectIds []string) error {
resp := s.mw.ObjectCollectionRemove(ctx, &pb.RpcObjectCollectionRemoveRequest{
ContextId: listId,
ObjectIds: objectIds,
})
if resp.Error.Code != pb.RpcObjectCollectionRemoveResponseError_NULL {
return ErrFailedRemoveObjectsFromList
}
return nil
}
// mapDataviewTypeName maps the dataview type to a string.
func (s *ListService) mapDataviewTypeName(dataviewType model.BlockContentDataviewViewType) string {
switch dataviewType {
case model.BlockContentDataviewView_Table:
return "grid"
default:
return strcase.ToSnake(model.BlockContentDataviewViewType_name[int32(dataviewType)])
}
}

View file

@ -0,0 +1,947 @@
package list
import (
"context"
"testing"
"github.com/gogo/protobuf/types"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/anyproto/anytype-heart/core/api/apicore/mock_apicore"
"github.com/anyproto/anytype-heart/core/api/internal/object"
"github.com/anyproto/anytype-heart/core/api/internal/space"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
const (
mockedSpaceId = "mocked-space-id"
mockedListId = "mocked-list-id"
mockedTypeId = "mocked-type-id"
mockedSetOfTypeId = "mocked-set-of-type-id"
mockedUniqueKey = "mocked-unique-key"
mockedViewId = "view-1"
offset = 0
limit = 100
)
type fixture struct {
*ListService
mwMock *mock_apicore.MockClientCommands
objectService *object.ObjectService
}
func newFixture(t *testing.T) *fixture {
mwMock := mock_apicore.NewMockClientCommands(t)
spaceService := space.NewService(mwMock)
objectService := object.NewService(mwMock, spaceService)
objectService.AccountInfo = &model.AccountInfo{
TechSpaceId: "mocked-tech-space-id",
GatewayUrl: "http://localhost:31006",
}
listService := NewService(mwMock, objectService)
return &fixture{
ListService: listService,
mwMock: mwMock,
objectService: objectService,
}
}
func TestListService_GetListViews(t *testing.T) {
ctx := context.Background()
t.Run("successful", func(t *testing.T) {
fx := newFixture(t)
// Prepare a view with one sort and one filter
sorts := []*model.BlockContentDataviewSort{
{
Id: "sort-1",
RelationKey: "dummy-sort",
Type: model.BlockContentDataviewSort_Asc,
},
}
filters := []*model.BlockContentDataviewFilter{
{
Id: "filter-1",
RelationKey: "dummy-filter",
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String("dummy-value"),
},
}
view := &model.BlockContentDataviewView{
Id: "view-1",
Name: "Test View",
Sorts: sorts,
Filters: filters,
Type: model.BlockContentDataviewView_Table,
}
resp := &pb.RpcObjectShowResponse{
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
ObjectView: &model.ObjectView{
Blocks: []*model.Block{
{
Id: "dataview",
Content: &model.BlockContentOfDataview{
Dataview: &model.BlockContentDataview{
Views: []*model.BlockContentDataviewView{view},
},
},
},
},
},
}
fx.mwMock.
On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedListId,
}).
Return(resp, nil).Once()
views, total, hasMore, err := fx.GetListViews(ctx, mockedSpaceId, mockedListId, offset, limit)
require.NoError(t, err)
require.Len(t, views, 1)
require.Equal(t, 1, total)
require.False(t, hasMore)
retView := views[0]
require.Equal(t, "view-1", retView.Id)
require.Equal(t, "Test View", retView.Name)
require.Len(t, retView.Filters, 1)
require.Len(t, retView.Sorts, 1)
})
t.Run("object show error", func(t *testing.T) {
fx := newFixture(t)
fx.mwMock.
On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedListId,
}).
Return(&pb.RpcObjectShowResponse{
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_UNKNOWN_ERROR},
}, nil).Once()
_, _, _, err := fx.GetListViews(ctx, mockedSpaceId, mockedListId, offset, limit)
require.ErrorIs(t, err, ErrFailedGetList)
})
t.Run("no dataview block", func(t *testing.T) {
fx := newFixture(t)
fx.mwMock.
On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedListId,
}).
Return(&pb.RpcObjectShowResponse{
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
ObjectView: &model.ObjectView{
Blocks: []*model.Block{
{Id: "non-dataview"},
},
},
}, nil).Once()
_, _, _, err := fx.GetListViews(ctx, mockedSpaceId, mockedListId, offset, limit)
require.ErrorIs(t, err, ErrFailedGetListDataview)
})
t.Run("invalid dataview content", func(t *testing.T) {
fx := newFixture(t)
fx.mwMock.
On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedListId,
}).
Return(&pb.RpcObjectShowResponse{
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
ObjectView: &model.ObjectView{
Blocks: []*model.Block{
{Id: "dataview", Content: nil},
},
},
}, nil).Once()
_, _, _, err := fx.GetListViews(ctx, mockedSpaceId, mockedListId, offset, limit)
require.ErrorIs(t, err, ErrFailedGetListDataview)
})
t.Run("view with no sorts", func(t *testing.T) {
fx := newFixture(t)
// Create a view with filters but no sorts
filters := []*model.BlockContentDataviewFilter{
{
Id: "filter-1",
RelationKey: "dummy-filter",
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String("dummy-value"),
},
}
view := &model.BlockContentDataviewView{
Id: "view-2",
Name: "No Sort View",
Sorts: []*model.BlockContentDataviewSort{},
Filters: filters,
Type: model.BlockContentDataviewView_Table,
}
resp := &pb.RpcObjectShowResponse{
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
ObjectView: &model.ObjectView{
Blocks: []*model.Block{
{
Id: "dataview",
Content: &model.BlockContentOfDataview{
Dataview: &model.BlockContentDataview{
Views: []*model.BlockContentDataviewView{view},
},
},
},
},
},
}
fx.mwMock.
On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedListId,
}).
Return(resp, nil).Once()
views, total, hasMore, err := fx.GetListViews(ctx, mockedSpaceId, mockedListId, offset, limit)
require.NoError(t, err)
require.Len(t, views, 1)
require.Equal(t, 1, total)
require.False(t, hasMore)
})
t.Run("view with multiple sorts", func(t *testing.T) {
fx := newFixture(t)
// Create a view with 2 sorts
sorts := []*model.BlockContentDataviewSort{
{
Id: "sort-1",
RelationKey: "dummy-sort",
Type: model.BlockContentDataviewSort_Asc,
},
{
Id: "sort-2",
RelationKey: "dummy-sort2",
Type: model.BlockContentDataviewSort_Desc,
},
}
filters := []*model.BlockContentDataviewFilter{
{
Id: "filter-1",
RelationKey: "dummy-filter",
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String("dummy-value"),
},
}
view := &model.BlockContentDataviewView{
Id: "view-3",
Name: "Multi-Sort View",
Sorts: sorts,
Filters: filters,
Type: model.BlockContentDataviewView_Table,
}
resp := &pb.RpcObjectShowResponse{
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
ObjectView: &model.ObjectView{
Blocks: []*model.Block{
{
Id: "dataview",
Content: &model.BlockContentOfDataview{
Dataview: &model.BlockContentDataview{
Views: []*model.BlockContentDataviewView{view},
},
},
},
},
},
}
fx.mwMock.
On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedListId,
}).
Return(resp, nil).Once()
views, total, hasMore, err := fx.GetListViews(ctx, mockedSpaceId, mockedListId, offset, limit)
require.NoError(t, err)
require.Len(t, views, 1)
require.Equal(t, 1, total)
require.False(t, hasMore)
require.Equal(t, "view-3", views[0].Id)
require.Len(t, views[0].Sorts, 2)
})
}
func TestListService_GetObjectsInList(t *testing.T) {
t.Run("successful", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
// Prepare a dataview view with dummy sorts and filters.
sorts := []*model.BlockContentDataviewSort{
{
RelationKey: "dummy-sort",
Type: model.BlockContentDataviewSort_Asc,
},
}
filters := []*model.BlockContentDataviewFilter{
{
RelationKey: "dummy-filter",
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String("dummy-value"),
},
}
view := &model.BlockContentDataviewView{
Id: mockedViewId,
Sorts: sorts,
Filters: filters,
}
// Expect the ObjectShow call for the list to return the type in the details and dataview block.
fx.mwMock.
On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedListId,
}).
Return(&pb.RpcObjectShowResponse{
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
ObjectView: &model.ObjectView{
Details: []*model.ObjectViewDetailsSet{
{
Id: mockedListId,
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyType.String(): pbtypes.String(mockedTypeId),
bundle.RelationKeySetOf.String(): pbtypes.StringList([]string{mockedSetOfTypeId}),
},
},
},
{
Id: mockedTypeId,
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyRecommendedLayout.String(): pbtypes.Int64(int64(model.ObjectType_set)),
},
},
},
},
Blocks: []*model.Block{
{
Id: "dataview",
Content: &model.BlockContentOfDataview{
Dataview: &model.BlockContentDataview{
Views: []*model.BlockContentDataviewView{view},
},
},
},
},
},
}, nil).Once()
// Expect the ObjectShow to return the type's unique key.
fx.mwMock.
On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedSetOfTypeId,
}).
Return(&pb.RpcObjectShowResponse{
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
ObjectView: &model.ObjectView{
Details: []*model.ObjectViewDetailsSet{
{
Id: mockedSetOfTypeId,
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyUniqueKey.String(): pbtypes.String(mockedUniqueKey),
},
},
},
},
},
}, nil).Once()
// Expect the ObjectSearchSubscribe call to return one record.
fx.mwMock.
On("ObjectSearchSubscribe", mock.Anything, &pb.RpcObjectSearchSubscribeRequest{
SpaceId: mockedSpaceId,
Limit: int64(limit),
Offset: int64(offset),
Keys: []string{bundle.RelationKeyId.String()},
Sorts: sorts,
Filters: filters,
Source: []string{mockedUniqueKey},
}).
Return(&pb.RpcObjectSearchSubscribeResponse{
Error: &pb.RpcObjectSearchSubscribeResponseError{Code: pb.RpcObjectSearchSubscribeResponseError_NULL},
Counters: &pb.EventObjectSubscriptionCounters{Total: 1},
Records: []*types.Struct{
{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String("object-1"),
},
},
},
}, nil).Once()
// Expect the object service to be called to get details for "object-1".
fx.mwMock.
On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: "object-1",
}).
Return(&pb.RpcObjectShowResponse{
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
ObjectView: &model.ObjectView{
Details: []*model.ObjectViewDetailsSet{
{
Id: "object-1",
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String("object-1"),
bundle.RelationKeyName.String(): pbtypes.String("Object One"),
},
},
},
},
},
}, nil).Once()
// when
objects, total, hasMore, err := fx.GetObjectsInList(ctx, mockedSpaceId, mockedListId, mockedViewId, offset, limit)
// then
require.NoError(t, err)
require.Len(t, objects, 1)
require.Equal(t, 1, total)
require.False(t, hasMore)
require.Equal(t, "object-1", objects[0].Id)
require.Equal(t, "Object One", objects[0].Name)
})
t.Run("successful with empty viewId", func(t *testing.T) {
ctx := context.Background()
fx := newFixture(t)
// Prepare an ObjectShow response with a dataview block containing a view (which will not be used since viewId is empty)
resp := &pb.RpcObjectShowResponse{
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
ObjectView: &model.ObjectView{
Details: []*model.ObjectViewDetailsSet{
{
Id: mockedListId,
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyType.String(): pbtypes.String(mockedTypeId),
bundle.RelationKeySetOf.String(): pbtypes.StringList([]string{mockedSetOfTypeId}),
},
},
},
{
Id: mockedTypeId,
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyRecommendedLayout.String(): pbtypes.Int64(int64(model.ObjectType_collection)),
},
},
},
},
Blocks: []*model.Block{
{
Id: "dataview",
Content: &model.BlockContentOfDataview{
Dataview: &model.BlockContentDataview{
Views: []*model.BlockContentDataviewView{
{
Id: mockedListId,
Sorts: []*model.BlockContentDataviewSort{
{
Id: "view_sort",
RelationKey: bundle.RelationKeyLastModifiedDate.String(),
Format: model.RelationFormat_date,
Type: model.BlockContentDataviewSort_Asc,
},
},
Filters: []*model.BlockContentDataviewFilter{
{
Id: "view_filter",
RelationKey: bundle.RelationKeyStatus.String(),
Format: model.RelationFormat_longtext,
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String("active"),
},
},
},
},
},
},
},
},
},
}
fx.mwMock.
On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedListId,
}).
Return(resp, nil).Once()
// Since viewId is empty, sorts and filters should be nil.
fx.mwMock.
On("ObjectSearchSubscribe", mock.Anything, &pb.RpcObjectSearchSubscribeRequest{
SpaceId: mockedSpaceId,
Limit: int64(limit),
Offset: int64(offset),
Keys: []string{bundle.RelationKeyId.String()},
Sorts: nil,
Filters: nil,
CollectionId: mockedListId,
}).
Return(&pb.RpcObjectSearchSubscribeResponse{
Error: &pb.RpcObjectSearchSubscribeResponseError{Code: pb.RpcObjectSearchSubscribeResponseError_NULL},
Counters: &pb.EventObjectSubscriptionCounters{Total: 1},
Records: []*types.Struct{
{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String("object-1"),
},
},
},
}, nil).Once()
// Expect the ObjectShow call for "object-1" to return its details.
fx.mwMock.
On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: "object-1",
}).
Return(&pb.RpcObjectShowResponse{
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
ObjectView: &model.ObjectView{
Details: []*model.ObjectViewDetailsSet{
{
Id: "object-1",
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String("object-1"),
bundle.RelationKeyName.String(): pbtypes.String("Object One"),
},
},
},
},
},
}, nil).Once()
// when
objects, total, hasMore, err := fx.GetObjectsInList(ctx, mockedSpaceId, mockedListId, "", offset, limit)
// then
require.NoError(t, err)
require.Len(t, objects, 1)
require.Equal(t, 1, total)
require.False(t, hasMore)
require.Equal(t, "object-1", objects[0].Id)
require.Equal(t, "Object One", objects[0].Name)
})
t.Run("object show error", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
// Simulate an error response (non-NULL error code) from ObjectShow for the list.
fx.mwMock.
On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedListId,
}).
Return(&pb.RpcObjectShowResponse{
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_UNKNOWN_ERROR},
}, nil).Once()
// when
_, _, _, err := fx.GetObjectsInList(ctx, mockedSpaceId, mockedListId, "", offset, limit)
// then
require.ErrorIs(t, err, ErrFailedGetList)
})
t.Run("no dataview block", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
// Return an ObjectView that does not contain a block with ID "dataview".
fx.mwMock.
On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedListId,
}).
Return(&pb.RpcObjectShowResponse{
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
ObjectView: &model.ObjectView{
Blocks: []*model.Block{
{Id: "non-dataview"},
},
},
}, nil).Once()
// when
_, _, _, err := fx.GetObjectsInList(ctx, mockedSpaceId, mockedListId, "", offset, limit)
// then
require.ErrorIs(t, err, ErrFailedGetListDataview)
})
t.Run("invalid dataview content", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
// Return a "dataview" block that does not have the expected content type.
fx.mwMock.
On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedListId,
}).
Return(&pb.RpcObjectShowResponse{
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
ObjectView: &model.ObjectView{
Blocks: []*model.Block{
{
Id: "dataview",
Content: nil,
},
},
},
}, nil).Once()
// when
_, _, _, err := fx.GetObjectsInList(ctx, mockedSpaceId, mockedListId, "", offset, limit)
// then
require.ErrorIs(t, err, ErrFailedGetListDataview)
})
t.Run("view not found", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
// Prepare a dataview that only contains a view with an ID different from the one requested.
view := &model.BlockContentDataviewView{
Id: "some-other-view",
Sorts: []*model.BlockContentDataviewSort{},
Filters: []*model.BlockContentDataviewFilter{},
}
fx.mwMock.
On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedListId,
}).
Return(&pb.RpcObjectShowResponse{
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
ObjectView: &model.ObjectView{
Blocks: []*model.Block{
{
Id: "dataview",
Content: &model.BlockContentOfDataview{
Dataview: &model.BlockContentDataview{
Views: []*model.BlockContentDataviewView{view},
},
},
},
},
},
}, nil).Once()
// when
_, _, _, err := fx.GetObjectsInList(ctx, mockedSpaceId, mockedListId, "non-existent-view", offset, limit)
// then
require.ErrorIs(t, err, ErrFailedGetListDataviewView)
})
t.Run("search subscribe error", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
// Prepare an empty dataview view (no sorts/filters).
sorts := []*model.BlockContentDataviewSort{}
filters := []*model.BlockContentDataviewFilter{}
view := &model.BlockContentDataviewView{
Id: mockedViewId,
Sorts: sorts,
Filters: filters,
}
fx.mwMock.
On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedListId,
}).
Return(&pb.RpcObjectShowResponse{
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
ObjectView: &model.ObjectView{
Details: []*model.ObjectViewDetailsSet{
{
Id: mockedListId,
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyType.String(): pbtypes.String(mockedTypeId),
bundle.RelationKeySetOf.String(): pbtypes.StringList([]string{mockedSetOfTypeId}),
},
},
},
{
Id: mockedTypeId,
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyRecommendedLayout.String(): pbtypes.Int64(int64(model.ObjectType_collection)),
},
},
},
},
Blocks: []*model.Block{
{
Id: "dataview",
Content: &model.BlockContentOfDataview{
Dataview: &model.BlockContentDataview{
Views: []*model.BlockContentDataviewView{view},
},
},
},
},
},
}, nil).Once()
// Simulate an error from ObjectSearchSubscribe.
fx.mwMock.
On("ObjectSearchSubscribe", mock.Anything, &pb.RpcObjectSearchSubscribeRequest{
SpaceId: mockedSpaceId,
Limit: int64(limit),
Offset: int64(offset),
Keys: []string{bundle.RelationKeyId.String()},
Sorts: sorts,
Filters: filters,
CollectionId: mockedListId,
}).
Return(&pb.RpcObjectSearchSubscribeResponse{
Error: &pb.RpcObjectSearchSubscribeResponseError{Code: pb.RpcObjectSearchSubscribeResponseError_UNKNOWN_ERROR},
}, nil).Once()
// when
_, _, _, err := fx.GetObjectsInList(ctx, mockedSpaceId, mockedListId, mockedViewId, offset, limit)
// then
require.ErrorIs(t, err, ErrFailedGetObjectsInList)
})
t.Run("get object error", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
var sorts []*model.BlockContentDataviewSort
var filters []*model.BlockContentDataviewFilter
view := &model.BlockContentDataviewView{
Id: mockedViewId,
Sorts: sorts,
Filters: filters,
}
fx.mwMock.
On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedListId,
}).
Return(&pb.RpcObjectShowResponse{
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
ObjectView: &model.ObjectView{
Details: []*model.ObjectViewDetailsSet{
{
Id: mockedListId,
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyType.String(): pbtypes.String(mockedTypeId),
bundle.RelationKeySetOf.String(): pbtypes.StringList([]string{mockedSetOfTypeId}),
},
},
},
{
Id: mockedTypeId,
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyRecommendedLayout.String(): pbtypes.Int64(int64(model.ObjectType_collection)),
},
},
},
},
Blocks: []*model.Block{
{
Id: "dataview",
Content: &model.BlockContentOfDataview{
Dataview: &model.BlockContentDataview{
Views: []*model.BlockContentDataviewView{view},
},
},
},
},
},
}, nil).Once()
fx.mwMock.
On("ObjectSearchSubscribe", mock.Anything, &pb.RpcObjectSearchSubscribeRequest{
SpaceId: mockedSpaceId,
Limit: int64(limit),
Offset: int64(offset),
Keys: []string{bundle.RelationKeyId.String()},
Sorts: sorts,
Filters: filters,
CollectionId: mockedListId,
}).
Return(&pb.RpcObjectSearchSubscribeResponse{
Error: &pb.RpcObjectSearchSubscribeResponseError{Code: pb.RpcObjectSearchSubscribeResponseError_NULL},
Counters: &pb.EventObjectSubscriptionCounters{Total: 1},
Records: []*types.Struct{
{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String("object-err"),
},
},
},
}, nil).Once()
// Simulate an error when trying to retrieve the object details.
fx.mwMock.
On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: "object-err",
}).
Return(&pb.RpcObjectShowResponse{
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NOT_FOUND},
}, nil).Once()
// when
_, _, _, err := fx.GetObjectsInList(ctx, mockedSpaceId, mockedListId, "", offset, limit)
// then
require.ErrorIs(t, err, object.ErrObjectNotFound)
})
}
func TestListService_AddObjectsToList(t *testing.T) {
t.Run("success", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
objectIds := []string{"obj-1", "obj-2"}
fx.mwMock.
On("ObjectCollectionAdd", mock.Anything, &pb.RpcObjectCollectionAddRequest{
ContextId: mockedListId,
ObjectIds: objectIds,
}).
Return(&pb.RpcObjectCollectionAddResponse{
Error: &pb.RpcObjectCollectionAddResponseError{Code: pb.RpcObjectCollectionAddResponseError_NULL},
}, nil).Once()
// when
err := fx.AddObjectsToList(ctx, mockedSpaceId, mockedListId, objectIds)
// then
require.NoError(t, err)
})
t.Run("failure", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
objectIds := []string{"obj-1"}
fx.mwMock.
On("ObjectCollectionAdd", mock.Anything, &pb.RpcObjectCollectionAddRequest{
ContextId: mockedListId,
ObjectIds: objectIds,
}).
Return(&pb.RpcObjectCollectionAddResponse{
Error: &pb.RpcObjectCollectionAddResponseError{Code: pb.RpcObjectCollectionAddResponseError_UNKNOWN_ERROR},
}, nil).Once()
// when
err := fx.AddObjectsToList(ctx, mockedSpaceId, mockedListId, objectIds)
// then
require.ErrorIs(t, err, ErrFailedAddObjectsToList)
})
}
func TestListService_RemoveObjectsFromList(t *testing.T) {
t.Run("success", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
objectIds := []string{"obj-1", "obj-2"}
fx.mwMock.
On("ObjectCollectionRemove", mock.Anything, &pb.RpcObjectCollectionRemoveRequest{
ContextId: mockedListId,
ObjectIds: objectIds,
}).
Return(&pb.RpcObjectCollectionRemoveResponse{
Error: &pb.RpcObjectCollectionRemoveResponseError{Code: pb.RpcObjectCollectionRemoveResponseError_NULL},
}, nil).Once()
// when
err := fx.RemoveObjectsFromList(ctx, mockedSpaceId, mockedListId, objectIds)
// then
require.NoError(t, err)
})
t.Run("failure", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
objectIds := []string{"obj-1"}
fx.mwMock.
On("ObjectCollectionRemove", mock.Anything, &pb.RpcObjectCollectionRemoveRequest{
ContextId: mockedListId,
ObjectIds: objectIds,
}).
Return(&pb.RpcObjectCollectionRemoveResponse{
Error: &pb.RpcObjectCollectionRemoveResponseError{Code: pb.RpcObjectCollectionRemoveResponseError_UNKNOWN_ERROR},
}, nil).Once()
// when
err := fx.RemoveObjectsFromList(ctx, mockedSpaceId, mockedListId, objectIds)
// then
require.ErrorIs(t, err, ErrFailedRemoveObjectsFromList)
})
}

View file

@ -11,17 +11,18 @@ import (
// GetObjectsHandler retrieves a list of objects in a space
//
// @Summary List objects
// @Tags objects
// @Accept json
// @Produce json
// @Param space_id path string true "Space ID"
// @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[Object] "List of objects"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /spaces/{space_id}/objects [get]
// @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.
// @Tags objects
// @Produce json
// @Param space_id path string true "Space ID"
// @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[Object] "List of objects"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/objects [get]
func GetObjectsHandler(s *ObjectService) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -47,17 +48,19 @@ func GetObjectsHandler(s *ObjectService) gin.HandlerFunc {
// GetObjectHandler retrieves an object in a space
//
// @Summary Get object
// @Tags objects
// @Accept json
// @Produce json
// @Param space_id path string true "Space ID"
// @Param object_id path string true "Object ID"
// @Success 200 {object} ObjectResponse "The requested object"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 404 {object} util.NotFoundError "Resource not found"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /spaces/{space_id}/objects/{object_id} [get]
// @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.
// @Tags objects
// @Produce json
// @Param space_id path string true "Space ID"
// @Param object_id path string true "Object ID"
// @Success 200 {object} ObjectResponse "The requested object"
// @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}/objects/{object_id} [get]
func GetObjectHandler(s *ObjectService) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -66,6 +69,7 @@ func GetObjectHandler(s *ObjectService) gin.HandlerFunc {
object, err := s.GetObject(c.Request.Context(), spaceId, objectId)
code := util.MapErrorCode(err,
util.ErrToCode(ErrObjectNotFound, http.StatusNotFound),
util.ErrToCode(ErrObjectDeleted, http.StatusGone),
util.ErrToCode(ErrFailedRetrieveObject, http.StatusInternalServerError),
)
@ -81,18 +85,20 @@ func GetObjectHandler(s *ObjectService) gin.HandlerFunc {
// DeleteObjectHandler deletes an object in a space
//
// @Summary Delete object
// @Tags objects
// @Accept json
// @Produce json
// @Param space_id path string true "Space ID"
// @Param object_id path string true "Object ID"
// @Success 200 {object} ObjectResponse "The deleted object"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 403 {object} util.ForbiddenError "Forbidden"
// @Failure 404 {object} util.NotFoundError "Resource not found"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /spaces/{space_id}/objects/{object_id} [delete]
// @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.
// @Tags objects
// @Produce json
// @Param space_id path string true "Space ID"
// @Param object_id path string true "Object ID"
// @Success 200 {object} ObjectResponse "The deleted object"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 403 {object} util.ForbiddenError "Forbidden"
// @Failure 404 {object} util.NotFoundError "Resource not found"
// @Failure 423 {object} util.RateLimitError "Rate limit exceeded"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/objects/{object_id} [delete]
func DeleteObjectHandler(s *ObjectService) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -117,17 +123,20 @@ func DeleteObjectHandler(s *ObjectService) gin.HandlerFunc {
// CreateObjectHandler creates a new object in a space
//
// @Summary Create object
// @Tags objects
// @Accept json
// @Produce json
// @Param space_id path string true "Space ID"
// @Param object body CreateObjectRequest true "Object to create"
// @Success 200 {object} ObjectResponse "The created object"
// @Failure 400 {object} util.ValidationError "Bad request"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /spaces/{space_id}/objects [post]
// @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.
// @Tags objects
// @Accept json
// @Produce json
// @Param space_id path string true "Space ID"
// @Param object body CreateObjectRequest true "Object to create"
// @Success 200 {object} ObjectResponse "The created object"
// @Failure 400 {object} util.ValidationError "Bad request"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 423 {object} util.RateLimitError "Rate limit exceeded"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/objects [post]
func CreateObjectHandler(s *ObjectService) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -142,9 +151,12 @@ func CreateObjectHandler(s *ObjectService) gin.HandlerFunc {
object, err := s.CreateObject(c.Request.Context(), spaceId, request)
code := util.MapErrorCode(err,
util.ErrToCode(ErrInputMissingSource, http.StatusBadRequest),
util.ErrToCode(ErrIconNameColorNotSupported, http.StatusBadRequest),
util.ErrToCode(ErrFailedCreateBookmark, http.StatusInternalServerError),
util.ErrToCode(ErrFailedCreateObject, http.StatusInternalServerError),
util.ErrToCode(ErrFailedSetRelationFeatured, http.StatusInternalServerError),
util.ErrToCode(ErrFailedFetchBookmark, http.StatusInternalServerError),
util.ErrToCode(ErrFailedSetPropertyFeatured, http.StatusInternalServerError),
util.ErrToCode(ErrFailedCreateBlock, http.StatusInternalServerError),
util.ErrToCode(ErrFailedPasteBody, http.StatusInternalServerError),
util.ErrToCode(ErrObjectNotFound, http.StatusInternalServerError),
util.ErrToCode(ErrFailedRetrieveObject, http.StatusInternalServerError),
)
@ -161,17 +173,18 @@ func CreateObjectHandler(s *ObjectService) gin.HandlerFunc {
// GetTypesHandler retrieves a list of types in a space
//
// @Summary List types
// @Tags types
// @Accept json
// @Produce json
// @Param space_id path string true "Space ID"
// @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[Type] "List of types"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /spaces/{space_id}/types [get]
// @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 a recommended layout. While a type's id is truly unique, a type's key can be the same across spaces for known types, e.g. 'ot-page' for 'Page'. Clients use this information when offering choices for object creation or for filtering objects by type through search.
// @Tags types
// @Produce json
// @Param space_id path string true "Space ID"
// @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[Type] "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]
func GetTypesHandler(s *ObjectService) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -195,17 +208,19 @@ func GetTypesHandler(s *ObjectService) gin.HandlerFunc {
// GetTypeHandler retrieves a type in a space
//
// @Summary Get type
// @Tags types
// @Accept json
// @Produce json
// @Param space_id path string true "Space ID"
// @Param type_id path string true "Type ID"
// @Success 200 {object} TypeResponse "The requested type"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 404 {object} util.NotFoundError "Resource not found"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /spaces/{space_id}/types/{type_id} [get]
// @Summary Get type
// @Description Fetches detailed information about one specific object type by its ID. This includes the types unique key, name, icon, and recommended 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).
// @Tags types
// @Produce json
// @Param space_id path string true "Space ID"
// @Param type_id path string true "Type ID"
// @Success 200 {object} 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]
func GetTypeHandler(s *ObjectService) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -214,6 +229,7 @@ func GetTypeHandler(s *ObjectService) gin.HandlerFunc {
object, err := s.GetType(c.Request.Context(), spaceId, typeId)
code := util.MapErrorCode(err,
util.ErrToCode(ErrTypeNotFound, http.StatusNotFound),
util.ErrToCode(ErrTypeDeleted, http.StatusGone),
util.ErrToCode(ErrFailedRetrieveType, http.StatusInternalServerError),
)
@ -229,18 +245,19 @@ func GetTypeHandler(s *ObjectService) gin.HandlerFunc {
// GetTemplatesHandler retrieves a list of templates for a type in a space
//
// @Summary List templates
// @Tags types
// @Accept json
// @Produce json
// @Param space_id path string true "Space ID"
// @Param type_id path string true "Type ID"
// @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[Template] "List of templates"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /spaces/{space_id}/types/{type_id}/templates [get]
// @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.
// @Tags templates
// @Produce json
// @Param space_id path string true "Space ID"
// @Param type_id path string true "Type ID"
// @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[Template] "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]
func GetTemplatesHandler(s *ObjectService) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -268,18 +285,20 @@ func GetTemplatesHandler(s *ObjectService) gin.HandlerFunc {
// GetTemplateHandler retrieves a template for a type in a space
//
// @Summary Get template
// @Tags types
// @Accept json
// @Produce json
// @Param space_id path string true "Space ID"
// @Param type_id path string true "Type ID"
// @Param template_id path string true "Template ID"
// @Success 200 {object} TemplateResponse "The requested template"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 404 {object} util.NotFoundError "Resource not found"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /spaces/{space_id}/types/{type_id}/templates/{template_id} [get]
// @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.
// @Tags templates
// @Produce json
// @Param space_id path string true "Space ID"
// @Param type_id path string true "Type ID"
// @Param template_id path string true "Template ID"
// @Success 200 {object} 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]
func GetTemplateHandler(s *ObjectService) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -289,6 +308,7 @@ func GetTemplateHandler(s *ObjectService) gin.HandlerFunc {
object, err := s.GetTemplate(c.Request.Context(), spaceId, typeId, templateId)
code := util.MapErrorCode(err,
util.ErrToCode(ErrTemplateNotFound, http.StatusNotFound),
util.ErrToCode(ErrTemplateDeleted, http.StatusGone),
util.ErrToCode(ErrFailedRetrieveTemplate, http.StatusInternalServerError),
)

View file

@ -1,93 +1,111 @@
package object
import "github.com/anyproto/anytype-heart/core/api/util"
type CreateObjectRequest struct {
Name string `json:"name" example:"Object Name"`
Icon string `json:"icon" example:"📄"`
Description string `json:"description" example:"Object Description"`
Body string `json:"body" example:"Object Body"`
Source string `json:"source" example:"https://source.com"`
TemplateId string `json:"template_id" example:"bafyreictrp3obmnf6dwejy5o4p7bderaaia4bdg2psxbfzf44yya5uutge"`
ObjectTypeUniqueKey string `json:"object_type_unique_key" example:"ot-page"`
Name string `json:"name" example:"My object"` // The name of the object
Icon util.Icon `json:"icon"` // The icon of the object
Description string `json:"description" example:"This is a description of the object."` // The description of the object
Body string `json:"body" example:"This is the body of the object. Markdown syntax is supported here."` // The body of the object
Source string `json:"source" example:"https://bookmark-source.com"` // The source url, only applicable for bookmarks
TemplateId string `json:"template_id" example:"bafyreictrp3obmnf6dwejy5o4p7bderaaia4bdg2psxbfzf44yya5uutge"` // The id of the template to use
TypeKey string `json:"type_key" example:"ot-page"` // The key of the type of object to create
}
type ObjectResponse struct {
Object Object `json:"object"`
Object Object `json:"object"` // The object
}
type Object struct {
Type string `json:"type" example:"Page"`
Id string `json:"id" example:"bafyreie6n5l5nkbjal37su54cha4coy7qzuhrnajluzv5qd5jvtsrxkequ"`
Name string `json:"name" example:"Object Name"`
Icon string `json:"icon" example:"📄"`
Snippet string `json:"snippet" example:"The beginning of the object body..."`
Layout string `json:"layout" example:"basic"`
SpaceId string `json:"space_id" example:"bafyreigyfkt6rbv24sbv5aq2hko3bhmv5xxlf22b4bypdu6j7hnphm3psq.23me69r569oi1"`
RootId string `json:"root_id" example:"bafyreicypzj6uvu54664ucv3hmbsd5cmdy2dv4fwua26sciq74khzpyn4u"`
Blocks []Block `json:"blocks"`
Details []Detail `json:"details"`
Object string `json:"object" example:"object"` // The data model of the object
Id string `json:"id" example:"bafyreie6n5l5nkbjal37su54cha4coy7qzuhrnajluzv5qd5jvtsrxkequ"` // The id of the object
Name string `json:"name" example:"My object"` // The name of the object
Icon util.Icon `json:"icon"` // The icon of the object
Archived bool `json:"archived" example:"false"` // Whether the object is archived
SpaceId string `json:"space_id" example:"bafyreigyfkt6rbv24sbv5aq2hko3bhmv5xxlf22b4bypdu6j7hnphm3psq.23me69r569oi1"` // The id of the space the object is in
Snippet string `json:"snippet" example:"The beginning of the object body..."` // The snippet of the object, especially important for notes as they don't have a name
Layout string `json:"layout" example:"basic"` // The layout of the object
Type Type `json:"type"` // The type of the object
Blocks []Block `json:"blocks"` // The blocks of the object. Omitted in Search endpoints, returned only in GetObject endpoint
Properties []Property `json:"properties"` // The properties of the object
}
type Block struct {
Id string `json:"id" example:"64394517de52ad5acb89c66c"`
ChildrenIds []string `json:"children_ids" example:"['6797ce8ecda913cde14b02dc']"`
BackgroundColor string `json:"background_color" example:"red"`
Align string `json:"align" enums:"AlignLeft,AlignCenter,AlignRight,AlignJustify" example:"AlignLeft"`
VerticalAlign string `json:"vertical_align" enums:"VerticalAlignTop,VerticalAlignMiddle,VerticalAlignBottom" example:"VerticalAlignTop"`
Text *Text `json:"text,omitempty"`
File *File `json:"file,omitempty"`
Id string `json:"id" example:"64394517de52ad5acb89c66c"` // The id of the block
ChildrenIds []string `json:"children_ids" example:"['6797ce8ecda913cde14b02dc']"` // The ids of the block's children
BackgroundColor string `json:"background_color" example:"red"` // The background color of the block
Align string `json:"align" enums:"AlignLeft,AlignCenter,AlignRight,AlignJustify" example:"AlignLeft"` // The alignment of the block
VerticalAlign string `json:"vertical_align" enums:"VerticalAlignTop,VerticalAlignMiddle,VerticalAlignBottom" example:"VerticalAlignTop"` // The vertical alignment of the block
Text *Text `json:"text,omitempty"` // The text of the block, if applicable
File *File `json:"file,omitempty"` // The file of the block, if applicable
Property *Property `json:"property,omitempty"` // The property block, if applicable
}
type Text struct {
Text string `json:"text" example:"Some text"`
Style string `json:"style" enums:"Paragraph,Header1,Header2,Header3,Header4,Quote,Code,Title,Checkbox,Marked,Numbered,Toggle,Description,Callout" example:"Paragraph"`
Checked bool `json:"checked" example:"true"`
Color string `json:"color" example:"red"`
Icon string `json:"icon" example:"📄"`
Text string `json:"text" example:"Some text..."` // The text
Style string `json:"style" enums:"Paragraph,Header1,Header2,Header3,Header4,Quote,Code,Title,Checkbox,Marked,Numbered,Toggle,Description,Callout" example:"Paragraph"` // The style of the text
Checked bool `json:"checked" example:"true"` // Whether the text is checked
Color string `json:"color" example:"red"` // The color of the text
Icon util.Icon `json:"icon"` // The icon of the text
}
type File struct {
Hash string `json:"hash"`
Name string `json:"name"`
Type string `json:"type"`
Mime string `json:"mime"`
Size int `json:"size"`
AddedAt int `json:"added_at"`
TargetObjectId string `json:"target_object_id"`
State string `json:"state"`
Style string `json:"style"`
Hash string `json:"hash"` // The hash of the file
Name string `json:"name"` // The name of the file
Type string `json:"type"` // The type of the file
Mime string `json:"mime"` // The mime of the file
Size int `json:"size"` // The size of the file
AddedAt int `json:"added_at"` // The added at of the file
TargetObjectId string `json:"target_object_id"` // The target object id of the file
State string `json:"state"` // The state of the file
Style string `json:"style"` // The style of the file
}
type Detail struct {
Id string `json:"id" enums:"last_modified_date,last_modified_by,created_date,created_by,last_opened_date,tags" example:"last_modified_date"`
Details map[string]interface{} `json:"details"`
type Property struct {
Id string `json:"id" example:"last_modified_date"` // The id of the property
Name string `json:"name" example:"Last modified date"` // The name of the property
Format string `json:"format" example:"date" enums:"text,number,select,multi_select,date,file,checkbox,url,email,phone,object"` // The format of the property
Text *string `json:"text,omitempty" example:"Some text..."` // The text value, if applicable
Number *float64 `json:"number,omitempty" example:"42"` // The number value, if applicable
Select *Tag `json:"select,omitempty"` // The select value, if applicable
MultiSelect []Tag `json:"multi_select,omitempty"` // The multi-select values, if applicable
Date *string `json:"date,omitempty" example:"2025-02-14T12:34:56Z"` // The date value, if applicable
File []string `json:"file,omitempty" example:"['fileId']"` // The file references, if applicable
Checkbox *bool `json:"checkbox,omitempty" example:"true" enum:"true,false"` // The checkbox value, if applicable
Url *string `json:"url,omitempty" example:"https://example.com"` // The url value, if applicable
Email *string `json:"email,omitempty" example:"example@example.com"` // The email value, if applicable
Phone *string `json:"phone,omitempty" example:"+1234567890"` // The phone number value, if applicable
Object []string `json:"object,omitempty" example:"['objectId']"` // The object references, if applicable
}
type Tag struct {
Id string `json:"id" example:"bafyreiaixlnaefu3ci22zdenjhsdlyaeeoyjrsid5qhfeejzlccijbj7sq"`
Name string `json:"name" example:"Tag Name"`
Color string `json:"color" example:"yellow"`
Id string `json:"id" example:"bafyreiaixlnaefu3ci22zdenjhsdlyaeeoyjrsid5qhfeejzlccijbj7sq"` // The id of the tag
Name string `json:"name" example:"in-progress"` // The name of the tag
Color string `json:"color" example:"yellow"` // The color of the tag
}
type TypeResponse struct {
Type Type `json:"type"`
Type Type `json:"type"` // The type
}
type Type struct {
Type string `json:"type" example:"type"`
Id string `json:"id" example:"bafyreigyb6l5szohs32ts26ku2j42yd65e6hqy2u3gtzgdwqv6hzftsetu"`
UniqueKey string `json:"unique_key" example:"ot-page"`
Name string `json:"name" example:"Page"`
Icon string `json:"icon" example:"📄"`
RecommendedLayout string `json:"recommended_layout" example:"todo"`
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:"ot-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
Icon util.Icon `json:"icon"` // The icon of the type
Archived bool `json:"archived" example:"false"` // Whether the type is archived
RecommendedLayout string `json:"recommended_layout" example:"todo"` // The recommended layout of the type
}
type TemplateResponse struct {
Template Template `json:"template"`
Template Template `json:"template"` // The template
}
type Template struct {
Type string `json:"type" example:"template"`
Id string `json:"id" example:"bafyreictrp3obmnf6dwejy5o4p7bderaaia4bdg2psxbfzf44yya5uutge"`
Name string `json:"name" example:"Template Name"`
Icon string `json:"icon" example:"📄"`
Object string `json:"object" example:"template"` // The data model of the object
Id string `json:"id" example:"bafyreictrp3obmnf6dwejy5o4p7bderaaia4bdg2psxbfzf44yya5uutge"` // The id of the template
Name string `json:"name" example:"My template"` // The name of the template
Icon util.Icon `json:"icon"` // The icon of the template
Archived bool `json:"archived" example:"false"` // Whether the template is archived
}

View file

@ -6,12 +6,14 @@ import (
"time"
"github.com/gogo/protobuf/types"
"github.com/iancoleman/strcase"
"github.com/anyproto/anytype-heart/core/api/apicore"
"github.com/anyproto/anytype-heart/core/api/internal/space"
"github.com/anyproto/anytype-heart/core/api/pagination"
"github.com/anyproto/anytype-heart/core/api/util"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pb/service"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/pbtypes"
@ -20,26 +22,75 @@ import (
var (
// objects
ErrObjectNotFound = errors.New("object not found")
ErrObjectDeleted = errors.New("object deleted")
ErrFailedRetrieveObject = errors.New("failed to retrieve object")
ErrFailedRetrieveObjects = errors.New("failed to retrieve list of objects")
ErrFailedDeleteObject = errors.New("failed to delete object")
ErrFailedCreateObject = errors.New("failed to create object")
ErrInputMissingSource = errors.New("source is missing for bookmark")
ErrFailedSetRelationFeatured = errors.New("failed to set relation featured")
ErrFailedFetchBookmark = errors.New("failed to fetch bookmark")
ErrIconNameColorNotSupported = errors.New("icon name and color are not supported for object")
ErrFailedSetPropertyFeatured = errors.New("failed to set property featured")
ErrFailedCreateBookmark = errors.New("failed to fetch bookmark")
ErrFailedCreateBlock = errors.New("failed to create block")
ErrFailedPasteBody = errors.New("failed to paste body")
// types
ErrFailedRetrieveTypes = errors.New("failed to retrieve types")
ErrTypeNotFound = errors.New("type not found")
ErrTypeDeleted = errors.New("type deleted")
ErrFailedRetrieveType = errors.New("failed to retrieve type")
ErrFailedRetrieveTemplateType = errors.New("failed to retrieve template type")
ErrTemplateTypeNotFound = errors.New("template type not found")
ErrFailedRetrieveTemplate = errors.New("failed to retrieve template")
ErrFailedRetrieveTemplates = errors.New("failed to retrieve templates")
ErrTemplateNotFound = errors.New("template not found")
ErrTemplateDeleted = errors.New("template deleted")
)
var excludedSystemProperties = map[string]bool{
bundle.RelationKeyId.String(): true,
bundle.RelationKeySpaceId.String(): true,
bundle.RelationKeyName.String(): true,
bundle.RelationKeyIconEmoji.String(): true,
bundle.RelationKeyIconImage.String(): true,
bundle.RelationKeyType.String(): true,
bundle.RelationKeyResolvedLayout.String(): true,
bundle.RelationKeyIsFavorite.String(): true,
bundle.RelationKeyIsArchived.String(): true,
bundle.RelationKeyIsDeleted.String(): true,
bundle.RelationKeyIsHidden.String(): true,
bundle.RelationKeyWorkspaceId.String(): true,
bundle.RelationKeyInternalFlags.String(): true,
bundle.RelationKeyRestrictions.String(): true,
bundle.RelationKeyOrigin.String(): true,
bundle.RelationKeySnippet.String(): true,
bundle.RelationKeySyncStatus.String(): true,
bundle.RelationKeySyncError.String(): true,
bundle.RelationKeySyncDate.String(): true,
bundle.RelationKeyCoverId.String(): true,
bundle.RelationKeyCoverType.String(): true,
bundle.RelationKeyCoverScale.String(): true,
bundle.RelationKeyCoverX.String(): true,
bundle.RelationKeyCoverY.String(): true,
bundle.RelationKeyMentions.String(): true,
bundle.RelationKeyOldAnytypeID.String(): true,
bundle.RelationKeySource.String(): true,
bundle.RelationKeySourceFilePath.String(): true,
bundle.RelationKeyImportType.String(): true,
bundle.RelationKeyTargetObjectType.String(): true,
bundle.RelationKeyFeaturedRelations.String(): true,
bundle.RelationKeySetOf.String(): true,
bundle.RelationKeyLinks.String(): true,
bundle.RelationKeyBacklinks.String(): true,
bundle.RelationKeySourceObject.String(): true,
bundle.RelationKeyLayoutAlign.String(): true,
bundle.RelationKeyIsHiddenDiscovery.String(): true,
bundle.RelationKeyLayout.String(): true,
bundle.RelationKeyIsReadonly.String(): true,
bundle.RelationKeyParticipantStatus.String(): true,
bundle.RelationKeyParticipantPermissions.String(): true,
}
type Service interface {
ListObjects(ctx context.Context, spaceId string, offset int, limit int) ([]Object, int, bool, error)
GetObject(ctx context.Context, spaceId string, objectId string) (Object, error)
@ -52,12 +103,12 @@ type Service interface {
}
type ObjectService struct {
mw service.ClientCommandsServer
mw apicore.ClientCommands
spaceService *space.SpaceService
AccountInfo *model.AccountInfo
}
func NewService(mw service.ClientCommandsServer, spaceService *space.SpaceService) *ObjectService {
func NewService(mw apicore.ClientCommands, spaceService *space.SpaceService) *ObjectService {
return &ObjectService{mw: mw, spaceService: spaceService}
}
@ -67,7 +118,8 @@ func (s *ObjectService) ListObjects(ctx context.Context, spaceId string, offset
SpaceId: spaceId,
Filters: []*model.BlockContentDataviewFilter{
{
RelationKey: bundle.RelationKeyLayout.String(),
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyResolvedLayout.String(),
Condition: model.BlockContentDataviewFilter_In,
Value: pbtypes.IntList([]int{
int(model.ObjectType_basic),
@ -80,6 +132,18 @@ func (s *ObjectService) ListObjects(ctx context.Context, spaceId string, offset
int(model.ObjectType_participant),
}...),
},
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: "type.uniqueKey",
Condition: model.BlockContentDataviewFilter_NotEqual,
Value: pbtypes.String("ot-template"),
},
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyIsHidden.String(),
Condition: model.BlockContentDataviewFilter_NotEqual,
Value: pbtypes.Bool(true),
},
},
Sorts: []*model.BlockContentDataviewSort{{
RelationKey: bundle.RelationKeyLastModifiedDate.String(),
@ -117,31 +181,35 @@ func (s *ObjectService) GetObject(ctx context.Context, spaceId string, objectId
ObjectId: objectId,
})
if resp.Error.Code == pb.RpcObjectShowResponseError_NOT_FOUND {
return Object{}, ErrObjectNotFound
if resp.Error != nil {
if resp.Error.Code == pb.RpcObjectShowResponseError_NOT_FOUND {
return Object{}, ErrObjectNotFound
}
if resp.Error.Code == pb.RpcObjectShowResponseError_OBJECT_DELETED {
return Object{}, ErrObjectDeleted
}
if resp.Error.Code != pb.RpcObjectShowResponseError_NULL {
return Object{}, ErrFailedRetrieveObject
}
}
if resp.Error.Code != pb.RpcObjectShowResponseError_NULL {
return Object{}, ErrFailedRetrieveObject
}
icon := util.GetIconFromEmojiOrImage(s.AccountInfo, resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(), resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyIconImage.String()].GetStringValue())
objectTypeName, err := util.ResolveTypeToName(s.mw, spaceId, resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyType.String()].GetStringValue())
if err != nil {
return Object{}, err
}
details := resp.ObjectView.Details[0].Details.Fields
icon := util.GetIcon(s.AccountInfo, details[bundle.RelationKeyIconEmoji.String()].GetStringValue(), details[bundle.RelationKeyIconImage.String()].GetStringValue(), details[bundle.RelationKeyIconName.String()].GetStringValue(), details[bundle.RelationKeyIconOption.String()].GetNumberValue())
object := Object{
Type: objectTypeName,
Id: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyId.String()].GetStringValue(),
Name: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyName.String()].GetStringValue(),
Icon: icon,
Snippet: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeySnippet.String()].GetStringValue(),
Layout: model.ObjectTypeLayout_name[int32(resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyLayout.String()].GetNumberValue())],
SpaceId: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeySpaceId.String()].GetStringValue(),
RootId: resp.ObjectView.RootId,
Blocks: s.GetBlocks(resp),
Details: s.GetDetails(resp),
Object: "object",
Id: details[bundle.RelationKeyId.String()].GetStringValue(),
Name: details[bundle.RelationKeyName.String()].GetStringValue(),
Icon: icon,
Archived: details[bundle.RelationKeyIsArchived.String()].GetBoolValue(),
SpaceId: details[bundle.RelationKeySpaceId.String()].GetStringValue(),
Snippet: details[bundle.RelationKeySnippet.String()].GetStringValue(),
Layout: model.ObjectTypeLayout_name[int32(details[bundle.RelationKeyResolvedLayout.String()].GetNumberValue())],
Type: s.GetTypeFromDetails(details[bundle.RelationKeyType.String()].GetStringValue(), resp.ObjectView.Details),
Blocks: s.getBlocks(resp),
Properties: s.getProperties(resp),
}
return object, nil
@ -168,62 +236,54 @@ func (s *ObjectService) DeleteObject(ctx context.Context, spaceId string, object
// CreateObject creates a new object in a specific space.
func (s *ObjectService) CreateObject(ctx context.Context, spaceId string, request CreateObjectRequest) (Object, error) {
if request.ObjectTypeUniqueKey == "ot-bookmark" && request.Source == "" {
return Object{}, ErrInputMissingSource
details, err := s.buildObjectDetails(request)
if err != nil {
return Object{}, err
}
details := &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String(request.Name),
bundle.RelationKeyIconEmoji.String(): pbtypes.String(request.Icon),
bundle.RelationKeyDescription.String(): pbtypes.String(request.Description),
bundle.RelationKeySource.String(): pbtypes.String(request.Source),
bundle.RelationKeyOrigin.String(): pbtypes.Int64(int64(model.ObjectOrigin_api)),
},
}
var objectId string
if request.TypeKey == "ot-bookmark" {
resp := s.mw.ObjectCreateBookmark(ctx, &pb.RpcObjectCreateBookmarkRequest{
Details: details,
SpaceId: spaceId,
TemplateId: request.TemplateId,
})
resp := s.mw.ObjectCreate(ctx, &pb.RpcObjectCreateRequest{
Details: details,
TemplateId: request.TemplateId,
SpaceId: spaceId,
ObjectTypeUniqueKey: request.ObjectTypeUniqueKey,
WithChat: false,
})
if resp.Error.Code != pb.RpcObjectCreateBookmarkResponseError_NULL {
return Object{}, ErrFailedCreateBookmark
}
objectId = resp.ObjectId
} else {
resp := s.mw.ObjectCreate(ctx, &pb.RpcObjectCreateRequest{
Details: details,
TemplateId: request.TemplateId,
SpaceId: spaceId,
ObjectTypeUniqueKey: request.TypeKey,
})
if resp.Error.Code != pb.RpcObjectCreateResponseError_NULL {
return Object{}, ErrFailedCreateObject
if resp.Error.Code != pb.RpcObjectCreateResponseError_NULL {
return Object{}, ErrFailedCreateObject
}
objectId = resp.ObjectId
}
// ObjectRelationAddFeatured if description was set
if request.Description != "" {
relAddFeatResp := s.mw.ObjectRelationAddFeatured(ctx, &pb.RpcObjectRelationAddFeaturedRequest{
ContextId: resp.ObjectId,
ContextId: objectId,
Relations: []string{bundle.RelationKeyDescription.String()},
})
if relAddFeatResp.Error.Code != pb.RpcObjectRelationAddFeaturedResponseError_NULL {
object, _ := s.GetObject(ctx, spaceId, resp.ObjectId) // nolint:errcheck
return object, ErrFailedSetRelationFeatured
}
}
// ObjectBookmarkFetch after creating a bookmark object
if request.ObjectTypeUniqueKey == "ot-bookmark" {
bookmarkResp := s.mw.ObjectBookmarkFetch(ctx, &pb.RpcObjectBookmarkFetchRequest{
ContextId: resp.ObjectId,
Url: request.Source,
})
if bookmarkResp.Error.Code != pb.RpcObjectBookmarkFetchResponseError_NULL {
object, _ := s.GetObject(ctx, spaceId, resp.ObjectId) // nolint:errcheck
return object, ErrFailedFetchBookmark
object, _ := s.GetObject(ctx, spaceId, objectId) // nolint:errcheck
return object, ErrFailedSetPropertyFeatured
}
}
// First call BlockCreate at top, then BlockPaste to paste the body
if request.Body != "" {
blockCreateResp := s.mw.BlockCreate(ctx, &pb.RpcBlockCreateRequest{
ContextId: resp.ObjectId,
ContextId: objectId,
TargetId: "",
Block: &model.Block{
Id: "",
@ -245,23 +305,55 @@ func (s *ObjectService) CreateObject(ctx context.Context, spaceId string, reques
})
if blockCreateResp.Error.Code != pb.RpcBlockCreateResponseError_NULL {
object, _ := s.GetObject(ctx, spaceId, resp.ObjectId) // nolint:errcheck
return object, ErrFailedCreateObject
object, _ := s.GetObject(ctx, spaceId, objectId) // nolint:errcheck
return object, ErrFailedCreateBlock
}
blockPasteResp := s.mw.BlockPaste(ctx, &pb.RpcBlockPasteRequest{
ContextId: resp.ObjectId,
ContextId: objectId,
FocusedBlockId: blockCreateResp.BlockId,
TextSlot: request.Body,
})
if blockPasteResp.Error.Code != pb.RpcBlockPasteResponseError_NULL {
object, _ := s.GetObject(ctx, spaceId, resp.ObjectId) // nolint:errcheck
object, _ := s.GetObject(ctx, spaceId, objectId) // nolint:errcheck
return object, ErrFailedPasteBody
}
}
return s.GetObject(ctx, spaceId, resp.ObjectId)
return s.GetObject(ctx, spaceId, objectId)
}
// buildObjectDetails extracts the details structure from the CreateObjectRequest.
func (s *ObjectService) buildObjectDetails(request CreateObjectRequest) (*types.Struct, error) {
// Validate bookmark source
if request.TypeKey == "ot-bookmark" && request.Source == "" {
return nil, ErrInputMissingSource
}
// Validate icon: only allow either emoji or file, and disallow name and color fields.
if request.Icon.Name != nil || request.Icon.Color != nil {
return nil, ErrIconNameColorNotSupported
}
iconFields := map[string]*types.Value{}
if request.Icon.Emoji != nil {
iconFields[bundle.RelationKeyIconEmoji.String()] = pbtypes.String(*request.Icon.Emoji)
} else if request.Icon.File != nil {
iconFields[bundle.RelationKeyIconImage.String()] = pbtypes.String(*request.Icon.File)
}
fields := map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String(request.Name),
bundle.RelationKeyDescription.String(): pbtypes.String(request.Description),
bundle.RelationKeySource.String(): pbtypes.String(request.Source),
bundle.RelationKeyOrigin.String(): pbtypes.Int64(int64(model.ObjectOrigin_api)),
}
for k, v := range iconFields {
fields[k] = v
}
return &types.Struct{Fields: fields}, nil
}
// ListTypes returns a paginated list of types in a specific space.
@ -270,11 +362,13 @@ func (s *ObjectService) ListTypes(ctx context.Context, spaceId string, offset in
SpaceId: spaceId,
Filters: []*model.BlockContentDataviewFilter{
{
RelationKey: bundle.RelationKeyLayout.String(),
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyResolvedLayout.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.Int64(int64(model.ObjectType_objectType)),
},
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyIsHidden.String(),
Condition: model.BlockContentDataviewFilter_NotEqual,
Value: pbtypes.Bool(true),
@ -286,7 +380,7 @@ func (s *ObjectService) ListTypes(ctx context.Context, spaceId string, offset in
Type: model.BlockContentDataviewSort_Asc,
},
},
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeyUniqueKey.String(), bundle.RelationKeyName.String(), bundle.RelationKeyIconEmoji.String(), bundle.RelationKeyRecommendedLayout.String()},
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeyUniqueKey.String(), bundle.RelationKeyName.String(), bundle.RelationKeyIconEmoji.String(), bundle.RelationKeyIconName.String(), bundle.RelationKeyIconOption.String(), bundle.RelationKeyRecommendedLayout.String(), bundle.RelationKeyIsArchived.String()},
})
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
@ -299,11 +393,12 @@ func (s *ObjectService) ListTypes(ctx context.Context, spaceId string, offset in
for _, record := range paginatedTypes {
types = append(types, Type{
Type: "type",
Object: "type",
Id: record.Fields[bundle.RelationKeyId.String()].GetStringValue(),
UniqueKey: record.Fields[bundle.RelationKeyUniqueKey.String()].GetStringValue(),
Key: record.Fields[bundle.RelationKeyUniqueKey.String()].GetStringValue(),
Name: record.Fields[bundle.RelationKeyName.String()].GetStringValue(),
Icon: record.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(),
Icon: util.GetIcon(s.AccountInfo, record.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(), "", record.Fields[bundle.RelationKeyIconName.String()].GetStringValue(), record.Fields[bundle.RelationKeyIconOption.String()].GetNumberValue()),
Archived: record.Fields[bundle.RelationKeyIsArchived.String()].GetBoolValue(),
RecommendedLayout: model.ObjectTypeLayout_name[int32(record.Fields[bundle.RelationKeyRecommendedLayout.String()].GetNumberValue())],
})
}
@ -317,21 +412,29 @@ func (s *ObjectService) GetType(ctx context.Context, spaceId string, typeId stri
ObjectId: typeId,
})
if resp.Error.Code == pb.RpcObjectShowResponseError_NOT_FOUND {
return Type{}, ErrTypeNotFound
}
if resp.Error.Code != pb.RpcObjectShowResponseError_NULL {
return Type{}, ErrFailedRetrieveType
if resp.Error != nil {
if resp.Error.Code == pb.RpcObjectShowResponseError_NOT_FOUND {
return Type{}, ErrTypeNotFound
}
if resp.Error.Code == pb.RpcObjectShowResponseError_OBJECT_DELETED {
return Type{}, ErrTypeDeleted
}
if resp.Error.Code != pb.RpcObjectShowResponseError_NULL {
return Type{}, ErrFailedRetrieveType
}
}
details := resp.ObjectView.Details[0].Details.Fields
return Type{
Type: "type",
Object: "type",
Id: typeId,
UniqueKey: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyUniqueKey.String()].GetStringValue(),
Name: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyName.String()].GetStringValue(),
Icon: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(),
RecommendedLayout: model.ObjectTypeLayout_name[int32(resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyRecommendedLayout.String()].GetNumberValue())],
Key: details[bundle.RelationKeyUniqueKey.String()].GetStringValue(),
Name: details[bundle.RelationKeyName.String()].GetStringValue(),
Icon: util.GetIcon(s.AccountInfo, details[bundle.RelationKeyIconEmoji.String()].GetStringValue(), "", details[bundle.RelationKeyIconName.String()].GetStringValue(), details[bundle.RelationKeyIconOption.String()].GetNumberValue()),
Archived: details[bundle.RelationKeyIsArchived.String()].GetBoolValue(),
RecommendedLayout: model.ObjectTypeLayout_name[int32(details[bundle.RelationKeyRecommendedLayout.String()].GetNumberValue())],
}, nil
}
@ -342,6 +445,7 @@ func (s *ObjectService) ListTemplates(ctx context.Context, spaceId string, typeI
SpaceId: spaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyUniqueKey.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String("ot-template"),
@ -364,12 +468,13 @@ func (s *ObjectService) ListTemplates(ctx context.Context, spaceId string, typeI
SpaceId: spaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyType.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(templateTypeId),
},
},
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeyTargetObjectType.String(), bundle.RelationKeyName.String(), bundle.RelationKeyIconEmoji.String()},
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeyTargetObjectType.String(), bundle.RelationKeyName.String(), bundle.RelationKeyIconEmoji.String(), bundle.RelationKeyIsArchived.String()},
})
if templateObjectsResp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
@ -399,10 +504,11 @@ func (s *ObjectService) ListTemplates(ctx context.Context, spaceId string, typeI
}
templates = append(templates, Template{
Type: "template",
Id: templateId,
Name: templateResp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyName.String()].GetStringValue(),
Icon: templateResp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(),
Object: "template",
Id: templateId,
Name: templateResp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyName.String()].GetStringValue(),
Icon: util.GetIcon(s.AccountInfo, templateResp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(), "", "", 0),
Archived: templateResp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyIsArchived.String()].GetBoolValue(),
})
}
@ -416,89 +522,299 @@ func (s *ObjectService) GetTemplate(ctx context.Context, spaceId string, typeId
ObjectId: templateId,
})
if resp.Error.Code == pb.RpcObjectShowResponseError_NOT_FOUND {
return Template{}, ErrTemplateNotFound
}
if resp.Error != nil {
if resp.Error.Code == pb.RpcObjectShowResponseError_NOT_FOUND {
return Template{}, ErrTemplateNotFound
}
if resp.Error.Code != pb.RpcObjectShowResponseError_NULL {
return Template{}, ErrFailedRetrieveTemplate
if resp.Error.Code == pb.RpcObjectShowResponseError_OBJECT_DELETED {
return Template{}, ErrTemplateDeleted
}
if resp.Error.Code != pb.RpcObjectShowResponseError_NULL {
return Template{}, ErrFailedRetrieveTemplate
}
}
return Template{
Type: "template",
Id: templateId,
Name: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyName.String()].GetStringValue(),
Icon: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(),
Object: "template",
Id: templateId,
Name: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyName.String()].GetStringValue(),
Icon: util.GetIcon(s.AccountInfo, resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(), "", "", 0),
Archived: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyIsArchived.String()].GetBoolValue(),
}, nil
}
// GetDetails returns the list of details from the ObjectShowResponse.
func (s *ObjectService) GetDetails(resp *pb.RpcObjectShowResponse) []Detail {
creator := resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyCreator.String()].GetStringValue()
lastModifiedBy := resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyLastModifiedBy.String()].GetStringValue()
var creatorId, lastModifiedById string
for _, detail := range resp.ObjectView.Details {
if detail.Id == creator {
creatorId = detail.Id
}
if detail.Id == lastModifiedBy {
lastModifiedById = detail.Id
// GetTypeFromDetails returns the type from the details of the ObjectShowResponse.
func (s *ObjectService) GetTypeFromDetails(typeId string, details []*model.ObjectViewDetailsSet) Type {
var objectTypeDetail *types.Struct
for _, detail := range details {
if detail.Id == typeId {
objectTypeDetail = detail.GetDetails()
break
}
}
return []Detail{
{
Id: "last_modified_date",
Details: map[string]interface{}{
"last_modified_date": PosixToISO8601(resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyLastModifiedDate.String()].GetNumberValue()),
},
},
{
Id: "last_modified_by",
Details: map[string]interface{}{
"details": s.spaceService.GetParticipantDetails(s.mw, resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeySpaceId.String()].GetStringValue(), lastModifiedById),
},
},
{
Id: "created_date",
Details: map[string]interface{}{
"created_date": PosixToISO8601(resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyCreatedDate.String()].GetNumberValue()),
},
},
{
Id: "created_by",
Details: map[string]interface{}{
"details": s.spaceService.GetParticipantDetails(s.mw, resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeySpaceId.String()].GetStringValue(), creatorId),
},
},
{
Id: "last_opened_date",
Details: map[string]interface{}{
"last_opened_date": PosixToISO8601(resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyLastOpenedDate.String()].GetNumberValue()),
},
},
{
Id: "tags",
Details: map[string]interface{}{
"tags": s.getTags(resp),
},
},
if objectTypeDetail == nil {
return Type{}
}
return Type{
Object: "type",
Id: typeId,
Key: objectTypeDetail.Fields[bundle.RelationKeyUniqueKey.String()].GetStringValue(),
Name: objectTypeDetail.Fields[bundle.RelationKeyName.String()].GetStringValue(),
Icon: util.GetIcon(s.AccountInfo, objectTypeDetail.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(), "", objectTypeDetail.Fields[bundle.RelationKeyIconName.String()].GetStringValue(), objectTypeDetail.Fields[bundle.RelationKeyIconOption.String()].GetNumberValue()),
RecommendedLayout: model.ObjectTypeLayout_name[int32(objectTypeDetail.Fields[bundle.RelationKeyRecommendedLayout.String()].GetNumberValue())],
}
}
// getProperties returns a list of properties by iterating over all properties found in the RelationLinks and mapping their format and value.
func (s *ObjectService) getProperties(resp *pb.RpcObjectShowResponse) []Property {
propertyFormatMap := s.getPropertyFormatMap(resp.ObjectView.RelationLinks)
linkedProperties := resp.ObjectView.RelationLinks
primaryDetailFields := resp.ObjectView.Details[0].Details.Fields
properties := make([]Property, 0, len(linkedProperties))
for _, r := range linkedProperties {
key := r.Key
if _, isExcluded := excludedSystemProperties[key]; isExcluded {
continue
}
if _, ok := primaryDetailFields[key]; !ok {
continue
}
id, name := s.getProperty(key, resp)
format := propertyFormatMap[key]
convertedVal := s.convertValue(key, primaryDetailFields[key], format, resp.ObjectView.Details)
if s.isMissingObject(convertedVal) {
continue
}
properties = append(properties, s.buildProperty(id, name, format, convertedVal))
}
return properties
}
// isMissingObject returns true if val indicates a "_missing_object" placeholder.
func (s *ObjectService) isMissingObject(val interface{}) bool {
switch v := val.(type) {
case string:
return v == "_missing_object"
case []interface{}:
if len(v) == 1 {
if str, ok := v[0].(string); ok {
return str == "_missing_object"
}
}
case Tag:
return v.Id == "_missing_object"
case []Tag:
if len(v) == 1 && v[0].Id == "_missing_object" {
return true
}
}
return false
}
// buildProperty creates a Property based on the format and converted value.
func (s *ObjectService) buildProperty(id string, name string, format string, val interface{}) Property {
prop := &Property{
Id: id,
Name: name,
Format: format,
}
switch format {
case "text":
if str, ok := val.(string); ok {
prop.Text = &str
}
case "number":
if num, ok := val.(float64); ok {
prop.Number = &num
}
case "select":
if sel, ok := val.(Tag); ok {
prop.Select = &sel
}
case "multi_select":
if ms, ok := val.([]Tag); ok {
prop.MultiSelect = ms
}
case "date":
if dateStr, ok := val.(string); ok {
prop.Date = &dateStr
}
case "file":
if fileList, ok := val.([]interface{}); ok {
var files []string
for _, v := range fileList {
if str, ok := v.(string); ok {
files = append(files, str)
}
}
prop.File = files
}
case "checkbox":
if cb, ok := val.(bool); ok {
prop.Checkbox = &cb
}
case "url":
if urlStr, ok := val.(string); ok {
prop.Url = &urlStr
}
case "email":
if email, ok := val.(string); ok {
prop.Email = &email
}
case "phone":
if phone, ok := val.(string); ok {
prop.Phone = &phone
}
case "object":
if obj, ok := val.(string); ok {
prop.Object = []string{obj}
} else if objSlice, ok := val.([]interface{}); ok {
var objects []string
for _, v := range objSlice {
if str, ok := v.(string); ok {
objects = append(objects, str)
}
}
prop.Object = objects
}
default:
if str, ok := val.(string); ok {
prop.Text = &str
}
}
return *prop
}
// getProperty returns the property id and name from the ObjectShowResponse.
func (s *ObjectService) getProperty(key string, resp *pb.RpcObjectShowResponse) (string, string) {
// Handle special cases first
switch key {
case bundle.RelationKeyCreator.String():
return "created_by", "Created By"
case bundle.RelationKeyCreatedDate.String():
return "created_date", "Created Date"
}
if property, err := bundle.GetRelation(domain.RelationKey(key)); err == nil {
return strcase.ToSnake(key), property.Name
}
// Fallback to resolving the property name
spaceId := resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeySpaceId.String()].GetStringValue()
if name, err2 := util.ResolveRelationKeyToPropertyName(s.mw, spaceId, key); err2 == nil {
return key, name
}
return key, key
}
// convertValue converts a protobuf types.Value into a native Go value.
func (s *ObjectService) convertValue(key string, value *types.Value, format string, details []*model.ObjectViewDetailsSet) interface{} {
switch kind := value.Kind.(type) {
case *types.Value_NullValue:
return nil
case *types.Value_NumberValue:
if format == "date" {
return time.Unix(int64(kind.NumberValue), 0).UTC().Format(time.RFC3339)
}
return kind.NumberValue
case *types.Value_StringValue:
// TODO: investigate how this is possible? select option not list and not returned in further details
if format == "select" {
return s.resolveTag(details[0].Details.Fields[bundle.RelationKeySpaceId.String()].GetStringValue(), kind.StringValue)
}
if format == "multi_select" {
return []Tag{s.resolveTag(details[0].Details.Fields[bundle.RelationKeySpaceId.String()].GetStringValue(), kind.StringValue)}
}
return kind.StringValue
case *types.Value_BoolValue:
return kind.BoolValue
case *types.Value_StructValue:
m := make(map[string]interface{})
for k, v := range kind.StructValue.Fields {
m[k] = s.convertValue(key, v, format, details)
}
return m
case *types.Value_ListValue:
var list []interface{}
for _, v := range kind.ListValue.Values {
list = append(list, s.convertValue(key, v, format, details))
}
if format == "select" {
tags := s.getTags(key, details)
if len(tags) > 0 {
return tags[0]
}
return nil
}
if format == "multi_select" {
return s.getTags(key, details)
}
return list
default:
return nil
}
}
// getPropertyFormatMap returns the map of property key to property format from the ObjectShowResponse.
func (s *ObjectService) getPropertyFormatMap(propertyLinks []*model.RelationLink) map[string]string {
propertyFormatToName := make(map[int32]string, len(model.RelationFormat_name))
for k := range model.RelationFormat_name {
propertyFormatToName[k] = s.MapRelationFormat(model.RelationFormat(k))
}
propertyFormatMap := map[string]string{}
for _, detail := range propertyLinks {
propertyFormatMap[detail.Key] = propertyFormatToName[int32(detail.Format)]
}
return propertyFormatMap
}
// TODO: remove once bug of select option not being returned in details is fixed
func (s *ObjectService) resolveTag(spaceId, tagId string) Tag {
if tagId == "" {
return Tag{}
}
resp := s.mw.ObjectShow(context.Background(), &pb.RpcObjectShowRequest{
SpaceId: spaceId,
ObjectId: tagId,
})
if resp.Error.Code != pb.RpcObjectShowResponseError_NULL {
return Tag{}
}
return Tag{
Id: tagId,
Name: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyName.String()].GetStringValue(),
Color: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyRelationOptionColor.String()].GetStringValue(),
}
}
// getTags returns the list of tags from the ObjectShowResponse
func (s *ObjectService) getTags(resp *pb.RpcObjectShowResponse) []Tag {
func (s *ObjectService) getTags(key string, details []*model.ObjectViewDetailsSet) []Tag {
tags := []Tag{}
tagField, ok := resp.ObjectView.Details[0].Details.Fields["tag"]
tagField, ok := details[0].Details.Fields[key]
if !ok || tagField.GetListValue() == nil {
return tags
}
for _, tagId := range tagField.GetListValue().Values {
id := tagId.GetStringValue()
for _, detail := range resp.ObjectView.Details {
for _, detail := range details {
if detail.Id == id {
tags = append(tags, Tag{
Id: id,
@ -512,13 +828,14 @@ func (s *ObjectService) getTags(resp *pb.RpcObjectShowResponse) []Tag {
return tags
}
// GetBlocks returns the list of blocks from the ObjectShowResponse.
func (s *ObjectService) GetBlocks(resp *pb.RpcObjectShowResponse) []Block {
// getBlocks returns the list of blocks from the ObjectShowResponse.
func (s *ObjectService) getBlocks(resp *pb.RpcObjectShowResponse) []Block {
blocks := []Block{}
for _, block := range resp.ObjectView.Blocks {
var text *Text
var file *File
var property *Property
switch content := block.Content.(type) {
case *model.BlockContentOfText:
@ -527,7 +844,7 @@ func (s *ObjectService) GetBlocks(resp *pb.RpcObjectShowResponse) []Block {
Style: model.BlockContentTextStyle_name[int32(content.Text.Style)],
Checked: content.Text.Checked,
Color: content.Text.Color,
Icon: util.GetIconFromEmojiOrImage(s.AccountInfo, content.Text.IconEmoji, content.Text.IconImage),
Icon: util.GetIcon(s.AccountInfo, content.Text.IconEmoji, content.Text.IconImage, "", 0),
}
case *model.BlockContentOfFile:
file = &File{
@ -541,8 +858,13 @@ func (s *ObjectService) GetBlocks(resp *pb.RpcObjectShowResponse) []Block {
State: model.BlockContentFileState_name[int32(content.File.State)],
Style: model.BlockContentFileStyle_name[int32(content.File.Style)],
}
// TODO: other content types?
case *model.BlockContentOfRelation:
property = &Property{
// TODO: is it sufficient to return the id only?
Id: content.Relation.Key,
}
}
// TODO: other content types?
blocks = append(blocks, Block{
Id: block.Id,
@ -552,13 +874,25 @@ func (s *ObjectService) GetBlocks(resp *pb.RpcObjectShowResponse) []Block {
VerticalAlign: model.BlockVerticalAlign_name[int32(block.VerticalAlign)],
Text: text,
File: file,
Property: property,
})
}
return blocks
}
func PosixToISO8601(posix float64) string {
t := time.Unix(int64(posix), 0).UTC()
return t.Format(time.RFC3339)
// MapRelationFormat maps the relation format to a string.
func (s *ObjectService) MapRelationFormat(format model.RelationFormat) string {
switch format {
case model.RelationFormat_longtext:
return "text"
case model.RelationFormat_shorttext:
return "text"
case model.RelationFormat_tag:
return "multi_select"
case model.RelationFormat_status:
return "select"
default:
return strcase.ToSnake(model.RelationFormat_name[int32(format)])
}
}

View file

@ -8,46 +8,46 @@ import (
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/anyproto/anytype-heart/core/api/apicore/mock_apicore"
"github.com/anyproto/anytype-heart/core/api/internal/space"
"github.com/anyproto/anytype-heart/core/api/util"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pb/service/mock_service"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
const (
offset = 0
limit = 100
mockedTechSpaceId = "mocked-tech-space-id"
gatewayUrl = "http://localhost:31006"
mockedSpaceId = "mocked-space-id"
mockedObjectId = "mocked-object-id"
mockedObjectType = "mocked-object-type"
mockedNewObjectId = "mocked-new-object-id"
mockedObjectName = "mocked-object-name"
mockedObjectSnippet = "mocked-object-snippet"
mockedObjectIcon = "🔍"
mockedObjectTypeUniqueKey = "ot-page"
mockedTypeId = "mocked-type-id"
mockedTypeName = "mocked-type-name"
mockedTypeUniqueKey = "mocked-type-unique-key"
mockedTypeIcon = "📝"
mockedTemplateId = "mocked-template-id"
mockedTemplateName = "mocked-template-name"
mockedTemplateIcon = "📃"
offset = 0
limit = 100
mockedTechSpaceId = "mocked-tech-space-id"
gatewayUrl = "http://localhost:31006"
mockedSpaceId = "mocked-space-id"
mockedObjectId = "mocked-object-id"
mockedNewObjectId = "mocked-new-object-id"
mockedObjectName = "mocked-object-name"
mockedObjectSnippet = "mocked-object-snippet"
mockedObjectIcon = "🔍"
mockedParticipantId = "mocked-participant-id"
mockedTypeKey = "ot-page"
mockedTypeId = "mocked-type-id"
mockedTypeName = "mocked-type-name"
mockedTypeIcon = "📝"
mockedTemplateId = "mocked-template-id"
mockedTemplateName = "mocked-template-name"
mockedTemplateIcon = "📃"
)
type fixture struct {
*ObjectService
mwMock *mock_service.MockClientCommandsServer
mwMock *mock_apicore.MockClientCommands
}
func newFixture(t *testing.T) *fixture {
mw := mock_service.NewMockClientCommandsServer(t)
mwMock := mock_apicore.NewMockClientCommands(t)
spaceService := space.NewService(mw)
objectService := NewService(mw, spaceService)
spaceService := space.NewService(mwMock)
objectService := NewService(mwMock, spaceService)
objectService.AccountInfo = &model.AccountInfo{
TechSpaceId: mockedTechSpaceId,
GatewayUrl: gatewayUrl,
@ -55,7 +55,7 @@ func newFixture(t *testing.T) *fixture {
return &fixture{
ObjectService: objectService,
mwMock: mw,
mwMock: mwMock,
}
}
@ -65,11 +65,12 @@ func TestObjectService_ListObjects(t *testing.T) {
ctx := context.Background()
fx := newFixture(t)
// Mock object search
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
RelationKey: bundle.RelationKeyLayout.String(),
RelationKey: bundle.RelationKeyResolvedLayout.String(),
Condition: model.BlockContentDataviewFilter_In,
Value: pbtypes.IntList([]int{
int(model.ObjectType_basic),
@ -82,6 +83,18 @@ func TestObjectService_ListObjects(t *testing.T) {
int(model.ObjectType_participant),
}...),
},
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: "type.uniqueKey",
Condition: model.BlockContentDataviewFilter_NotEqual,
Value: pbtypes.String("ot-template"),
},
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyIsHidden.String(),
Condition: model.BlockContentDataviewFilter_NotEqual,
Value: pbtypes.Bool(true),
},
},
Sorts: []*model.BlockContentDataviewSort{{
RelationKey: bundle.RelationKeyLastModifiedDate.String(),
@ -95,12 +108,12 @@ func TestObjectService_ListObjects(t *testing.T) {
Records: []*types.Struct{
{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String(mockedObjectId),
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectName),
bundle.RelationKeySnippet.String(): pbtypes.String(mockedObjectSnippet),
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedObjectIcon),
bundle.RelationKeyType.String(): pbtypes.String(mockedObjectTypeUniqueKey),
bundle.RelationKeyLayout.String(): pbtypes.Float64(float64(model.ObjectType_basic)),
bundle.RelationKeyId.String(): pbtypes.String(mockedObjectId),
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectName),
bundle.RelationKeySnippet.String(): pbtypes.String(mockedObjectSnippet),
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedObjectIcon),
bundle.RelationKeyType.String(): pbtypes.String(mockedTypeId),
bundle.RelationKeyResolvedLayout.String(): pbtypes.Float64(float64(model.ObjectType_basic)),
},
},
},
@ -116,100 +129,102 @@ func TestObjectService_ListObjects(t *testing.T) {
RootId: mockedObjectId,
Details: []*model.ObjectViewDetailsSet{
{
Id: mockedObjectId,
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String(mockedObjectId),
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectName),
bundle.RelationKeySnippet.String(): pbtypes.String(mockedObjectSnippet),
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedObjectIcon),
bundle.RelationKeyType.String(): pbtypes.String(mockedObjectTypeUniqueKey),
bundle.RelationKeyCreatedDate.String(): pbtypes.Float64(888888),
bundle.RelationKeyLastModifiedBy.String(): pbtypes.String(mockedParticipantId),
bundle.RelationKeyLastModifiedDate.String(): pbtypes.Float64(999999),
bundle.RelationKeyCreator.String(): pbtypes.String(mockedParticipantId),
bundle.RelationKeyLastOpenedDate.String(): pbtypes.Float64(0),
bundle.RelationKeySpaceId.String(): pbtypes.String(mockedSpaceId),
bundle.RelationKeyType.String(): pbtypes.String(mockedTypeId),
},
},
},
{
Id: mockedParticipantId,
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String(mockedParticipantId),
},
},
},
{
Id: mockedTypeId,
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String(mockedTypeId),
bundle.RelationKeyName.String(): pbtypes.String(mockedTypeName),
bundle.RelationKeyUniqueKey.String(): pbtypes.String(mockedTypeKey),
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedTypeIcon),
},
},
},
},
RelationLinks: []*model.RelationLink{
{
Key: bundle.RelationKeyLastModifiedDate.String(),
Format: model.RelationFormat_date,
},
{
Key: bundle.RelationKeyLastModifiedBy.String(),
Format: model.RelationFormat_object,
},
{
Key: bundle.RelationKeyCreatedDate.String(),
Format: model.RelationFormat_date,
},
{
Key: bundle.RelationKeyCreator.String(),
Format: model.RelationFormat_object,
},
{
Key: bundle.RelationKeyLastOpenedDate.String(),
Format: model.RelationFormat_date,
},
{
Key: bundle.RelationKeyTag.String(),
Format: model.RelationFormat_tag,
},
},
},
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
}).Once()
// Mock type resolution
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
RelationKey: bundle.RelationKeyUniqueKey.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(mockedObjectTypeUniqueKey),
},
},
Keys: []string{bundle.RelationKeyName.String()},
}).Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{
{
Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectType),
},
},
},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// Mock participant details
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyId.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(""),
},
},
Keys: []string{
bundle.RelationKeyId.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyIconEmoji.String(),
bundle.RelationKeyIconImage.String(),
bundle.RelationKeyIdentity.String(),
bundle.RelationKeyGlobalName.String(),
bundle.RelationKeyParticipantPermissions.String(),
},
}).Return(&pb.RpcObjectSearchResponse{
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
Records: []*types.Struct{
{},
},
}).Twice()
// when
objects, total, hasMore, err := fx.ListObjects(ctx, mockedSpaceId, offset, limit)
// then
require.NoError(t, err)
require.Len(t, objects, 1)
require.Equal(t, mockedObjectType, objects[0].Type)
require.Equal(t, mockedTypeId, objects[0].Type.Id)
require.Equal(t, mockedTypeName, objects[0].Type.Name)
require.Equal(t, mockedTypeKey, objects[0].Type.Key)
require.Equal(t, util.Icon{Format: "emoji", Emoji: util.StringPtr(mockedTypeIcon)}, objects[0].Type.Icon)
require.Equal(t, mockedObjectId, objects[0].Id)
require.Equal(t, mockedObjectName, objects[0].Name)
require.Equal(t, mockedObjectSnippet, objects[0].Snippet)
require.Equal(t, mockedObjectIcon, objects[0].Icon)
require.Equal(t, 6, len(objects[0].Details))
require.Equal(t, util.Icon{Format: "emoji", Emoji: util.StringPtr(mockedObjectIcon)}, objects[0].Icon)
require.Equal(t, 5, len(objects[0].Properties))
for _, detail := range objects[0].Details {
for _, detail := range objects[0].Properties {
if detail.Id == "created_date" {
require.Equal(t, "1970-01-11T06:54:48Z", detail.Details["created_date"])
require.Equal(t, "1970-01-11T06:54:48Z", *detail.Date)
} else if detail.Id == "created_by" {
require.Empty(t, detail.Details["created_by"])
require.Equal(t, []string{mockedParticipantId}, detail.Object)
} else if detail.Id == "last_modified_date" {
require.Equal(t, "1970-01-12T13:46:39Z", detail.Details["last_modified_date"])
require.Equal(t, "1970-01-12T13:46:39Z", *detail.Date)
} else if detail.Id == "last_modified_by" {
require.Empty(t, detail.Details["last_modified_by"])
require.Equal(t, []string{mockedParticipantId}, detail.Object)
} else if detail.Id == "last_opened_date" {
require.Equal(t, "1970-01-01T00:00:00Z", detail.Details["last_opened_date"])
} else if detail.Id == "tags" {
require.Empty(t, detail.Details["tags"])
require.Equal(t, "1970-01-01T00:00:00Z", *detail.Date)
} else if detail.Id == "tag" {
require.Empty(t, detail.MultiSelect)
} else {
t.Errorf("unexpected detail id: %s", detail.Id)
}
@ -224,14 +239,52 @@ func TestObjectService_ListObjects(t *testing.T) {
ctx := context.Background()
fx := newFixture(t)
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).
Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// Mock object search
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
RelationKey: bundle.RelationKeyResolvedLayout.String(),
Condition: model.BlockContentDataviewFilter_In,
Value: pbtypes.IntList([]int{
int(model.ObjectType_basic),
int(model.ObjectType_profile),
int(model.ObjectType_todo),
int(model.ObjectType_note),
int(model.ObjectType_bookmark),
int(model.ObjectType_set),
int(model.ObjectType_collection),
int(model.ObjectType_participant),
}...),
},
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: "type.uniqueKey",
Condition: model.BlockContentDataviewFilter_NotEqual,
Value: pbtypes.String("ot-template"),
},
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyIsHidden.String(),
Condition: model.BlockContentDataviewFilter_NotEqual,
Value: pbtypes.Bool(true),
},
},
Sorts: []*model.BlockContentDataviewSort{{
RelationKey: bundle.RelationKeyLastModifiedDate.String(),
Type: model.BlockContentDataviewSort_Desc,
Format: model.RelationFormat_longtext,
IncludeTime: true,
EmptyPlacement: model.BlockContentDataviewSort_NotSpecified,
}},
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeyName.String()},
}).Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// when
objects, total, hasMore, err := fx.ListObjects(ctx, "empty-space", offset, limit)
objects, total, hasMore, err := fx.ListObjects(ctx, mockedSpaceId, offset, limit)
// then
require.NoError(t, err)
@ -257,13 +310,14 @@ func TestObjectService_GetObject(t *testing.T) {
RootId: mockedObjectId,
Details: []*model.ObjectViewDetailsSet{
{
Id: mockedObjectId,
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String(mockedObjectId),
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectName),
bundle.RelationKeySnippet.String(): pbtypes.String(mockedObjectSnippet),
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedObjectName),
bundle.RelationKeyType.String(): pbtypes.String(mockedObjectTypeUniqueKey),
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedObjectIcon),
bundle.RelationKeyType.String(): pbtypes.String(mockedTypeId),
bundle.RelationKeyLastModifiedDate.String(): pbtypes.Float64(999999),
bundle.RelationKeyCreatedDate.String(): pbtypes.Float64(888888),
bundle.RelationKeyLastOpenedDate.String(): pbtypes.Float64(0),
@ -271,86 +325,78 @@ func TestObjectService_GetObject(t *testing.T) {
},
},
},
{
Id: mockedTypeId,
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String(mockedTypeId),
bundle.RelationKeyName.String(): pbtypes.String(mockedTypeName),
bundle.RelationKeyUniqueKey.String(): pbtypes.String(mockedTypeKey),
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedTypeIcon),
},
},
},
},
RelationLinks: []*model.RelationLink{
{
Key: bundle.RelationKeyLastModifiedDate.String(),
Format: model.RelationFormat_date,
},
{
Key: bundle.RelationKeyLastModifiedBy.String(),
Format: model.RelationFormat_object,
},
{
Key: bundle.RelationKeyCreatedDate.String(),
Format: model.RelationFormat_date,
},
{
Key: bundle.RelationKeyCreator.String(),
Format: model.RelationFormat_object,
},
{
Key: bundle.RelationKeyLastOpenedDate.String(),
Format: model.RelationFormat_date,
},
{
Key: bundle.RelationKeyTag.String(),
Format: model.RelationFormat_tag,
},
},
},
}, nil).Once()
// Mock type resolution
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
RelationKey: bundle.RelationKeyUniqueKey.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(mockedObjectTypeUniqueKey),
},
},
Keys: []string{bundle.RelationKeyName.String()},
}).Return(&pb.RpcObjectSearchResponse{
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
Records: []*types.Struct{
{
Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectType),
},
},
},
}, nil).Once()
// Mock participant details
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyId.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(""),
},
},
Keys: []string{
bundle.RelationKeyId.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyIconEmoji.String(),
bundle.RelationKeyIconImage.String(),
bundle.RelationKeyIdentity.String(),
bundle.RelationKeyGlobalName.String(),
bundle.RelationKeyParticipantPermissions.String(),
},
}).Return(&pb.RpcObjectSearchResponse{
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
Records: []*types.Struct{
{},
},
}).Twice()
// when
object, err := fx.GetObject(ctx, mockedSpaceId, mockedObjectId)
// then
require.NoError(t, err)
require.Equal(t, mockedObjectType, object.Type)
require.Equal(t, "object", object.Object)
require.Equal(t, mockedTypeId, object.Type.Id)
require.Equal(t, mockedTypeName, object.Type.Name)
require.Equal(t, mockedTypeKey, object.Type.Key)
require.Equal(t, util.Icon{Format: "emoji", Emoji: util.StringPtr(mockedTypeIcon)}, object.Type.Icon)
require.Equal(t, mockedObjectId, object.Id)
require.Equal(t, mockedObjectName, object.Name)
require.Equal(t, mockedObjectSnippet, object.Snippet)
require.Equal(t, mockedObjectName, object.Icon)
require.Equal(t, 6, len(object.Details))
require.Equal(t, util.Icon{Format: "emoji", Emoji: util.StringPtr(mockedObjectIcon)}, object.Icon)
require.Equal(t, 3, len(object.Properties))
for _, detail := range object.Details {
if detail.Id == "created_date" {
require.Equal(t, "1970-01-11T06:54:48Z", detail.Details["created_date"])
} else if detail.Id == "created_by" {
require.Empty(t, detail.Details["created_by"])
} else if detail.Id == "last_modified_date" {
require.Equal(t, "1970-01-12T13:46:39Z", detail.Details["last_modified_date"])
} else if detail.Id == "last_modified_by" {
require.Empty(t, detail.Details["last_modified_by"])
} else if detail.Id == "last_opened_date" {
require.Equal(t, "1970-01-01T00:00:00Z", detail.Details["last_opened_date"])
} else if detail.Id == "tags" {
require.Empty(t, detail.Details["tags"])
for _, property := range object.Properties {
if property.Id == "created_date" {
require.Equal(t, "1970-01-11T06:54:48Z", *property.Date)
} else if property.Id == "created_by" {
require.Empty(t, property.Object)
} else if property.Id == "last_modified_date" {
require.Equal(t, "1970-01-12T13:46:39Z", *property.Date)
} else if property.Id == "last_modified_by" {
require.Empty(t, property.Object)
} else if property.Id == "last_opened_date" {
require.Equal(t, "1970-01-01T00:00:00Z", *property.Date)
} else if property.Id == "tag" {
require.Empty(t, property.MultiSelect)
} else {
t.Errorf("unexpected detail id: %s", detail.Id)
t.Errorf("unexpected property id: %s", property.Id)
}
}
})
@ -390,9 +436,9 @@ func TestObjectService_CreateObject(t *testing.T) {
bundle.RelationKeyOrigin.String(): pbtypes.Int64(int64(model.ObjectOrigin_api)),
},
},
TemplateId: "",
TemplateId: mockedTemplateId,
SpaceId: mockedSpaceId,
ObjectTypeUniqueKey: mockedObjectTypeUniqueKey,
ObjectTypeUniqueKey: mockedTypeKey,
WithChat: false,
}).Return(&pb.RpcObjectCreateResponse{
ObjectId: mockedNewObjectId,
@ -416,14 +462,26 @@ func TestObjectService_CreateObject(t *testing.T) {
RootId: mockedNewObjectId,
Details: []*model.ObjectViewDetailsSet{
{
Id: mockedNewObjectId,
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String(mockedNewObjectId),
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectName),
bundle.RelationKeyLayout.String(): pbtypes.Float64(float64(model.ObjectType_basic)),
bundle.RelationKeyType.String(): pbtypes.String(mockedObjectTypeUniqueKey),
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedObjectIcon),
bundle.RelationKeySpaceId.String(): pbtypes.String(mockedSpaceId),
bundle.RelationKeyId.String(): pbtypes.String(mockedNewObjectId),
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectName),
bundle.RelationKeyResolvedLayout.String(): pbtypes.Float64(float64(model.ObjectType_basic)),
bundle.RelationKeyType.String(): pbtypes.String(mockedTypeId),
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedObjectIcon),
bundle.RelationKeySpaceId.String(): pbtypes.String(mockedSpaceId),
},
},
},
{
Id: mockedTypeId,
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String(mockedTypeId),
bundle.RelationKeyName.String(): pbtypes.String(mockedTypeName),
bundle.RelationKeyUniqueKey.String(): pbtypes.String(mockedTypeKey),
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedTypeIcon),
},
},
},
@ -432,70 +490,24 @@ func TestObjectService_CreateObject(t *testing.T) {
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
}).Once()
// Mock type resolution
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
RelationKey: bundle.RelationKeyUniqueKey.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(mockedObjectTypeUniqueKey),
},
},
Keys: []string{bundle.RelationKeyName.String()},
}).Return(&pb.RpcObjectSearchResponse{
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
Records: []*types.Struct{
{
Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectType),
},
},
},
}).Once()
// Mock participant details
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyId.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(""),
},
},
Keys: []string{
bundle.RelationKeyId.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyIconEmoji.String(),
bundle.RelationKeyIconImage.String(),
bundle.RelationKeyIdentity.String(),
bundle.RelationKeyGlobalName.String(),
bundle.RelationKeyParticipantPermissions.String(),
},
}).Return(&pb.RpcObjectSearchResponse{
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
Records: []*types.Struct{
{},
},
}).Twice()
// when
object, err := fx.CreateObject(ctx, mockedSpaceId, CreateObjectRequest{
Name: mockedObjectName,
Icon: mockedObjectIcon,
// TODO: use actual values
TemplateId: "",
ObjectTypeUniqueKey: mockedObjectTypeUniqueKey,
Name: mockedObjectName,
Icon: util.Icon{Format: util.IconFormatEmoji, Emoji: util.StringPtr(mockedObjectIcon)},
TemplateId: mockedTemplateId,
TypeKey: mockedTypeKey,
})
// then
require.NoError(t, err)
require.Equal(t, mockedObjectType, object.Type)
require.Equal(t, "object", object.Object)
require.Equal(t, mockedTypeId, object.Type.Id)
require.Equal(t, mockedTypeName, object.Type.Name)
require.Equal(t, mockedTypeKey, object.Type.Key)
require.Equal(t, util.Icon{Format: "emoji", Emoji: util.StringPtr(mockedTypeIcon)}, object.Type.Icon)
require.Equal(t, mockedNewObjectId, object.Id)
require.Equal(t, mockedObjectName, object.Name)
require.Equal(t, mockedObjectIcon, object.Icon)
require.Equal(t, util.Icon{Format: "emoji", Emoji: util.StringPtr(mockedObjectIcon)}, object.Icon)
require.Equal(t, mockedSpaceId, object.SpaceId)
})
@ -512,7 +524,7 @@ func TestObjectService_CreateObject(t *testing.T) {
// when
object, err := fx.CreateObject(ctx, mockedSpaceId, CreateObjectRequest{
Name: "Fail Object",
Icon: "",
Icon: util.Icon{},
})
// then
@ -550,8 +562,8 @@ func TestObjectService_ListTypes(t *testing.T) {
require.Len(t, types, 1)
require.Equal(t, "type-1", types[0].Id)
require.Equal(t, "Type One", types[0].Name)
require.Equal(t, "type-one-key", types[0].UniqueKey)
require.Equal(t, "🗂️", types[0].Icon)
require.Equal(t, "type-one-key", types[0].Key)
require.Equal(t, util.Icon{Format: "emoji", Emoji: util.StringPtr("🗂️")}, types[0].Icon)
require.Equal(t, 1, total)
require.False(t, hasMore)
})
@ -596,7 +608,7 @@ func TestObjectService_GetType(t *testing.T) {
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String(mockedTypeId),
bundle.RelationKeyName.String(): pbtypes.String(mockedTypeName),
bundle.RelationKeyUniqueKey.String(): pbtypes.String(mockedTypeUniqueKey),
bundle.RelationKeyUniqueKey.String(): pbtypes.String(mockedTypeKey),
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedTypeIcon),
bundle.RelationKeyRecommendedLayout.String(): pbtypes.Float64(float64(model.ObjectType_basic)),
},
@ -613,8 +625,8 @@ func TestObjectService_GetType(t *testing.T) {
require.NoError(t, err)
require.Equal(t, mockedTypeId, objType.Id)
require.Equal(t, mockedTypeName, objType.Name)
require.Equal(t, mockedTypeUniqueKey, objType.UniqueKey)
require.Equal(t, mockedTypeIcon, objType.Icon)
require.Equal(t, mockedTypeKey, objType.Key)
require.Equal(t, util.Icon{Format: "emoji", Emoji: util.StringPtr(mockedTypeIcon)}, objType.Icon)
require.Equal(t, model.ObjectTypeLayout_name[int32(model.ObjectType_basic)], objType.RecommendedLayout)
})
@ -696,7 +708,7 @@ func TestObjectService_ListTemplates(t *testing.T) {
require.Len(t, templates, 1)
require.Equal(t, "template-1", templates[0].Id)
require.Equal(t, "Template Name", templates[0].Name)
require.Equal(t, "📝", templates[0].Icon)
require.Equal(t, util.Icon{Format: "emoji", Emoji: util.StringPtr("📝")}, templates[0].Icon)
require.Equal(t, 1, total)
require.False(t, hasMore)
})
@ -750,13 +762,13 @@ func TestObjectService_GetTemplate(t *testing.T) {
}).Once()
// when
template, err := fx.GetTemplate(ctx, mockedSpaceId, mockedObjectType, mockedTemplateId)
template, err := fx.GetTemplate(ctx, mockedSpaceId, mockedTypeId, mockedTemplateId)
// then
require.NoError(t, err)
require.Equal(t, mockedTemplateId, template.Id)
require.Equal(t, mockedTemplateName, template.Name)
require.Equal(t, mockedTemplateIcon, template.Icon)
require.Equal(t, util.Icon{Format: "emoji", Emoji: util.StringPtr(mockedTemplateIcon)}, template.Icon)
})
t.Run("template not found", func(t *testing.T) {

View file

@ -11,17 +11,19 @@ import (
// GlobalSearchHandler searches and retrieves objects across all spaces
//
// @Summary Search objects across all spaces
// @Tags search
// @Accept json
// @Produce json
// @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 SearchRequest true "Search parameters"
// @Success 200 {object} pagination.PaginatedResponse[object.Object] "List of objects"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /search [post]
// @Summary Search objects across all spaces
// @Description This endpoint executes a global search over every space the user has access to. It accepts pagination parameters (offset and limit) and a JSON body containing search criteria. The criteria include a search query string, an optional list of object types, and sort options (e.g. ascending/descending by creation, modification, or last opened dates). Internally, the endpoint aggregates results from each space, merges and sorts them (after last modified date by default), and returns a unified, paginated list of objects that match the search parameters.
// @Tags search
// @Accept json
// @Produce json
// @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 SearchRequest true "Search parameters"
// @Success 200 {object} pagination.PaginatedResponse[object.Object] "List of objects"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /search [post]
func GlobalSearchHandler(s *SearchService) gin.HandlerFunc {
return func(c *gin.Context) {
offset := c.GetInt("offset")
@ -51,18 +53,20 @@ func GlobalSearchHandler(s *SearchService) gin.HandlerFunc {
// SearchHandler searches and retrieves objects within a space
//
// @Summary Search objects within a space
// @Tags search
// @Accept json
// @Produce json
// @Param space_id path string true "Space ID"
// @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 SearchRequest true "Search parameters"
// @Success 200 {object} pagination.PaginatedResponse[object.Object] "List of objects"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /spaces/{space_id}/search [post]
// @Summary Search objects within a space
// @Description This endpoint 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.
// @Tags search
// @Accept json
// @Produce json
// @Param space_id path string true "Space ID"
// @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 SearchRequest true "Search parameters"
// @Success 200 {object} pagination.PaginatedResponse[object.Object] "List of objects"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/search [post]
func SearchHandler(s *SearchService) gin.HandlerFunc {
return func(c *gin.Context) {
spaceID := c.Param("space_id")

View file

@ -1,12 +1,28 @@
package search
type SortDirection string
const (
Asc SortDirection = "asc"
Desc SortDirection = "desc"
)
type SortProperty string
const (
CreatedDate SortProperty = "created_date"
LastModifiedDate SortProperty = "last_modified_date"
LastOpenedDate SortProperty = "last_opened_date"
Name SortProperty = "name"
)
type SearchRequest struct {
Query string `json:"query"`
Types []string `json:"types"`
Sort SortOptions `json:"sort"`
Query string `json:"query" example:"test"` // The search term to look for in object names and snippets
Types []string `json:"types" example:"ot-page,ot-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
}
type SortOptions struct {
Direction string `json:"direction" enums:"asc,desc" default:"desc"`
Timestamp string `json:"timestamp" enums:"created_date,last_modified_date,last_opened_date" default:"last_modified_date"`
Property SortProperty `json:"property" 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
}

View file

@ -6,12 +6,12 @@ import (
"sort"
"strings"
"github.com/anyproto/anytype-heart/core/api/apicore"
"github.com/anyproto/anytype-heart/core/api/internal/object"
"github.com/anyproto/anytype-heart/core/api/internal/space"
"github.com/anyproto/anytype-heart/core/api/pagination"
"github.com/anyproto/anytype-heart/core/api/util"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pb/service"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/pbtypes"
@ -28,13 +28,13 @@ type Service interface {
}
type SearchService struct {
mw service.ClientCommandsServer
mw apicore.ClientCommands
spaceService *space.SpaceService
objectService *object.ObjectService
AccountInfo *model.AccountInfo
}
func NewService(mw service.ClientCommandsServer, spaceService *space.SpaceService, objectService *object.ObjectService) *SearchService {
func NewService(mw apicore.ClientCommands, spaceService *space.SpaceService, objectService *object.ObjectService) *SearchService {
return &SearchService{mw: mw, spaceService: spaceService, objectService: objectService}
}
@ -48,19 +48,31 @@ func (s *SearchService) GlobalSearch(ctx context.Context, request SearchRequest,
baseFilters := s.prepareBaseFilters()
queryFilters := s.prepareQueryFilter(request.Query)
sorts := s.prepareSorts(request.Sort)
dateToSortAfter := sorts.RelationKey
if len(sorts) == 0 {
return nil, 0, false, errors.New("no sort criteria provided")
}
criterionToSortAfter := sorts[0].RelationKey
type sortRecord struct {
Id string
SpaceId string
numericSort float64
stringSort string
}
combinedRecords := make([]sortRecord, 0)
allResponses := make([]*pb.RpcObjectSearchResponse, 0, len(spaces))
for _, space := range spaces {
// Resolve object type IDs per space, as they are unique per space
// Resolve template type and object type IDs per space, as they are unique per space
templateFilter := s.prepareTemplateFilter()
typeFilters := s.prepareObjectTypeFilters(space.Id, request.Types)
filters := s.combineFilters(model.BlockContentDataviewFilter_And, baseFilters, queryFilters, typeFilters)
filters := s.combineFilters(model.BlockContentDataviewFilter_And, baseFilters, templateFilter, queryFilters, typeFilters)
objResp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
SpaceId: space.Id,
Filters: filters,
Sorts: []*model.BlockContentDataviewSort{sorts},
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeySpaceId.String(), dateToSortAfter},
Sorts: sorts,
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeySpaceId.String(), criterionToSortAfter},
Limit: int32(offset + limit), // nolint: gosec
})
@ -71,29 +83,35 @@ func (s *SearchService) GlobalSearch(ctx context.Context, request SearchRequest,
allResponses = append(allResponses, objResp)
}
combinedRecords := make([]struct {
Id string
SpaceId string
DateToSortAfter float64
}, 0)
for _, objResp := range allResponses {
for _, record := range objResp.Records {
combinedRecords = append(combinedRecords, struct {
Id string
SpaceId string
DateToSortAfter float64
}{
Id: record.Fields[bundle.RelationKeyId.String()].GetStringValue(),
SpaceId: record.Fields[bundle.RelationKeySpaceId.String()].GetStringValue(),
DateToSortAfter: record.Fields[dateToSortAfter].GetNumberValue(),
})
sr := sortRecord{
Id: record.Fields[bundle.RelationKeyId.String()].GetStringValue(),
SpaceId: record.Fields[bundle.RelationKeySpaceId.String()].GetStringValue(),
}
if criterionToSortAfter == bundle.RelationKeyName.String() {
sr.stringSort = record.Fields[criterionToSortAfter].GetStringValue()
} else {
sr.numericSort = record.Fields[criterionToSortAfter].GetNumberValue()
}
combinedRecords = append(combinedRecords, sr)
}
}
// sort after posix last_modified_date to achieve descending sort order across all spaces
sort.Slice(combinedRecords, func(i, j int) bool {
return combinedRecords[i].DateToSortAfter > combinedRecords[j].DateToSortAfter
})
sortFunc := func(i, j int) bool {
if criterionToSortAfter == bundle.RelationKeyName.String() {
if sorts[0].Type == model.BlockContentDataviewSort_Asc {
return combinedRecords[i].stringSort < combinedRecords[j].stringSort
}
return combinedRecords[i].stringSort > combinedRecords[j].stringSort
} else {
if sorts[0].Type == model.BlockContentDataviewSort_Asc {
return combinedRecords[i].numericSort < combinedRecords[j].numericSort
}
return combinedRecords[i].numericSort > combinedRecords[j].numericSort
}
}
sort.SliceStable(combinedRecords, sortFunc)
total = len(combinedRecords)
paginatedRecords, hasMore := pagination.Paginate(combinedRecords, offset, limit)
@ -113,18 +131,22 @@ func (s *SearchService) GlobalSearch(ctx context.Context, request SearchRequest,
// Search retrieves a paginated list of objects from a specific space that match the search parameters.
func (s *SearchService) Search(ctx context.Context, spaceId string, request SearchRequest, offset int, limit int) (objects []object.Object, total int, hasMore bool, err error) {
baseFilters := s.prepareBaseFilters()
templateFilter := s.prepareTemplateFilter()
queryFilters := s.prepareQueryFilter(request.Query)
typeFilters := s.prepareObjectTypeFilters(spaceId, request.Types)
filters := s.combineFilters(model.BlockContentDataviewFilter_And, baseFilters, queryFilters, typeFilters)
filters := s.combineFilters(model.BlockContentDataviewFilter_And, baseFilters, templateFilter, queryFilters, typeFilters)
sorts := s.prepareSorts(request.Sort)
dateToSortAfter := sorts.RelationKey
if len(sorts) == 0 {
return nil, 0, false, errors.New("no sort criteria provided")
}
criterionToSortAfter := sorts[0].RelationKey
resp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
SpaceId: spaceId,
Filters: filters,
Sorts: []*model.BlockContentDataviewSort{sorts},
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeySpaceId.String(), dateToSortAfter},
Sorts: sorts,
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeySpaceId.String(), criterionToSortAfter},
})
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
@ -172,7 +194,7 @@ func (s *SearchService) prepareBaseFilters() []*model.BlockContentDataviewFilter
return []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyLayout.String(),
RelationKey: bundle.RelationKeyResolvedLayout.String(),
Condition: model.BlockContentDataviewFilter_In,
Value: pbtypes.IntList([]int{
int(model.ObjectType_basic),
@ -194,6 +216,18 @@ func (s *SearchService) prepareBaseFilters() []*model.BlockContentDataviewFilter
}
}
// prepareTemplateFilter returns a filter that excludes templates from the search results.
func (s *SearchService) prepareTemplateFilter() []*model.BlockContentDataviewFilter {
return []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: "type.uniqueKey",
Condition: model.BlockContentDataviewFilter_NotEqual,
Value: pbtypes.String("ot-template"),
},
}
}
// prepareQueryFilter combines object name and snippet filters with an OR condition.
func (s *SearchService) prepareQueryFilter(searchQuery string) []*model.BlockContentDataviewFilter {
if searchQuery == "" {
@ -262,38 +296,66 @@ func (s *SearchService) prepareObjectTypeFilters(spaceId string, objectTypes []s
}
// prepareSorts returns a sort filter based on the given sort parameters
func (s *SearchService) prepareSorts(sort SortOptions) *model.BlockContentDataviewSort {
return &model.BlockContentDataviewSort{
RelationKey: s.getSortRelationKey(sort.Timestamp),
func (s *SearchService) prepareSorts(sort SortOptions) []*model.BlockContentDataviewSort {
primarySort := &model.BlockContentDataviewSort{
RelationKey: s.getSortRelationKey(sort.Property),
Type: s.getSortDirection(sort.Direction),
Format: model.RelationFormat_date,
Format: s.getSortFormat(sort.Property),
IncludeTime: true,
EmptyPlacement: model.BlockContentDataviewSort_NotSpecified,
}
// last_opened_date possibly is empty, wherefore we sort by last_modified_date as secondary criterion
if primarySort.RelationKey == bundle.RelationKeyLastOpenedDate.String() {
secondarySort := &model.BlockContentDataviewSort{
RelationKey: bundle.RelationKeyLastModifiedDate.String(),
Type: s.getSortDirection(sort.Direction),
Format: model.RelationFormat_date,
IncludeTime: true,
EmptyPlacement: model.BlockContentDataviewSort_NotSpecified,
}
return []*model.BlockContentDataviewSort{primarySort, secondarySort}
}
return []*model.BlockContentDataviewSort{primarySort}
}
// getSortRelationKey returns the relation key for the given sort timestamp
func (s *SearchService) getSortRelationKey(timestamp string) string {
func (s *SearchService) getSortRelationKey(timestamp SortProperty) string {
switch timestamp {
case "created_date":
case CreatedDate:
return bundle.RelationKeyCreatedDate.String()
case "last_modified_date":
case LastModifiedDate:
return bundle.RelationKeyLastModifiedDate.String()
case "last_opened_date":
case LastOpenedDate:
return bundle.RelationKeyLastOpenedDate.String()
case Name:
return bundle.RelationKeyName.String()
default:
return bundle.RelationKeyLastModifiedDate.String()
}
}
// getSortDirection returns the sort direction for the given string
func (s *SearchService) getSortDirection(direction string) model.BlockContentDataviewSortType {
func (s *SearchService) getSortDirection(direction SortDirection) model.BlockContentDataviewSortType {
switch direction {
case "asc":
case Asc:
return model.BlockContentDataviewSort_Asc
case "desc":
case Desc:
return model.BlockContentDataviewSort_Desc
default:
return model.BlockContentDataviewSort_Desc
}
}
// getSortFormat returns the sort format for the given timestamp
func (s *SearchService) getSortFormat(timestamp SortProperty) model.RelationFormat {
switch timestamp {
case CreatedDate, LastModifiedDate, LastOpenedDate:
return model.RelationFormat_date
case Name:
return model.RelationFormat_longtext
default:
return model.RelationFormat_date
}
}

View file

@ -8,54 +8,49 @@ import (
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/anyproto/anytype-heart/core/api/apicore/mock_apicore"
"github.com/anyproto/anytype-heart/core/api/internal/object"
"github.com/anyproto/anytype-heart/core/api/internal/space"
"github.com/anyproto/anytype-heart/core/api/util"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pb/service/mock_service"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
const (
offset = 0
limit = 100
techSpaceId = "tech-space-id"
gatewayUrl = "http://localhost:31006"
mockedSpaceId = "mocked-space-id"
mockedSearchTerm = "mocked-search-term"
mockedObjectId = "mocked-object-id"
mockedObjectName = "mocked-object-name"
mockedRootId = "mocked-root-id"
mockedParticipantId = "mocked-participant-id"
mockedType = "mocked-type"
mockedTagId1 = "mocked-tag-id-1"
mockedTagValue1 = "mocked-tag-value-1"
mockedTagColor1 = "mocked-tag-color-1"
mockedTagId2 = "mocked-tag-id-2"
mockedTagValue2 = "mocked-tag-value-2"
mockedTagColor2 = "mocked-tag-color-2"
mockedObjectTypeName = "mocked-object-type-name"
mockedParticipantName = "mocked-participant-name"
mockedParticipantIcon = "mocked-participant-icon"
mockedParticipantImage = "mocked-participant-image"
mockedParticipantIdentity = "mocked-participant-identity"
mockedParticipantGlobalName = "mocked-participant-global-name"
offset = 0
limit = 100
techSpaceId = "tech-space-id"
gatewayUrl = "http://localhost:31006"
mockedSpaceId = "mocked-space-id"
mockedSearchTerm = "mocked-search-term"
mockedObjectId = "mocked-object-id"
mockedObjectName = "mocked-object-name"
mockedObjectIcon = "🌐"
mockedParticipantId = "mocked-participant-id"
mockedType = "mocked-type"
mockedTagId1 = "mocked-tag-id-1"
mockedTagValue1 = "mocked-tag-value-1"
mockedTagColor1 = "mocked-tag-color-1"
mockedTagId2 = "mocked-tag-id-2"
mockedTagValue2 = "mocked-tag-value-2"
mockedTagColor2 = "mocked-tag-color-2"
)
type fixture struct {
*SearchService
mwMock *mock_service.MockClientCommandsServer
mwMock *mock_apicore.MockClientCommands
}
func newFixture(t *testing.T) *fixture {
mw := mock_service.NewMockClientCommandsServer(t)
mwMock := mock_apicore.NewMockClientCommands(t)
spaceService := space.NewService(mw)
spaceService := space.NewService(mwMock)
spaceService.AccountInfo = &model.AccountInfo{TechSpaceId: techSpaceId, GatewayUrl: gatewayUrl}
objectService := object.NewService(mw, spaceService)
objectService := object.NewService(mwMock, spaceService)
objectService.AccountInfo = &model.AccountInfo{TechSpaceId: techSpaceId}
searchService := NewService(mw, spaceService, objectService)
searchService := NewService(mwMock, spaceService, objectService)
searchService.AccountInfo = &model.AccountInfo{
TechSpaceId: techSpaceId,
GatewayUrl: gatewayUrl,
@ -63,7 +58,7 @@ func newFixture(t *testing.T) *fixture {
return &fixture{
SearchService: searchService,
mwMock: mw,
mwMock: mwMock,
}
}
@ -79,7 +74,7 @@ func TestSearchService_GlobalSearch(t *testing.T) {
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyLayout.String(),
RelationKey: bundle.RelationKeyResolvedLayout.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.Int64(int64(model.ObjectType_spaceView)),
},
@ -98,7 +93,7 @@ func TestSearchService_GlobalSearch(t *testing.T) {
EmptyPlacement: model.BlockContentDataviewSort_End,
},
},
Keys: []string{bundle.RelationKeyTargetSpaceId.String(), bundle.RelationKeyName.String(), bundle.RelationKeyIconEmoji.String(), bundle.RelationKeyIconImage.String()},
Keys: []string{bundle.RelationKeyTargetSpaceId.String()},
}).Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{
{
@ -112,15 +107,32 @@ func TestSearchService_GlobalSearch(t *testing.T) {
// Mock workspace opening
fx.mwMock.On("WorkspaceOpen", mock.Anything, &pb.RpcWorkspaceOpenRequest{
SpaceId: mockedSpaceId,
WithChat: true,
SpaceId: mockedSpaceId,
}).Return(&pb.RpcWorkspaceOpenResponse{
Info: &model.AccountInfo{
TechSpaceId: mockedSpaceId,
WorkspaceObjectId: "workspace-object-id",
},
Error: &pb.RpcWorkspaceOpenResponseError{Code: pb.RpcWorkspaceOpenResponseError_NULL},
}).Once()
// Mock object show of workspace
fx.mwMock.On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: "workspace-object-id",
}).Return(&pb.RpcObjectShowResponse{
ObjectView: &model.ObjectView{
Details: []*model.ObjectViewDetailsSet{
{
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String("Space Name"),
},
},
},
},
},
}, nil).Once()
// Mock objects in space
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
@ -130,7 +142,7 @@ func TestSearchService_GlobalSearch(t *testing.T) {
NestedFilters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyLayout.String(),
RelationKey: bundle.RelationKeyResolvedLayout.String(),
Condition: model.BlockContentDataviewFilter_In,
Value: pbtypes.IntList([]int{
int(model.ObjectType_basic),
@ -149,6 +161,12 @@ func TestSearchService_GlobalSearch(t *testing.T) {
Condition: model.BlockContentDataviewFilter_NotEqual,
Value: pbtypes.Bool(true),
},
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: "type.uniqueKey",
Condition: model.BlockContentDataviewFilter_NotEqual,
Value: pbtypes.String("ot-template"),
},
{
Operator: model.BlockContentDataviewFilter_Or,
NestedFilters: []*model.BlockContentDataviewFilter{
@ -197,10 +215,10 @@ func TestSearchService_GlobalSearch(t *testing.T) {
ObjectId: mockedObjectId,
}).Return(&pb.RpcObjectShowResponse{
ObjectView: &model.ObjectView{
RootId: mockedRootId,
RootId: mockedObjectId,
Blocks: []*model.Block{
{
Id: mockedRootId,
Id: mockedObjectId,
Restrictions: &model.BlockRestrictions{
Read: false,
Edit: false,
@ -233,13 +251,13 @@ func TestSearchService_GlobalSearch(t *testing.T) {
},
Details: []*model.ObjectViewDetailsSet{
{
Id: mockedRootId,
Id: mockedObjectId,
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String(mockedObjectId),
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectName),
bundle.RelationKeyLayout.String(): pbtypes.Int64(int64(model.ObjectType_basic)),
bundle.RelationKeyIconEmoji.String(): pbtypes.String("🌐"),
bundle.RelationKeyResolvedLayout.String(): pbtypes.Int64(int64(model.ObjectType_basic)),
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedObjectIcon),
bundle.RelationKeyLastModifiedDate.String(): pbtypes.Float64(999999),
bundle.RelationKeyLastModifiedBy.String(): pbtypes.String(mockedParticipantId),
bundle.RelationKeyCreatedDate.String(): pbtypes.Float64(888888),
@ -276,108 +294,114 @@ func TestSearchService_GlobalSearch(t *testing.T) {
},
},
},
{
Id: mockedType,
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String(mockedType),
},
},
},
},
RelationLinks: []*model.RelationLink{
{
Key: bundle.RelationKeyLastModifiedDate.String(),
Format: model.RelationFormat_date,
},
{
Key: bundle.RelationKeyLastModifiedBy.String(),
Format: model.RelationFormat_object,
},
{
Key: bundle.RelationKeyCreatedDate.String(),
Format: model.RelationFormat_date,
},
{
Key: bundle.RelationKeyCreator.String(),
Format: model.RelationFormat_object,
},
{
Key: bundle.RelationKeyTag.String(),
Format: model.RelationFormat_tag,
},
},
},
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
}, nil).Once()
// Mock type resolution
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyId.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(mockedType),
},
},
Keys: []string{bundle.RelationKeyName.String()},
}).Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{
{
Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectTypeName),
// Mock tag-1 open
fx.mwMock.On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedTagId1,
}).Return(&pb.RpcObjectShowResponse{
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
ObjectView: &model.ObjectView{
Details: []*model.ObjectViewDetailsSet{
{
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String(mockedTagValue1),
bundle.RelationKeyRelationOptionColor.String(): pbtypes.String(mockedTagColor1),
},
},
},
},
},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
}, nil).Once()
// Mock participant details
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyId.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(mockedParticipantId),
},
},
Keys: []string{bundle.RelationKeyId.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyIconEmoji.String(),
bundle.RelationKeyIconImage.String(),
bundle.RelationKeyIdentity.String(),
bundle.RelationKeyGlobalName.String(),
bundle.RelationKeyParticipantPermissions.String(),
},
}).Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{
{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String(mockedParticipantId),
bundle.RelationKeyName.String(): pbtypes.String(mockedParticipantName),
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedParticipantIcon),
bundle.RelationKeyIconImage.String(): pbtypes.String(mockedParticipantImage),
bundle.RelationKeyIdentity.String(): pbtypes.String(mockedParticipantIdentity),
bundle.RelationKeyGlobalName.String(): pbtypes.String(mockedParticipantGlobalName),
bundle.RelationKeyParticipantPermissions.String(): pbtypes.Int64(int64(model.ParticipantPermissions_Reader)),
// Mock tag-2 open
fx.mwMock.On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedTagId2,
}).Return(&pb.RpcObjectShowResponse{
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
ObjectView: &model.ObjectView{
Details: []*model.ObjectViewDetailsSet{
{
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String(mockedTagValue2),
bundle.RelationKeyRelationOptionColor.String(): pbtypes.String(mockedTagColor2),
},
},
},
},
},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Twice()
}, nil).Once()
// when
objects, total, hasMore, err := fx.GlobalSearch(ctx, SearchRequest{Query: mockedSearchTerm, Types: []string{}, Sort: SortOptions{Direction: "desc", Timestamp: "last_modified_date"}}, offset, limit)
objects, total, hasMore, err := fx.GlobalSearch(ctx, SearchRequest{Query: mockedSearchTerm, Types: []string{}, Sort: SortOptions{Property: LastModifiedDate, Direction: Desc}}, offset, limit)
// then
require.NoError(t, err)
require.Len(t, objects, 1)
require.Equal(t, mockedObjectTypeName, objects[0].Type)
require.Equal(t, mockedSpaceId, objects[0].SpaceId)
require.Equal(t, mockedObjectName, objects[0].Name)
require.Equal(t, mockedObjectId, objects[0].Id)
require.Equal(t, mockedObjectName, objects[0].Name)
require.Equal(t, mockedType, objects[0].Type.Id)
require.Equal(t, mockedSpaceId, objects[0].SpaceId)
require.Equal(t, model.ObjectTypeLayout_name[int32(model.ObjectType_basic)], objects[0].Layout)
require.Equal(t, "🌐", objects[0].Icon)
require.Equal(t, util.Icon{Format: "emoji", Emoji: util.StringPtr(mockedObjectIcon)}, objects[0].Icon)
require.Equal(t, "This is a sample text block", objects[0].Blocks[2].Text.Text)
// check details
for _, detail := range objects[0].Details {
if detail.Id == "created_date" {
require.Equal(t, "1970-01-11T06:54:48Z", detail.Details["created_date"])
} else if detail.Id == "last_modified_date" {
require.Equal(t, "1970-01-12T13:46:39Z", detail.Details["last_modified_date"])
} else if detail.Id == "created_by" {
require.Equal(t, mockedParticipantId, detail.Details["details"].(space.Member).Id)
require.Equal(t, mockedParticipantName, detail.Details["details"].(space.Member).Name)
require.Equal(t, gatewayUrl+"/image/"+mockedParticipantImage, detail.Details["details"].(space.Member).Icon)
require.Equal(t, mockedParticipantIdentity, detail.Details["details"].(space.Member).Identity)
require.Equal(t, mockedParticipantGlobalName, detail.Details["details"].(space.Member).GlobalName)
} else if detail.Id == "last_modified_by" {
require.Equal(t, mockedParticipantId, detail.Details["details"].(space.Member).Id)
for _, property := range objects[0].Properties {
if property.Id == "created_date" {
require.Equal(t, "1970-01-11T06:54:48Z", *property.Date)
} else if property.Id == "last_modified_date" {
require.Equal(t, "1970-01-12T13:46:39Z", *property.Date)
} else if property.Id == "created_by" {
require.Equal(t, []string{mockedParticipantId}, property.Object)
} else if property.Id == "last_modified_by" {
require.Equal(t, []string{mockedParticipantId}, property.Object)
}
}
// check tags
tags := []object.Tag{}
for _, detail := range objects[0].Details {
if tagList, ok := detail.Details["tags"].([]object.Tag); ok {
for _, tag := range tagList {
tags = append(tags, tag)
}
for _, detail := range objects[0].Properties {
for _, tag := range detail.MultiSelect {
tags = append(tags, tag)
}
}
require.Len(t, tags, 2)
@ -391,6 +415,44 @@ func TestSearchService_GlobalSearch(t *testing.T) {
require.Equal(t, 1, total)
require.False(t, hasMore)
})
t.Run("no objects found globally", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// when
objects, total, hasMore, err := fx.GlobalSearch(ctx, SearchRequest{Query: mockedSearchTerm, Types: []string{}, Sort: SortOptions{Property: LastModifiedDate, Direction: Desc}}, offset, limit)
// then
require.NoError(t, err)
require.Len(t, objects, 0)
require.Equal(t, 0, total)
require.False(t, hasMore)
})
t.Run("error during global search", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).Return(&pb.RpcObjectSearchResponse{
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_UNKNOWN_ERROR},
}).Once()
// when
objects, total, hasMore, err := fx.GlobalSearch(ctx, SearchRequest{Query: mockedSearchTerm, Types: []string{}, Sort: SortOptions{Property: LastModifiedDate, Direction: Desc}}, offset, limit)
// then
require.Error(t, err)
require.Empty(t, objects)
require.Equal(t, 0, total)
require.False(t, hasMore)
})
}
func TestSearchService_Search(t *testing.T) {
@ -408,7 +470,7 @@ func TestSearchService_Search(t *testing.T) {
NestedFilters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyLayout.String(),
RelationKey: bundle.RelationKeyResolvedLayout.String(),
Condition: model.BlockContentDataviewFilter_In,
Value: pbtypes.IntList([]int{
int(model.ObjectType_basic),
@ -427,6 +489,12 @@ func TestSearchService_Search(t *testing.T) {
Condition: model.BlockContentDataviewFilter_NotEqual,
Value: pbtypes.Bool(true),
},
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: "type.uniqueKey",
Condition: model.BlockContentDataviewFilter_NotEqual,
Value: pbtypes.String("ot-template"),
},
{
Operator: model.BlockContentDataviewFilter_Or,
NestedFilters: []*model.BlockContentDataviewFilter{
@ -474,80 +542,43 @@ func TestSearchService_Search(t *testing.T) {
ObjectId: mockedObjectId,
}).Return(&pb.RpcObjectShowResponse{
ObjectView: &model.ObjectView{
RootId: mockedRootId,
RootId: mockedObjectId,
Details: []*model.ObjectViewDetailsSet{
{
Id: mockedObjectId,
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String(mockedObjectId),
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectName),
bundle.RelationKeyLayout.String(): pbtypes.Int64(int64(model.ObjectType_basic)),
bundle.RelationKeyResolvedLayout.String(): pbtypes.Int64(int64(model.ObjectType_basic)),
bundle.RelationKeyLastModifiedDate.String(): pbtypes.Float64(999999),
bundle.RelationKeySpaceId.String(): pbtypes.String(mockedSpaceId),
bundle.RelationKeyType.String(): pbtypes.String(mockedType),
},
},
},
{
Id: mockedType,
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String(mockedType),
},
},
},
},
},
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
}).Once()
// Mock type resolution
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyId.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(mockedType),
},
},
Keys: []string{bundle.RelationKeyName.String()},
}).Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{
{
Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectTypeName),
},
},
},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// Mock participant details
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyId.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(""),
},
},
Keys: []string{bundle.RelationKeyId.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyIconEmoji.String(),
bundle.RelationKeyIconImage.String(),
bundle.RelationKeyIdentity.String(),
bundle.RelationKeyGlobalName.String(),
bundle.RelationKeyParticipantPermissions.String(),
},
}).Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Twice()
// when
objects, total, hasMore, err := fx.Search(ctx, mockedSpaceId, SearchRequest{Query: mockedSearchTerm, Types: []string{}, Sort: SortOptions{Direction: "desc", Timestamp: "last_modified_date"}}, offset, limit)
objects, total, hasMore, err := fx.Search(ctx, mockedSpaceId, SearchRequest{Query: mockedSearchTerm, Types: []string{}, Sort: SortOptions{Property: LastModifiedDate, Direction: Desc}}, offset, limit)
// then
require.NoError(t, err)
require.Len(t, objects, 1)
require.Equal(t, mockedObjectName, objects[0].Name)
require.Equal(t, mockedObjectId, objects[0].Id)
require.Equal(t, mockedObjectName, objects[0].Name)
require.Equal(t, mockedType, objects[0].Type.Id)
require.Equal(t, mockedSpaceId, objects[0].SpaceId)
require.Equal(t, model.ObjectTypeLayout_name[int32(model.ObjectType_basic)], objects[0].Layout)
@ -559,14 +590,13 @@ func TestSearchService_Search(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// when
objects, total, hasMore, err := fx.Search(ctx, mockedSpaceId, SearchRequest{Query: mockedSearchTerm, Types: []string{}, Sort: SortOptions{Direction: "desc", Timestamp: "last_modified_date"}}, offset, limit)
objects, total, hasMore, err := fx.Search(ctx, mockedSpaceId, SearchRequest{Query: mockedSearchTerm, Types: []string{}, Sort: SortOptions{Property: LastModifiedDate, Direction: Desc}}, offset, limit)
// then
require.NoError(t, err)
@ -585,7 +615,7 @@ func TestSearchService_Search(t *testing.T) {
}).Once()
// when
objects, total, hasMore, err := fx.Search(ctx, mockedSpaceId, SearchRequest{Query: mockedSearchTerm, Types: []string{}, Sort: SortOptions{Direction: "desc", Timestamp: "last_modified_date"}}, offset, limit)
objects, total, hasMore, err := fx.Search(ctx, mockedSpaceId, SearchRequest{Query: mockedSearchTerm, Types: []string{}, Sort: SortOptions{Property: LastModifiedDate, Direction: Desc}}, offset, limit)
// then
require.Error(t, err)

View file

@ -11,16 +11,17 @@ import (
// GetSpacesHandler retrieves a list of spaces
//
// @Summary List spaces
// @Tags spaces
// @Accept json
// @Produce json
// @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[Space] "List of spaces"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /spaces [get]
// @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.
// @Tags spaces
// @Produce json
// @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[Space] "List of spaces"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces [get]
func GetSpacesHandler(s *SpaceService) gin.HandlerFunc {
return func(c *gin.Context) {
offset := c.GetInt("offset")
@ -30,6 +31,7 @@ func GetSpacesHandler(s *SpaceService) gin.HandlerFunc {
code := util.MapErrorCode(err,
util.ErrToCode(ErrFailedListSpaces, http.StatusInternalServerError),
util.ErrToCode(ErrFailedOpenWorkspace, http.StatusInternalServerError),
util.ErrToCode(ErrFailedOpenSpace, http.StatusInternalServerError),
)
if code != http.StatusOK {
@ -42,30 +44,28 @@ func GetSpacesHandler(s *SpaceService) gin.HandlerFunc {
}
}
// CreateSpaceHandler creates a new space
// GetSpaceHandler retrieves a space
//
// @Summary Create space
// @Tags spaces
// @Accept json
// @Produce json
// @Param name body CreateSpaceRequest true "Space to create"
// @Success 200 {object} CreateSpaceResponse "Space created successfully"
// @Failure 400 {object} util.ValidationError "Bad request"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /spaces [post]
func CreateSpaceHandler(s *SpaceService) 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.
// @Tags spaces
// @Produce json
// @Param space_id path string true "Space ID"
// @Success 200 {object} SpaceResponse "Space"
// @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]
func GetSpaceHandler(s *SpaceService) gin.HandlerFunc {
return func(c *gin.Context) {
nameRequest := CreateSpaceRequest{}
if err := c.BindJSON(&nameRequest); err != nil {
apiErr := util.CodeToAPIError(http.StatusBadRequest, err.Error())
c.JSON(http.StatusBadRequest, apiErr)
return
}
spaceId := c.Param("space_id")
space, err := s.CreateSpace(c.Request.Context(), nameRequest.Name)
space, err := s.GetSpace(c.Request.Context(), spaceId)
code := util.MapErrorCode(err,
util.ErrToCode(ErrFailedCreateSpace, http.StatusInternalServerError),
util.ErrToCode(ErrWorkspaceNotFound, http.StatusNotFound),
util.ErrToCode(ErrFailedOpenWorkspace, http.StatusInternalServerError),
util.ErrToCode(ErrFailedOpenSpace, http.StatusInternalServerError),
)
if code != http.StatusOK {
@ -74,23 +74,66 @@ func CreateSpaceHandler(s *SpaceService) gin.HandlerFunc {
return
}
c.JSON(http.StatusOK, CreateSpaceResponse{Space: space})
c.JSON(http.StatusOK, SpaceResponse{Space: space})
}
}
// CreateSpaceHandler creates a new space
//
// @Summary Create space
// @Description Creates a new workspace (or space) based on a supplied name 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 space.
// @Tags spaces
// @Accept json
// @Produce json
// @Param name body CreateSpaceRequest true "Space to create"
// @Success 200 {object} SpaceResponse "Space created successfully"
// @Failure 400 {object} util.ValidationError "Bad request"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 423 {object} util.RateLimitError "Rate limit exceeded"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces [post]
func CreateSpaceHandler(s *SpaceService) gin.HandlerFunc {
return func(c *gin.Context) {
var req CreateSpaceRequest
if err := c.BindJSON(&req); err != nil {
apiErr := util.CodeToAPIError(http.StatusBadRequest, err.Error())
c.JSON(http.StatusBadRequest, apiErr)
return
}
space, err := s.CreateSpace(c.Request.Context(), req)
code := util.MapErrorCode(err,
util.ErrToCode(ErrFailedCreateSpace, http.StatusInternalServerError),
util.ErrToCode(ErrFailedSetSpaceInfo, http.StatusInternalServerError),
util.ErrToCode(ErrFailedOpenWorkspace, http.StatusInternalServerError),
util.ErrToCode(ErrFailedOpenSpace, http.StatusInternalServerError),
)
if code != http.StatusOK {
apiErr := util.CodeToAPIError(code, err.Error())
c.JSON(code, apiErr)
return
}
c.JSON(http.StatusOK, SpaceResponse{Space: space})
}
}
// GetMembersHandler retrieves a list of members in a space
//
// @Summary List members
// @Tags spaces
// @Accept json
// @Produce json
// @Param space_id path string true "Space ID"
// @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[Member] "List of members"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /spaces/{space_id}/members [get]
// @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.
// @Tags members
// @Produce json
// @Param space_id path string true "Space ID"
// @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[Member] "List of members"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/members [get]
func GetMembersHandler(s *SpaceService) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -111,3 +154,38 @@ func GetMembersHandler(s *SpaceService) gin.HandlerFunc {
pagination.RespondWithPagination(c, http.StatusOK, members, total, offset, limit, hasMore)
}
}
// GetMemberHandler retrieves a member in a space
//
// @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.
// @Tags members
// @Produce json
// @Param space_id path string true "Space ID"
// @Param member_id path string true "Member ID or Identity"
// @Success 200 {object} MemberResponse "Member"
// @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]
func GetMemberHandler(s *SpaceService) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
memberId := c.Param("member_id")
member, err := s.GetMember(c.Request.Context(), spaceId, memberId)
code := util.MapErrorCode(err,
util.ErrToCode(ErrMemberNotFound, http.StatusNotFound),
util.ErrToCode(ErrFailedGetMember, http.StatusInternalServerError),
)
if code != http.StatusOK {
apiErr := util.CodeToAPIError(code, err.Error())
c.JSON(code, apiErr)
return
}
c.JSON(http.StatusOK, MemberResponse{Member: member})
}
}

View file

@ -1,41 +1,44 @@
package space
type CreateSpaceRequest struct {
Name string `json:"name" example:"New Space"`
import (
"github.com/anyproto/anytype-heart/core/api/util"
)
type SpaceResponse struct {
Space Space `json:"space"` // The space
}
type CreateSpaceResponse struct {
Space Space `json:"space"`
type CreateSpaceRequest struct {
Name string `json:"name" example:"New Space"` // The name of the space
Description string `json:"description" example:"The local-first wiki"` // The description of the space
}
type Space struct {
Type string `json:"type" example:"space"`
Id string `json:"id" example:"bafyreigyfkt6rbv24sbv5aq2hko3bhmv5xxlf22b4bypdu6j7hnphm3psq.23me69r569oi1"`
Name string `json:"name" example:"Space Name"`
Icon string `json:"icon" example:"http://127.0.0.1:31006/image/bafybeieptz5hvcy6txplcvphjbbh5yjc2zqhmihs3owkh5oab4ezauzqay"`
HomeObjectId string `json:"home_object_id" example:"bafyreie4qcl3wczb4cw5hrfyycikhjyh6oljdis3ewqrk5boaav3sbwqya"`
ArchiveObjectId string `json:"archive_object_id" example:"bafyreialsgoyflf3etjm3parzurivyaukzivwortf32b4twnlwpwocsrri"`
ProfileObjectId string `json:"profile_object_id" example:"bafyreiaxhwreshjqwndpwtdsu4mtihaqhhmlygqnyqpfyfwlqfq3rm3gw4"`
MarketplaceWorkspaceId string `json:"marketplace_workspace_id" example:"_anytype_marketplace"`
WorkspaceObjectId string `json:"workspace_object_id" example:"bafyreiapey2g6e6za4zfxvlgwdy4hbbfu676gmwrhnqvjbxvrchr7elr3y"`
DeviceId string `json:"device_id" example:"12D3KooWGZMJ4kQVyQVXaj7gJPZr3RZ2nvd9M2Eq2pprEoPih9WF"`
AccountSpaceId string `json:"account_space_id" example:"bafyreihpd2knon5wbljhtfeg3fcqtg3i2pomhhnigui6lrjmzcjzep7gcy.23me69r569oi1"`
WidgetsId string `json:"widgets_id" example:"bafyreialj7pceh53mifm5dixlho47ke4qjmsn2uh4wsjf7xq2pnlo5xfva"`
SpaceViewId string `json:"space_view_id" example:"bafyreigzv3vq7qwlrsin6njoduq727ssnhwd6bgyfj6nm4hv3pxoc2rxhy"`
TechSpaceId string `json:"tech_space_id" example:"bafyreif4xuwncrjl6jajt4zrrfnylpki476nv2w64yf42ovt7gia7oypii.23me69r569oi1"`
GatewayUrl string `json:"gateway_url" example:"http://127.0.0.1:31006"`
LocalStoragePath string `json:"local_storage_path" example:"/Users/johndoe/Library/Application Support/Anytype/data/AAHTtt1wuQEnaYBNZ2Cyfcvs6DqPqxgn8VXDVk4avsUkMuha"`
Timezone string `json:"timezone" example:""`
AnalyticsId string `json:"analytics_id" example:"624aecdd-4797-4611-9d61-a2ae5f53cf1c"`
NetworkId string `json:"network_id" example:"N83gJpVd9MuNRZAuJLZ7LiMntTThhPc6DtzWWVjb1M3PouVU"`
Object string `json:"object" example:"space"` // The data model of the object
Id string `json:"id" example:"bafyreigyfkt6rbv24sbv5aq2hko3bhmv5xxlf22b4bypdu6j7hnphm3psq.23me69r569oi1"` // The id of the space
Name string `json:"name" example:"My Space"` // The name of the space
Icon util.Icon `json:"icon"` // The icon of the space
Description string `json:"description" example:"The local-first wiki"` // The description of the space
GatewayUrl string `json:"gateway_url" example:"http://127.0.0.1:31006"` // The gateway url to serve files and media
NetworkId string `json:"network_id" example:"N83gJpVd9MuNRZAuJLZ7LiMntTThhPc6DtzWWVjb1M3PouVU"` // The network id of the space
}
type MemberResponse struct {
Member Member `json:"member"` // The member
}
type UpdateMemberRequest struct {
Status string `json:"status" enums:"active,removed,declined" example:"active"` // Status of the member
Role string `json:"role,omitempty" enums:"viewer,editor" example:"viewer"` // Role to assign if approving a joining member
}
type Member struct {
Type string `json:"type" example:"member"`
Id string `json:"id" example:"_participant_bafyreigyfkt6rbv24sbv5aq2hko1bhmv5xxlf22b4bypdu6j7hnphm3psq_23me69r569oi1_AAjEaEwPF4nkEh9AWkqEnzcQ8HziBB4ETjiTpvRCQvWnSMDZ"`
Name string `json:"name" example:"John Doe"`
Icon string `json:"icon" example:"http://127.0.0.1:31006/image/bafybeieptz5hvcy6txplcvphjbbh5yjc2zqhmihs3owkh5oab4ezauzqay?width=100"`
Identity string `json:"identity" example:"AAjEaEwPF4nkEh7AWkqEnzcQ8HziGB4ETjiTpvRCQvWnSMDZ"`
GlobalName string `json:"global_name" example:"john.any"`
Role string `json:"role" enums:"Reader,Writer,Owner,NoPermission" example:"Owner"`
Object string `json:"object" example:"member"` // The data model of the object
Id string `json:"id" example:"_participant_bafyreigyfkt6rbv24sbv5aq2hko1bhmv5xxlf22b4bypdu6j7hnphm3psq_23me69r569oi1_AAjEaEwPF4nkEh9AWkqEnzcQ8HziBB4ETjiTpvRCQvWnSMDZ"` // The profile object id of the member
Name string `json:"name" example:"John Doe"` // The name of the member
Icon util.Icon `json:"icon"` // The icon of the member
Identity string `json:"identity" example:"AAjEaEwPF4nkEh7AWkqEnzcQ8HziGB4ETjiTpvRCQvWnSMDZ"` // The identity of the member in the network
GlobalName string `json:"global_name" example:"john.any"` // The global name of the member in the network
Status string `json:"status" enums:"joining,active,removed,declined,removing,canceled" example:"active"` // The status of the member
Role string `json:"role" enums:"viewer,editor,owner,no_permission" example:"owner"` // The role of the member
}

View file

@ -5,38 +5,51 @@ import (
"crypto/rand"
"errors"
"math/big"
"strings"
"github.com/gogo/protobuf/types"
"github.com/iancoleman/strcase"
"github.com/anyproto/anytype-heart/core/api/apicore"
"github.com/anyproto/anytype-heart/core/api/pagination"
"github.com/anyproto/anytype-heart/core/api/util"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pb/service"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
var (
ErrFailedListSpaces = errors.New("failed to retrieve list of spaces")
ErrFailedOpenWorkspace = errors.New("failed to open workspace")
ErrFailedGenerateRandomIcon = errors.New("failed to generate random icon")
ErrFailedCreateSpace = errors.New("failed to create space")
ErrFailedListMembers = errors.New("failed to retrieve list of members")
ErrFailedListSpaces = errors.New("failed to retrieve list of spaces")
ErrFailedOpenWorkspace = errors.New("failed to open workspace")
ErrFailedOpenSpace = errors.New("failed to open space")
ErrWorkspaceNotFound = errors.New("workspace not found")
ErrFailedGenerateRandomIcon = errors.New("failed to generate random icon")
ErrFailedCreateSpace = errors.New("failed to create space")
ErrFailedSetSpaceInfo = errors.New("failed to set space info")
ErrFailedListMembers = errors.New("failed to retrieve list of members")
ErrFailedGetMember = errors.New("failed to retrieve member")
ErrMemberNotFound = errors.New("member not found")
ErrInvalidApproveMemberStatus = errors.New("status must be 'active', 'declined', or 'removed'")
ErrInvalidApproveMemberRole = errors.New("role must be 'reader' or 'writer'")
ErrFailedUpdateMember = errors.New("failed to update member")
)
type Service interface {
ListSpaces(ctx context.Context, offset int, limit int) ([]Space, int, bool, error)
CreateSpace(ctx context.Context, name string) (Space, error)
GetSpace(ctx context.Context, spaceId string) (Space, error)
CreateSpace(ctx context.Context, request CreateSpaceRequest) (Space, error)
ListMembers(ctx context.Context, spaceId string, offset int, limit int) ([]Member, int, bool, error)
GetMember(ctx context.Context, spaceId string, memberId string) (Member, error)
UpdateMember(ctx context.Context, spaceId string, memberId string, request UpdateMemberRequest) (Member, error)
}
type SpaceService struct {
mw service.ClientCommandsServer
mw apicore.ClientCommands
AccountInfo *model.AccountInfo
}
func NewService(mw service.ClientCommandsServer) *SpaceService {
func NewService(mw apicore.ClientCommands) *SpaceService {
return &SpaceService{mw: mw}
}
@ -47,7 +60,7 @@ func (s *SpaceService) ListSpaces(ctx context.Context, offset int, limit int) (s
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyLayout.String(),
RelationKey: bundle.RelationKeyResolvedLayout.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.Int64(int64(model.ObjectType_spaceView)),
},
@ -66,7 +79,7 @@ func (s *SpaceService) ListSpaces(ctx context.Context, offset int, limit int) (s
EmptyPlacement: model.BlockContentDataviewSort_End,
},
},
Keys: []string{bundle.RelationKeyTargetSpaceId.String(), bundle.RelationKeyName.String(), bundle.RelationKeyIconEmoji.String(), bundle.RelationKeyIconImage.String()},
Keys: []string{bundle.RelationKeyTargetSpaceId.String()},
})
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
@ -78,34 +91,63 @@ func (s *SpaceService) ListSpaces(ctx context.Context, offset int, limit int) (s
spaces = make([]Space, 0, len(paginatedRecords))
for _, record := range paginatedRecords {
workspace, err := s.getWorkspaceInfo(record.Fields[bundle.RelationKeyTargetSpaceId.String()].GetStringValue())
workspace, err := s.getSpaceInfo(record.Fields[bundle.RelationKeyTargetSpaceId.String()].GetStringValue())
if err != nil {
return nil, 0, false, err
}
// TODO: name and icon are only returned here; fix that
workspace.Name = record.Fields[bundle.RelationKeyName.String()].GetStringValue()
workspace.Icon = util.GetIconFromEmojiOrImage(s.AccountInfo, record.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(), record.Fields[bundle.RelationKeyIconImage.String()].GetStringValue())
spaces = append(spaces, workspace)
}
return spaces, total, hasMore, nil
}
// GetSpace returns the space info for the space with the given ID.
func (s *SpaceService) GetSpace(ctx context.Context, spaceId string) (Space, error) {
// Check if the workspace exists and is active
resp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
SpaceId: s.AccountInfo.TechSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyTargetSpaceId.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(spaceId),
},
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeySpaceLocalStatus.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.Int64(int64(model.SpaceStatus_Ok)),
},
},
Keys: []string{bundle.RelationKeyTargetSpaceId.String()},
})
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
return Space{}, ErrFailedOpenWorkspace
}
if len(resp.Records) == 0 {
return Space{}, ErrWorkspaceNotFound
}
return s.getSpaceInfo(spaceId)
}
// CreateSpace creates a new space with the given name and returns the space info.
func (s *SpaceService) CreateSpace(ctx context.Context, name string) (Space, error) {
func (s *SpaceService) CreateSpace(ctx context.Context, request CreateSpaceRequest) (Space, error) {
name := request.Name
iconOption, err := rand.Int(rand.Reader, big.NewInt(13))
if err != nil {
return Space{}, ErrFailedGenerateRandomIcon
}
// Create new workspace with a random icon and import default use case
resp := s.mw.WorkspaceCreate(ctx, &pb.RpcWorkspaceCreateRequest{
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyIconOption.String(): pbtypes.Float64(float64(iconOption.Int64())),
bundle.RelationKeyName.String(): pbtypes.String(name),
bundle.RelationKeyIconOption.String(): pbtypes.Float64(float64(iconOption.Int64())),
bundle.RelationKeySpaceDashboardId.String(): pbtypes.String("lastOpened"),
},
},
@ -113,21 +155,37 @@ func (s *SpaceService) CreateSpace(ctx context.Context, name string) (Space, err
WithChat: true,
})
if resp.Error.Code != pb.RpcWorkspaceCreateResponseError_NULL {
if resp.Error != nil && resp.Error.Code != pb.RpcWorkspaceCreateResponseError_NULL {
return Space{}, ErrFailedCreateSpace
}
return s.getWorkspaceInfo(resp.SpaceId)
description := request.Description
if description != "" {
infoResp := s.mw.WorkspaceSetInfo(ctx, &pb.RpcWorkspaceSetInfoRequest{
SpaceId: resp.SpaceId,
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyDescription.String(): pbtypes.String(description),
},
},
})
if infoResp.Error != nil && infoResp.Error.Code != pb.RpcWorkspaceSetInfoResponseError_NULL {
return Space{}, ErrFailedSetSpaceInfo
}
}
return s.getSpaceInfo(resp.SpaceId)
}
// ListMembers returns a paginated list of members in the space with the given ID.
func (s *SpaceService) ListMembers(ctx context.Context, spaceId string, offset int, limit int) (members []Member, total int, hasMore bool, err error) {
resp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
activeResp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
SpaceId: spaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyLayout.String(),
RelationKey: bundle.RelationKeyResolvedLayout.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.Int64(int64(model.ObjectType_participant)),
},
@ -144,28 +202,62 @@ func (s *SpaceService) ListMembers(ctx context.Context, spaceId string, offset i
Type: model.BlockContentDataviewSort_Asc,
},
},
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeyName.String(), bundle.RelationKeyIconEmoji.String(), bundle.RelationKeyIconImage.String(), bundle.RelationKeyIdentity.String(), bundle.RelationKeyGlobalName.String(), bundle.RelationKeyParticipantPermissions.String()},
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeyName.String(), bundle.RelationKeyIconEmoji.String(), bundle.RelationKeyIconImage.String(), bundle.RelationKeyIdentity.String(), bundle.RelationKeyGlobalName.String(), bundle.RelationKeyParticipantPermissions.String(), bundle.RelationKeyParticipantStatus.String()},
})
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
if activeResp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
return nil, 0, false, ErrFailedListMembers
}
total = len(resp.Records)
paginatedMembers, hasMore := pagination.Paginate(resp.Records, offset, limit)
joiningResp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
SpaceId: spaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyResolvedLayout.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.Int64(int64(model.ObjectType_participant)),
},
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyParticipantStatus.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.Int64(int64(model.ParticipantStatus_Joining)),
},
},
Sorts: []*model.BlockContentDataviewSort{
{
RelationKey: bundle.RelationKeyName.String(),
Type: model.BlockContentDataviewSort_Asc,
},
},
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeyName.String(), bundle.RelationKeyIconEmoji.String(), bundle.RelationKeyIconImage.String(), bundle.RelationKeyIdentity.String(), bundle.RelationKeyGlobalName.String(), bundle.RelationKeyParticipantPermissions.String(), bundle.RelationKeyParticipantStatus.String()},
})
if joiningResp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
return nil, 0, false, ErrFailedListMembers
}
combinedRecords := make([]*types.Struct, 0, len(joiningResp.Records)+len(activeResp.Records))
combinedRecords = append(combinedRecords, joiningResp.Records...)
combinedRecords = append(combinedRecords, activeResp.Records...)
total = len(combinedRecords)
paginatedMembers, hasMore := pagination.Paginate(combinedRecords, offset, limit)
members = make([]Member, 0, len(paginatedMembers))
for _, record := range paginatedMembers {
icon := util.GetIconFromEmojiOrImage(s.AccountInfo, record.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(), record.Fields[bundle.RelationKeyIconImage.String()].GetStringValue())
icon := util.GetIcon(s.AccountInfo, record.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(), record.Fields[bundle.RelationKeyIconImage.String()].GetStringValue(), "", 0)
member := Member{
Type: "member",
Object: "member",
Id: record.Fields[bundle.RelationKeyId.String()].GetStringValue(),
Name: record.Fields[bundle.RelationKeyName.String()].GetStringValue(),
Icon: icon,
Identity: record.Fields[bundle.RelationKeyIdentity.String()].GetStringValue(),
GlobalName: record.Fields[bundle.RelationKeyGlobalName.String()].GetStringValue(),
Role: model.ParticipantPermissions_name[int32(record.Fields[bundle.RelationKeyParticipantPermissions.String()].GetNumberValue())],
Status: strcase.ToSnake(model.ParticipantStatus_name[int32(record.Fields[bundle.RelationKeyParticipantStatus.String()].GetNumberValue())]),
Role: s.mapMemberPermissions(model.ParticipantPermissions(record.Fields[bundle.RelationKeyParticipantPermissions.String()].GetNumberValue())),
}
members = append(members, member)
@ -174,69 +266,170 @@ func (s *SpaceService) ListMembers(ctx context.Context, spaceId string, offset i
return members, total, hasMore, nil
}
func (s *SpaceService) GetParticipantDetails(mw service.ClientCommandsServer, spaceId string, participantId string) Member {
resp := mw.ObjectSearch(context.Background(), &pb.RpcObjectSearchRequest{
// GetMember returns the member with the given ID in the space with the given ID.
func (s *SpaceService) GetMember(ctx context.Context, spaceId string, memberId string) (Member, error) {
// Member ID can be either a participant ID or an identity.
relationKey := bundle.RelationKeyId
if !strings.HasPrefix(memberId, "_participant") {
relationKey = bundle.RelationKeyIdentity
}
resp := s.mw.ObjectSearch(context.Background(), &pb.RpcObjectSearchRequest{
SpaceId: spaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyId.String(),
RelationKey: relationKey.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(participantId),
Value: pbtypes.String(memberId),
},
},
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeyName.String(), bundle.RelationKeyIconEmoji.String(), bundle.RelationKeyIconImage.String(), bundle.RelationKeyIdentity.String(), bundle.RelationKeyGlobalName.String(), bundle.RelationKeyParticipantPermissions.String()},
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeyName.String(), bundle.RelationKeyIconEmoji.String(), bundle.RelationKeyIconImage.String(), bundle.RelationKeyIdentity.String(), bundle.RelationKeyGlobalName.String(), bundle.RelationKeyParticipantPermissions.String(), bundle.RelationKeyParticipantStatus.String()},
})
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
return Member{}
return Member{}, ErrFailedGetMember
}
if len(resp.Records) == 0 {
return Member{}
return Member{}, ErrMemberNotFound
}
icon := util.GetIconFromEmojiOrImage(s.AccountInfo, "", resp.Records[0].Fields[bundle.RelationKeyIconImage.String()].GetStringValue())
icon := util.GetIcon(s.AccountInfo, "", resp.Records[0].Fields[bundle.RelationKeyIconImage.String()].GetStringValue(), "", 0)
return Member{
Type: "member",
Object: "member",
Id: resp.Records[0].Fields[bundle.RelationKeyId.String()].GetStringValue(),
Name: resp.Records[0].Fields[bundle.RelationKeyName.String()].GetStringValue(),
Icon: icon,
Identity: resp.Records[0].Fields[bundle.RelationKeyIdentity.String()].GetStringValue(),
GlobalName: resp.Records[0].Fields[bundle.RelationKeyGlobalName.String()].GetStringValue(),
Role: model.ParticipantPermissions_name[int32(resp.Records[0].Fields[bundle.RelationKeyParticipantPermissions.String()].GetNumberValue())],
}
Status: strcase.ToSnake(model.ParticipantStatus_name[int32(resp.Records[0].Fields[bundle.RelationKeyParticipantStatus.String()].GetNumberValue())]),
Role: s.mapMemberPermissions(model.ParticipantPermissions(resp.Records[0].Fields[bundle.RelationKeyParticipantPermissions.String()].GetNumberValue())),
}, nil
}
// getWorkspaceInfo returns the workspace info for the space with the given ID.
func (s *SpaceService) getWorkspaceInfo(spaceId string) (space Space, err error) {
// UpdateMember approves member with defined role or removes them
func (s *SpaceService) UpdateMember(ctx context.Context, spaceId string, memberId string, request UpdateMemberRequest) (Member, error) {
member, err := s.GetMember(ctx, spaceId, memberId)
if err != nil {
return Member{}, err
}
if request.Status != "active" && request.Status != "removed" && request.Status != "declined" {
return Member{}, ErrInvalidApproveMemberStatus
}
switch request.Status {
case "active":
if request.Role != "viewer" && request.Role != "editor" {
return Member{}, ErrInvalidApproveMemberRole
}
if member.Status == "joining" {
// Approve the member's join request.
approveResp := s.mw.SpaceRequestApprove(ctx, &pb.RpcSpaceRequestApproveRequest{
SpaceId: spaceId,
Identity: memberId,
Permissions: s.mapMemberRole(request.Role),
})
if approveResp.Error.Code != pb.RpcSpaceRequestApproveResponseError_NULL {
return Member{}, ErrFailedUpdateMember
}
} else {
// Update the member's role.
resp := s.mw.SpaceParticipantPermissionsChange(ctx, &pb.RpcSpaceParticipantPermissionsChangeRequest{
SpaceId: spaceId,
Changes: []*model.ParticipantPermissionChange{{Identity: memberId, Perms: s.mapMemberRole(request.Role)}},
})
if resp.Error.Code != pb.RpcSpaceParticipantPermissionsChangeResponseError_NULL {
return Member{}, ErrFailedUpdateMember
}
}
case "declined":
// Reject the member's join request.
rejectResp := s.mw.SpaceRequestDecline(ctx, &pb.RpcSpaceRequestDeclineRequest{
SpaceId: spaceId,
Identity: memberId,
})
if rejectResp.Error.Code != pb.RpcSpaceRequestDeclineResponseError_NULL {
return Member{}, ErrFailedUpdateMember
}
case "removed":
// Remove the member from the space.
removeResp := s.mw.SpaceParticipantRemove(ctx, &pb.RpcSpaceParticipantRemoveRequest{
SpaceId: spaceId,
Identities: []string{memberId},
})
if removeResp.Error.Code != pb.RpcSpaceParticipantRemoveResponseError_NULL {
return Member{}, ErrFailedUpdateMember
}
default:
return Member{}, ErrInvalidApproveMemberStatus
}
member, err = s.GetMember(ctx, spaceId, memberId)
if err != nil {
return Member{}, err
}
return member, nil
}
// getSpaceInfo returns the workspace info for the space with the given ID.
func (s *SpaceService) getSpaceInfo(spaceId string) (space Space, err error) {
workspaceResponse := s.mw.WorkspaceOpen(context.Background(), &pb.RpcWorkspaceOpenRequest{
SpaceId: spaceId,
WithChat: true,
SpaceId: spaceId,
})
if workspaceResponse.Error.Code != pb.RpcWorkspaceOpenResponseError_NULL {
if workspaceResponse.Error != nil && workspaceResponse.Error.Code != pb.RpcWorkspaceOpenResponseError_NULL {
return Space{}, ErrFailedOpenWorkspace
}
spaceResp := s.mw.ObjectShow(context.Background(), &pb.RpcObjectShowRequest{
SpaceId: spaceId,
ObjectId: workspaceResponse.Info.WorkspaceObjectId,
})
if spaceResp.Error != nil && spaceResp.Error.Code != pb.RpcObjectShowResponseError_NULL {
return Space{}, ErrFailedOpenSpace
}
name := spaceResp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyName.String()].GetStringValue()
icon := util.GetIcon(s.AccountInfo, 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 Space{
Type: "space",
Id: spaceId,
HomeObjectId: workspaceResponse.Info.HomeObjectId,
ArchiveObjectId: workspaceResponse.Info.ArchiveObjectId,
ProfileObjectId: workspaceResponse.Info.ProfileObjectId,
MarketplaceWorkspaceId: workspaceResponse.Info.MarketplaceWorkspaceId,
WorkspaceObjectId: workspaceResponse.Info.WorkspaceObjectId,
DeviceId: workspaceResponse.Info.DeviceId,
AccountSpaceId: workspaceResponse.Info.AccountSpaceId,
WidgetsId: workspaceResponse.Info.WidgetsId,
SpaceViewId: workspaceResponse.Info.SpaceViewId,
TechSpaceId: workspaceResponse.Info.TechSpaceId,
GatewayUrl: workspaceResponse.Info.GatewayUrl,
LocalStoragePath: workspaceResponse.Info.LocalStoragePath,
Timezone: workspaceResponse.Info.TimeZone,
AnalyticsId: workspaceResponse.Info.AnalyticsId,
NetworkId: workspaceResponse.Info.NetworkId,
Object: "space",
Id: spaceId,
Name: name,
Icon: icon,
Description: description,
GatewayUrl: workspaceResponse.Info.GatewayUrl,
NetworkId: workspaceResponse.Info.NetworkId,
}, nil
}
// mapMemberPermissions maps participant permissions to a role
func (s *SpaceService) mapMemberPermissions(permissions model.ParticipantPermissions) string {
switch permissions {
case model.ParticipantPermissions_Reader:
return "viewer"
case model.ParticipantPermissions_Writer:
return "editor"
default:
return strcase.ToSnake(model.ParticipantPermissions_name[int32(permissions)])
}
}
// mapMemberPermissions maps a role to participant permissions
func (s *SpaceService) mapMemberRole(role string) model.ParticipantPermissions {
switch role {
case "viewer":
return model.ParticipantPermissions_Reader
case "editor":
return model.ParticipantPermissions_Writer
default:
return model.ParticipantPermissions_Reader
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,13 @@
package pagination
type PaginationMeta struct {
Total int `json:"total" example:"1024"` // the total number of items available on that endpoint
Offset int `json:"offset" example:"0"` // the current offset
Limit int `json:"limit" example:"100"` // the current limit
HasMore bool `json:"has_more" example:"true"` // whether there are more items available
Total int `json:"total" example:"1024"` // The total number of items available for the endpoint
Offset int `json:"offset" example:"0"` // The number of items skipped before starting to collect the result set
Limit int `json:"limit" example:"100"` // The maximum number of items returned in the result set
HasMore bool `json:"has_more" example:"true"` // Indicates if there are more items available beyond the current result set
}
type PaginatedResponse[T any] struct {
Data []T `json:"data"`
Pagination PaginationMeta `json:"pagination"`
Data []T `json:"data"` // The list of items in the current result set
Pagination PaginationMeta `json:"pagination"` // The pagination metadata for the response
}

View file

@ -2,6 +2,7 @@ package server
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
@ -10,9 +11,15 @@ import (
"github.com/didip/tollbooth/v8/limiter"
"github.com/gin-gonic/gin"
"github.com/anyproto/anytype-heart/core/anytype/account"
"github.com/anyproto/anytype-heart/core/api/apicore"
"github.com/anyproto/anytype-heart/core/api/util"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pb/service"
)
var (
ErrMissingAuthorizationHeader = errors.New("missing authorization header")
ErrInvalidAuthorizationHeader = errors.New("invalid authorization header format")
ErrInvalidToken = errors.New("invalid token")
)
// rateLimit is a middleware that limits the number of requests per second.
@ -26,7 +33,8 @@ func (s *Server) rateLimit(max float64) gin.HandlerFunc {
return func(c *gin.Context) {
httpError := tollbooth.LimitByRequest(lmt, c.Writer, c.Request)
if httpError != nil {
c.AbortWithStatusJSON(httpError.StatusCode, gin.H{"error": httpError.Message})
apiErr := util.CodeToAPIError(httpError.StatusCode, httpError.Message)
c.AbortWithStatusJSON(httpError.StatusCode, apiErr)
return
}
c.Next()
@ -34,16 +42,18 @@ func (s *Server) rateLimit(max float64) gin.HandlerFunc {
}
// ensureAuthenticated is a middleware that ensures the request is authenticated.
func (s *Server) ensureAuthenticated(mw service.ClientCommandsServer) gin.HandlerFunc {
func (s *Server) ensureAuthenticated(mw apicore.ClientCommands) gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Missing Authorization header"})
apiErr := util.CodeToAPIError(http.StatusUnauthorized, ErrMissingAuthorizationHeader.Error())
c.AbortWithStatusJSON(http.StatusUnauthorized, apiErr)
return
}
if !strings.HasPrefix(authHeader, "Bearer ") {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization header format"})
apiErr := util.CodeToAPIError(http.StatusUnauthorized, ErrInvalidAuthorizationHeader.Error())
c.AbortWithStatusJSON(http.StatusUnauthorized, apiErr)
return
}
key := strings.TrimPrefix(authHeader, "Bearer ")
@ -57,7 +67,8 @@ func (s *Server) ensureAuthenticated(mw service.ClientCommandsServer) gin.Handle
if !exists {
response := mw.WalletCreateSession(context.Background(), &pb.RpcWalletCreateSessionRequest{Auth: &pb.RpcWalletCreateSessionRequestAuthOfAppKey{AppKey: key}})
if response.Error.Code != pb.RpcWalletCreateSessionResponseError_NULL {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
apiErr := util.CodeToAPIError(http.StatusUnauthorized, ErrInvalidToken.Error())
c.AbortWithStatusJSON(http.StatusUnauthorized, apiErr)
return
}
token = response.Token
@ -74,15 +85,15 @@ func (s *Server) ensureAuthenticated(mw service.ClientCommandsServer) gin.Handle
}
// ensureAccountInfo is a middleware that ensures the account info is available in the services.
func (s *Server) ensureAccountInfo(accountService account.Service) gin.HandlerFunc {
func (s *Server) ensureAccountInfo(accountService apicore.AccountService) gin.HandlerFunc {
return func(c *gin.Context) {
accInfo, err := accountService.GetInfo(context.Background())
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to get account info: %v", err)})
apiErr := util.CodeToAPIError(http.StatusInternalServerError, fmt.Sprintf("failed to get account info: %v", err))
c.AbortWithStatusJSON(http.StatusInternalServerError, apiErr)
return
}
s.exportService.AccountInfo = accInfo
s.objectService.AccountInfo = accInfo
s.spaceService.AccountInfo = accInfo
s.searchService.AccountInfo = accInfo
@ -90,3 +101,11 @@ func (s *Server) ensureAccountInfo(accountService account.Service) gin.HandlerFu
c.Next()
}
}
// ensureMetadataHeader is a middleware that ensures the metadata header is set.
func (s *Server) ensureMetadataHeader() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Anytype-Version", "2025-03-17")
c.Next()
}
}

View file

@ -0,0 +1,214 @@
package server
import (
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"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"
)
func TestEnsureMetadataHeader(t *testing.T) {
t.Run("sets correct header", func(t *testing.T) {
// given
fx := newFixture(t)
middleware := fx.ensureMetadataHeader()
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
// when
middleware(c)
// then
require.Equal(t, "2025-03-17", w.Header().Get("Anytype-Version"))
})
}
func TestEnsureAuthenticated(t *testing.T) {
t.Run("missing auth header", func(t *testing.T) {
// given
fx := newFixture(t)
fx.KeyToToken = make(map[string]string)
middleware := fx.ensureAuthenticated(fx.mwMock)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
req := httptest.NewRequest("GET", "/", nil)
c.Request = req
// when
middleware(c)
// then
require.Equal(t, http.StatusUnauthorized, w.Code)
expectedJSON, err := json.Marshal(util.CodeToAPIError(http.StatusUnauthorized, ErrMissingAuthorizationHeader.Error()))
require.NoError(t, err)
require.JSONEq(t, string(expectedJSON), w.Body.String())
})
t.Run("invalid auth header format", func(t *testing.T) {
// given
fx := newFixture(t)
fx.KeyToToken = make(map[string]string)
middleware := fx.ensureAuthenticated(fx.mwMock)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Authorization", "InvalidToken")
c.Request = req
// when
middleware(c)
// then
require.Equal(t, http.StatusUnauthorized, w.Code)
expectedJSON, err := json.Marshal(util.CodeToAPIError(http.StatusUnauthorized, ErrInvalidAuthorizationHeader.Error()))
require.NoError(t, err)
require.JSONEq(t, string(expectedJSON), w.Body.String())
})
t.Run("valid token creation", func(t *testing.T) {
// given
fx := newFixture(t)
fx.KeyToToken = make(map[string]string)
tokenExpected := "valid-token"
fx.mwMock.
On("WalletCreateSession", mock.Anything, &pb.RpcWalletCreateSessionRequest{
Auth: &pb.RpcWalletCreateSessionRequestAuthOfAppKey{AppKey: "someAppKey"},
}).
Return(&pb.RpcWalletCreateSessionResponse{
Token: tokenExpected,
Error: &pb.RpcWalletCreateSessionResponseError{
Code: pb.RpcWalletCreateSessionResponseError_NULL,
},
}, nil).Once()
middleware := fx.ensureAuthenticated(fx.mwMock)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Authorization", "Bearer someAppKey")
c.Request = req
// when
middleware(c)
// then
token, exists := c.Get("token")
require.True(t, exists)
require.Equal(t, tokenExpected, token)
})
t.Run("invalid token", func(t *testing.T) {
// given
fx := newFixture(t)
fx.KeyToToken = make(map[string]string)
middleware := fx.ensureAuthenticated(fx.mwMock)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Authorization", "Bearer invalidKey")
c.Request = req
fx.mwMock.
On("WalletCreateSession", mock.Anything, &pb.RpcWalletCreateSessionRequest{
Auth: &pb.RpcWalletCreateSessionRequestAuthOfAppKey{AppKey: "invalidKey"},
}).
Return(&pb.RpcWalletCreateSessionResponse{
Token: "",
Error: &pb.RpcWalletCreateSessionResponseError{
Code: pb.RpcWalletCreateSessionResponseError_UNKNOWN_ERROR,
},
}, nil).Once()
// when
middleware(c)
// then
require.Equal(t, http.StatusUnauthorized, w.Code)
expectedJSON, err := json.Marshal(util.CodeToAPIError(http.StatusUnauthorized, ErrInvalidToken.Error()))
require.NoError(t, err)
require.JSONEq(t, string(expectedJSON), w.Body.String())
})
}
func TestEnsureAccountInfo(t *testing.T) {
t.Run("successful account info", func(t *testing.T) {
// given
fx := newFixture(t)
expectedInfo := &model.AccountInfo{
GatewayUrl: "http://localhost:31006",
}
fx.accountService.On("GetInfo", mock.Anything).Return(expectedInfo, nil).Once()
// when
middleware := fx.ensureAccountInfo(&fx.accountService)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
// when
middleware(c)
// then
require.Equal(t, expectedInfo, fx.objectService.AccountInfo)
require.Equal(t, expectedInfo, fx.spaceService.AccountInfo)
require.Equal(t, expectedInfo, fx.searchService.AccountInfo)
})
t.Run("error retrieving account info", func(t *testing.T) {
// given
fx := newFixture(t)
expectedErr := errors.New("failed to get info")
fx.accountService.On("GetInfo", mock.Anything).Return(nil, expectedErr).Once()
middleware := fx.ensureAccountInfo(&fx.accountService)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
middleware(c)
// then
require.Equal(t, http.StatusInternalServerError, w.Code)
})
}
func TestRateLimit(t *testing.T) {
fx := newFixture(t)
router := gin.New()
router.GET("/", fx.rateLimit(1), func(c *gin.Context) {
c.String(http.StatusOK, "OK")
})
t.Run("first request allowed", func(t *testing.T) {
// given
w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/", nil)
req.RemoteAddr = "1.2.3.4:5678"
// when
router.ServeHTTP(w, req)
// then
require.Equal(t, http.StatusOK, w.Code)
})
t.Run("second request rate-limited", func(t *testing.T) {
// given
w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/", nil)
req.RemoteAddr = "1.2.3.4:5678"
// when
router.ServeHTTP(w, req)
// then
require.Equal(t, http.StatusTooManyRequests, w.Code)
})
}

View file

@ -7,16 +7,16 @@ import (
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"github.com/anyproto/anytype-heart/core/api/apicore"
_ "github.com/anyproto/anytype-heart/core/api/docs"
"github.com/anyproto/anytype-heart/core/anytype/account"
"github.com/anyproto/anytype-heart/core/api/internal/auth"
"github.com/anyproto/anytype-heart/core/api/internal/export"
"github.com/anyproto/anytype-heart/core/api/internal/list"
"github.com/anyproto/anytype-heart/core/api/internal/object"
"github.com/anyproto/anytype-heart/core/api/internal/search"
"github.com/anyproto/anytype-heart/core/api/internal/space"
"github.com/anyproto/anytype-heart/core/api/pagination"
"github.com/anyproto/anytype-heart/pb/service"
)
const (
@ -28,7 +28,7 @@ const (
)
// NewRouter builds and returns a *gin.Engine with all routes configured.
func (s *Server) NewRouter(accountService account.Service, mw service.ClientCommandsServer) *gin.Engine {
func (s *Server) NewRouter(mw apicore.ClientCommands, accountService apicore.AccountService) *gin.Engine {
debug := os.Getenv("ANYTYPE_API_DEBUG") == "1"
if !debug {
gin.SetMode(gin.ReleaseMode)
@ -36,6 +36,7 @@ func (s *Server) NewRouter(accountService account.Service, mw service.ClientComm
router := gin.New()
router.Use(gin.Recovery())
router.Use(s.ensureMetadataHeader())
if debug {
router.Use(gin.Logger())
@ -64,7 +65,13 @@ func (s *Server) NewRouter(accountService account.Service, mw service.ClientComm
v1.Use(s.ensureAccountInfo(accountService))
{
// Export
v1.POST("/spaces/:space_id/objects/:object_id/export/:format", export.GetObjectExportHandler(s.exportService))
v1.GET("/spaces/:space_id/objects/:object_id/:format", export.GetObjectExportHandler(s.exportService))
// List
v1.GET("/spaces/:space_id/lists/:list_id/views", list.GetListViewsHandler(s.listService))
v1.GET("/spaces/:space_id/lists/:list_id/:view_id/objects", list.GetObjectsInListHandler(s.listService))
v1.POST("/spaces/:space_id/lists/:list_id/objects", list.AddObjectsToListHandler(s.listService))
v1.DELETE("/spaces/:space_id/lists/:list_id/objects/:object_id", s.rateLimit(maxWriteRequestsPerSecond), list.RemoveObjectFromListHandler(s.listService))
// Object
v1.GET("/spaces/:space_id/objects", object.GetObjectsHandler(s.objectService))
@ -78,7 +85,10 @@ func (s *Server) NewRouter(accountService account.Service, mw service.ClientComm
// Space
v1.GET("/spaces", space.GetSpacesHandler(s.spaceService))
v1.GET("/spaces/:space_id", space.GetSpaceHandler(s.spaceService))
v1.GET("/spaces/:space_id/members", space.GetMembersHandler(s.spaceService))
v1.GET("/spaces/:space_id/members/:member_id", space.GetMemberHandler(s.spaceService))
// v1.PATCH("/spaces/:space_id/members/:member_id", s.rateLimit(maxWriteRequestsPerSecond), space.UpdateMemberHandler(s.spaceService))
v1.POST("/spaces", s.rateLimit(maxWriteRequestsPerSecond), space.CreateSpaceHandler(s.spaceService))
// Type

View file

@ -0,0 +1,74 @@
package server
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gogo/protobuf/types"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
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, &fx.accountService)
w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/v1/spaces", nil)
// when
engine.ServeHTTP(w, req)
// then
require.Equal(t, http.StatusUnauthorized, w.Code)
})
}
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, &fx.accountService)
w := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/v1/auth/token", nil)
// when
engine.ServeHTTP(w, req)
// then
require.NotEqual(t, http.StatusUnauthorized, w.Code)
})
}
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.accountService)
fx.KeyToToken = map[string]string{"validKey": "dummyToken"}
fx.accountService.On("GetInfo", mock.Anything).
Return(&model.AccountInfo{
GatewayUrl: "http://localhost:31006",
}, nil).Once()
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).
Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}, nil).Once()
w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/v1/spaces", nil)
req.Header.Set("Authorization", "Bearer validKey")
// when
engine.ServeHTTP(w, req)
// then
require.Equal(t, "2025-03-17", w.Header().Get("Anytype-Version"))
})
}

View file

@ -5,13 +5,13 @@ import (
"github.com/gin-gonic/gin"
"github.com/anyproto/anytype-heart/core/anytype/account"
"github.com/anyproto/anytype-heart/core/api/apicore"
"github.com/anyproto/anytype-heart/core/api/internal/auth"
"github.com/anyproto/anytype-heart/core/api/internal/export"
"github.com/anyproto/anytype-heart/core/api/internal/list"
"github.com/anyproto/anytype-heart/core/api/internal/object"
"github.com/anyproto/anytype-heart/core/api/internal/search"
"github.com/anyproto/anytype-heart/core/api/internal/space"
"github.com/anyproto/anytype-heart/pb/service"
)
// Server wraps the HTTP server and service logic.
@ -20,6 +20,7 @@ type Server struct {
authService *auth.AuthService
exportService *export.ExportService
listService *list.ListService
objectService *object.ObjectService
spaceService *space.SpaceService
searchService *search.SearchService
@ -29,16 +30,17 @@ type Server struct {
}
// NewServer constructs a new Server with default config and sets up the routes.
func NewServer(accountService account.Service, mw service.ClientCommandsServer) *Server {
func NewServer(mw apicore.ClientCommands, accountService apicore.AccountService, exportService apicore.ExportService) *Server {
s := &Server{
authService: auth.NewService(mw),
exportService: export.NewService(mw),
exportService: export.NewService(mw, exportService),
spaceService: space.NewService(mw),
}
s.objectService = object.NewService(mw, s.spaceService)
s.listService = list.NewService(mw, s.objectService)
s.searchService = search.NewService(mw, s.spaceService, s.objectService)
s.engine = s.NewRouter(accountService, mw)
s.engine = s.NewRouter(mw, accountService)
s.KeyToToken = make(map[string]string)
return s

View file

@ -0,0 +1,63 @@
package server
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/anyproto/anytype-heart/core/api/apicore/mock_apicore"
)
type fixture struct {
*Server
accountService mock_apicore.MockAccountService
exportService mock_apicore.MockExportService
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)
server := NewServer(mwMock, accountService, exportService)
return &fixture{
Server: server,
accountService: *accountService,
exportService: *exportService,
mwMock: mwMock,
}
}
func TestNewServer(t *testing.T) {
t.Run("returns valid server", func(t *testing.T) {
// when
s := newFixture(t)
// then
require.NotNil(t, s)
require.NotNil(t, s.engine)
require.NotNil(t, s.KeyToToken)
require.NotNil(t, s.authService)
require.NotNil(t, s.exportService)
require.NotNil(t, s.spaceService)
require.NotNil(t, s.objectService)
require.NotNil(t, s.listService)
require.NotNil(t, s.searchService)
})
}
func TestServer_Engine(t *testing.T) {
t.Run("Engine returns same engine instance", func(t *testing.T) {
// given
s := newFixture(t)
// when
engine := s.Engine()
// then
require.Equal(t, s.engine, engine)
})
}

View file

@ -12,8 +12,9 @@ import (
"github.com/anyproto/anytype-heart/core/anytype/account"
"github.com/anyproto/anytype-heart/core/anytype/config"
"github.com/anyproto/anytype-heart/core/api/apicore"
"github.com/anyproto/anytype-heart/core/api/server"
"github.com/anyproto/anytype-heart/pb/service"
"github.com/anyproto/anytype-heart/core/block/export"
)
const (
@ -22,7 +23,7 @@ const (
)
var (
mwSrv service.ClientCommandsServer
mwSrv apicore.ClientCommands
)
type Service interface {
@ -33,8 +34,9 @@ type Service interface {
type apiService struct {
srv *server.Server
httpSrv *http.Server
mw service.ClientCommandsServer
accountService account.Service
mw apicore.ClientCommands
accountService apicore.AccountService
exportService apicore.ExportService
listenAddr string
lock sync.Mutex
}
@ -49,23 +51,24 @@ func (s *apiService) Name() (name string) {
// Init initializes the API service.
//
// @title Anytype API
// @version 1.0
// @description This API allows interaction with Anytype resources such as spaces, objects and types.
// @termsOfService https://anytype.io/terms_of_use
// @contact.name Anytype Support
// @contact.url https://anytype.io/contact
// @contact.email support@anytype.io
// @license.name Any Source Available License 1.0
// @license.url https://github.com/anyproto/anytype-ts/blob/main/LICENSE.md
// @host localhost:31009
// @BasePath /v1
// @securityDefinitions.basic BasicAuth
// @externalDocs.description OpenAPI
// @externalDocs.url https://swagger.io/resources/open-api/
// @title Anytype API
// @version 2025-03-17
// @description This API allows interaction with Anytype resources such as spaces, objects and types.
// @termsOfService https://anytype.io/terms_of_use
// @contact.name Anytype Support
// @contact.url https://anytype.io/contact
// @contact.email support@anytype.io
// @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)
return nil
}
@ -86,7 +89,7 @@ func (s *apiService) runServer() {
return
}
s.srv = server.NewServer(s.accountService, s.mw)
s.srv = server.NewServer(s.mw, s.accountService, s.exportService)
s.httpSrv = &http.Server{
Addr: s.listenAddr,
Handler: s.srv.Engine(),
@ -130,6 +133,6 @@ func (s *apiService) ReassignAddress(ctx context.Context, listenAddr string) (er
return nil
}
func SetMiddlewareParams(mw service.ClientCommandsServer) {
func SetMiddlewareParams(mw apicore.ClientCommands) {
mwSrv = mw
}

View file

@ -5,38 +5,52 @@ import (
"net/http"
)
// 400
// ValidationError is a struct for 400 errors
type ValidationError struct {
Error struct {
Message string `json:"message"`
Message string `json:"message" example:"Bad request"`
} `json:"error"`
}
// 401
// UnauthorizedError is a struct for 401 errors
type UnauthorizedError struct {
Error struct {
Message string `json:"message"`
Message string `json:"message" example:"Unauthorized"`
} `json:"error"`
}
// 403
// ForbiddenError is a struct for 403 errors
type ForbiddenError struct {
Error struct {
Message string `json:"message"`
Message string `json:"message" example:"Forbidden"`
} `json:"error"`
}
// 404
// NotFoundError is a struct for 404 errors
type NotFoundError struct {
Error struct {
Message string `json:"message"`
Message string `json:"message" example:"Resource not found"`
} `json:"error"`
}
// 500
// GoneError is a struct for 410 errors
type GoneError struct {
Error struct {
Message string `json:"message" example:"Resource is gone"`
} `json:"error"`
}
// RateLimitError is a struct for 423 errors
type RateLimitError struct {
Error struct {
Message string `json:"message" example:"Rate limit exceeded"`
} `json:"error"`
}
// ServerError is a struct for 500 errors
type ServerError struct {
Error struct {
Message string `json:"message"`
Message string `json:"message" example:"Internal server error"`
} `json:"error"`
}
@ -72,10 +86,11 @@ func MapErrorCode(err error, mappings ...errCodeMapping) int {
// for the given HTTP code, embedding the supplied message.
func CodeToAPIError(code int, message string) any {
switch code {
case http.StatusNotFound:
return NotFoundError{
case http.StatusBadRequest:
return ValidationError{
Error: struct {
Message string `json:"message"`
Message string `json:"message" example:"Bad request"`
}{
Message: message,
},
@ -84,16 +99,34 @@ func CodeToAPIError(code int, message string) any {
case http.StatusUnauthorized:
return UnauthorizedError{
Error: struct {
Message string `json:"message"`
Message string `json:"message" example:"Unauthorized"`
}{
Message: message,
},
}
case http.StatusBadRequest:
return ValidationError{
case http.StatusForbidden:
return ForbiddenError{
Error: struct {
Message string `json:"message"`
Message string `json:"message" example:"Forbidden"`
}{
Message: message,
},
}
case http.StatusNotFound:
return NotFoundError{
Error: struct {
Message string `json:"message" example:"Resource not found"`
}{
Message: message,
},
}
case http.StatusTooManyRequests:
return RateLimitError{
Error: struct {
Message string `json:"message" example:"Rate limit exceeded"`
}{
Message: message,
},
@ -102,7 +135,7 @@ func CodeToAPIError(code int, message string) any {
default:
return ServerError{
Error: struct {
Message string `json:"message"`
Message string `json:"message" example:"Internal server error"`
}{
Message: message,
},

View file

@ -4,68 +4,84 @@ import (
"context"
"errors"
"fmt"
"strings"
"github.com/anyproto/anytype-heart/core/api/apicore"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pb/service"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
var (
ErrFailedSearchType = errors.New("failed to search for type")
ErrorTypeNotFound = errors.New("type not found")
ErrFailedSearchType = errors.New("failed to search for type")
ErrorTypeNotFound = errors.New("type not found")
ErrFailedSearchProperty = errors.New("failed to search for property")
ErrorPropertyNotFound = errors.New("property not found")
)
// GetIconFromEmojiOrImage returns the icon to use for the object, which can be either an emoji or an image url
func GetIconFromEmojiOrImage(accountInfo *model.AccountInfo, iconEmoji string, iconImage string) string {
var iconOptionToColor = map[float64]string{
1: "grey",
2: "yellow",
3: "orange",
4: "red",
5: "pink",
6: "purple",
7: "blue",
8: "ice",
9: "teal",
10: "lime",
}
type IconFormat string
const (
IconFormatEmoji IconFormat = "emoji"
IconFormatFile IconFormat = "file"
IconFormatIcon IconFormat = "icon"
)
type Icon struct {
Format IconFormat `json:"format" enums:"emoji,file,icon" example:"emoji"` // The type of the icon
Emoji *string `json:"emoji,omitempty" example:"📄"` // The emoji of the icon
File *string `json:"file,omitempty" example:"http://127.0.0.1:31006/image/bafybeieptz5hvcy6txplcvphjbbh5yjc2zqhmihs3owkh5oab4ezauzqay"` // The file of the icon
Name *string `json:"name,omitempty" example:"document"` // The name of the icon
Color *string `json:"color,omitempty" example:"red"` // The color of the icon
}
// StringPtr returns a pointer to the string
func StringPtr(s string) *string {
return &s
}
// GetIcon returns the icon to use for the object, which can be builtin icon, emoji or file
func GetIcon(accountInfo *model.AccountInfo, iconEmoji string, iconImage string, iconName string, iconOption float64) Icon {
if iconName != "" {
return Icon{
Format: "icon",
Name: &iconName,
Color: StringPtr(iconOptionToColor[iconOption]),
}
}
if iconEmoji != "" {
return iconEmoji
return Icon{
Format: "emoji",
Emoji: &iconEmoji,
}
}
if iconImage != "" {
return fmt.Sprintf("%s/image/%s", accountInfo.GatewayUrl, iconImage)
return Icon{
Format: "file",
File: StringPtr(fmt.Sprintf("%s/image/%s", accountInfo.GatewayUrl, iconImage)),
}
}
return ""
return Icon{}
}
// ResolveTypeToName resolves the type ID to the name of the type, e.g. "ot-page" to "Page" or "bafyreigyb6l5szohs32ts26ku2j42yd65e6hqy2u3gtzgdwqv6hzftsetu" to "Custom Type"
func ResolveTypeToName(mw service.ClientCommandsServer, spaceId string, typeId string) (typeName string, err error) {
// Can't look up preinstalled types based on relation key, therefore need to use unique key
relKey := bundle.RelationKeyId.String()
if strings.HasPrefix(typeId, "ot-") {
relKey = bundle.RelationKeyUniqueKey.String()
}
// Call ObjectSearch for object of specified type and return the name
resp := mw.ObjectSearch(context.Background(), &pb.RpcObjectSearchRequest{
SpaceId: spaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: relKey,
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(typeId),
},
},
Keys: []string{bundle.RelationKeyName.String()},
})
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
return "", ErrFailedSearchType
}
if len(resp.Records) == 0 {
return "", ErrorTypeNotFound
}
return resp.Records[0].Fields[bundle.RelationKeyName.String()].GetStringValue(), nil
}
func ResolveUniqueKeyToTypeId(mw service.ClientCommandsServer, spaceId string, uniqueKey string) (typeId string, err error) {
// Call ObjectSearch for type with unique key and return the type's ID
// 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{
@ -78,13 +94,59 @@ func ResolveUniqueKeyToTypeId(mw service.ClientCommandsServer, spaceId string, u
Keys: []string{bundle.RelationKeyId.String()},
})
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
return "", ErrFailedSearchType
}
if resp.Error != nil {
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
return "", ErrFailedSearchType
}
if len(resp.Records) == 0 {
return "", ErrorTypeNotFound
if len(resp.Records) == 0 {
return "", ErrorTypeNotFound
}
}
return resp.Records[0].Fields[bundle.RelationKeyId.String()].GetStringValue(), nil
}
// ResolveIdtoUniqueKey resolves the type's ID to the unique key
func ResolveIdtoUniqueKey(mw apicore.ClientCommands, spaceId string, typeId string) (uniqueKey string, err error) {
resp := mw.ObjectShow(context.Background(), &pb.RpcObjectShowRequest{
SpaceId: spaceId,
ObjectId: typeId,
})
if resp.Error != nil && resp.Error.Code != pb.RpcObjectShowResponseError_NULL {
return "", ErrorTypeNotFound
}
return resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyUniqueKey.String()].GetStringValue(), nil
}
// ResolveRelationKeyToPropertyName resolves the property key to the property's name
func ResolveRelationKeyToPropertyName(mw apicore.ClientCommands, spaceId string, relationKey string) (property string, err error) {
resp := mw.ObjectSearch(context.Background(), &pb.RpcObjectSearchRequest{
SpaceId: spaceId,
Filters: []*model.BlockContentDataviewFilter{
{
RelationKey: bundle.RelationKeyRelationKey.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(relationKey),
},
{
RelationKey: bundle.RelationKeyResolvedLayout.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.Int64(int64(model.ObjectType_relation)),
},
},
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeyName.String(), bundle.RelationKeyResolvedLayout.String()},
})
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
return "", ErrFailedSearchProperty
}
if len(resp.Records) == 0 {
return "", ErrorPropertyNotFound
}
return resp.Records[0].Fields[bundle.RelationKeyName.String()].GetStringValue(), nil
}

View file

@ -267,12 +267,15 @@ func (s *service) createFavoriteWidget(spc clientspace.Space) error {
if err != nil {
return fmt.Errorf("get widget details: %w", err)
}
if widgetDetails.GetBool(bundle.RelationKeyAutoWidgetDisabled) {
return nil
}
targetIds := widgetDetails.GetStringList(bundle.RelationKeyAutoWidgetTargets)
if slices.Contains(targetIds, widget.DefaultWidgetFavorite) {
return nil
}
return cache.DoState(s.objectGetter, widgetObjectId, func(st *state.State, w widget.Widget) (err error) {
return w.AddAutoWidget(st, widget.DefaultWidgetFavorite, widget.DefaultWidgetFavorite, "", model.BlockContentWidget_CompactList)
return w.AddAutoWidget(st, widget.DefaultWidgetFavorite, widget.DefaultWidgetFavorite, "", model.BlockContentWidget_CompactList, widget.DefaultWidgetFavoriteEventName)
})
}

View file

@ -90,6 +90,9 @@ func (p *Archive) autoInstallBinWidget() error {
if err != nil {
return err
}
if widgetDetails.GetBool(bundle.RelationKeyAutoWidgetDisabled) {
return nil
}
keys := widgetDetails.Get(bundle.RelationKeyAutoWidgetTargets).StringList()
if slices.Contains(keys, widget.DefaultWidgetBin) {
// cache to avoid unnecessary objectstore requests
@ -100,7 +103,7 @@ func (p *Archive) autoInstallBinWidget() error {
st := sb.NewState()
if w, ok := sb.(widget.Widget); ok {
// We rely on AddAutoWidget to check if the widget was already installed/removed before
err = w.AddAutoWidget(st, widget.DefaultWidgetBin, widget.DefaultWidgetBin, "", model.BlockContentWidget_Link)
err = w.AddAutoWidget(st, widget.DefaultWidgetBin, widget.DefaultWidgetBin, "", model.BlockContentWidget_Link, widget.DefaultWidgetBinEventName)
if err != nil {
return err
}

View file

@ -14,6 +14,7 @@ import (
"github.com/anyproto/anytype-heart/core/block/simple"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/core/session"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/addr"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore/spaceindex"
@ -56,6 +57,7 @@ func (w *WidgetObject) CreationStateMigration(ctx *smartblock.InitContext) migra
return migration.Migration{
Version: 2,
Proc: func(st *state.State) {
// we purposefully do not add the ALl Objects widget here(as in migration3), because for new users we don't want to auto-create it
template.InitTemplate(st,
template.WithEmpty,
template.WithObjectTypes([]domain.TypeKey{bundle.TypeKeyDashboard}),
@ -112,6 +114,28 @@ func (w *WidgetObject) StateMigrations() migration.Migrations {
},
},
{
Version: 3,
Proc: func(s *state.State) {
// add All Objects widget for existing spaces
_, err := w.CreateBlock(s, &pb.RpcBlockCreateWidgetRequest{
ContextId: s.RootId(),
WidgetLayout: model.BlockContentWidget_Link,
Position: model.Block_InnerFirst,
TargetId: s.RootId(),
ViewId: "",
Block: &model.Block{
Id: "allObjects",
Content: &model.BlockContentOfLink{Link: &model.BlockContentLink{
TargetBlockId: widget.DefaultWidgetAll,
}},
},
})
if err != nil {
log.Warnf("all objects migration failed: %s", err.Error())
}
},
},
},
)
}

View file

@ -8,6 +8,7 @@ import (
"github.com/anyproto/anytype-heart/core/block/editor/state"
"github.com/anyproto/anytype-heart/core/block/simple"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/core/event"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
@ -19,15 +20,20 @@ const (
DefaultWidgetRecent = "recent"
DefaultWidgetCollection = "collection"
DefaultWidgetBin = "bin"
DefaultWidgetAll = "allObjects"
DefaultWidgetRecentOpen = "recentOpen"
autoWidgetBlockIdPrefix = "auto_" // in case blockId is specifically provided to avoid bad tree merges
autoWidgetBlockSuffix = "-wrapper" // in case blockId is specifically provided to avoid bad tree merges
DefaultWidgetFavoriteEventName = "Favorite"
DefaultWidgetBinEventName = "Bin"
)
type Widget interface {
CreateBlock(s *state.State, req *pb.RpcBlockCreateWidgetRequest) (string, error)
// AddAutoWidget adds a widget block. If widget with the same targetId was installed/removed before, it will not be added again.
// blockId is optional and used to protect from multi-device conflicts.
AddAutoWidget(s *state.State, targetId, blockId, viewId string, layout model.BlockContentWidgetLayout) error
// if eventName is empty no event is produced
AddAutoWidget(s *state.State, targetId, blockId, viewId string, layout model.BlockContentWidgetLayout, eventName string) error
}
type widget struct {
@ -71,7 +77,11 @@ func NewWidget(sb smartblock.SmartBlock) Widget {
}
}
func (w *widget) AddAutoWidget(st *state.State, targetId, widgetBlockId, viewId string, layout model.BlockContentWidgetLayout) error {
func (w *widget) AddAutoWidget(st *state.State, targetId, widgetBlockId, viewId string, layout model.BlockContentWidgetLayout, eventName string) error {
isDisabled := st.Details().Get(bundle.RelationKeyAutoWidgetDisabled).Bool()
if isDisabled {
return nil
}
targets := st.Details().Get(bundle.RelationKeyAutoWidgetTargets).StringList()
if slices.Contains(targets, targetId) {
return nil
@ -125,7 +135,7 @@ func (w *widget) AddAutoWidget(st *state.State, targetId, widgetBlockId, viewId
position = model.Block_Bottom
}
_, err = w.CreateBlock(st, &pb.RpcBlockCreateWidgetRequest{
_, err = w.createBlock(st, &pb.RpcBlockCreateWidgetRequest{
ContextId: st.RootId(),
ObjectLimit: 6,
WidgetLayout: layout,
@ -138,11 +148,30 @@ func (w *widget) AddAutoWidget(st *state.State, targetId, widgetBlockId, viewId
TargetBlockId: targetId,
}},
},
})
return err
}, true)
if err != nil {
return err
}
if eventName != "" {
msg := event.NewMessage(w.SpaceID(), &pb.EventMessageValueOfSpaceAutoWidgetAdded{
SpaceAutoWidgetAdded: &pb.EventSpaceAutoWidgetAdded{
TargetId: targetId,
TargetName: eventName,
WidgetBlockId: widgetBlockId,
},
})
w.SendEvent([]*pb.EventMessage{msg})
}
return nil
}
func (w *widget) CreateBlock(s *state.State, req *pb.RpcBlockCreateWidgetRequest) (string, error) {
return w.createBlock(s, req, false)
}
func (w *widget) createBlock(s *state.State, req *pb.RpcBlockCreateWidgetRequest, isAutoAdded bool) (string, error) {
if req.Block.Content == nil {
return "", fmt.Errorf("block has no content")
}
@ -158,8 +187,8 @@ func (w *widget) CreateBlock(s *state.State, req *pb.RpcBlockCreateWidgetRequest
}
var wrapperBlockId string
if b.Model().Id != "" {
wrapperBlockId = autoWidgetBlockIdPrefix + b.Model().Id
if b.Model().Id != "" && isAutoAdded {
wrapperBlockId = b.Model().Id + autoWidgetBlockSuffix
}
wrapper := simple.New(&model.Block{
@ -169,9 +198,10 @@ func (w *widget) CreateBlock(s *state.State, req *pb.RpcBlockCreateWidgetRequest
},
Content: &model.BlockContentOfWidget{
Widget: &model.BlockContentWidget{
Layout: req.WidgetLayout,
Limit: req.ObjectLimit,
ViewId: req.ViewId,
Layout: req.WidgetLayout,
Limit: req.ObjectLimit,
ViewId: req.ViewId,
AutoAdded: isAutoAdded,
},
},
})

View file

@ -3,8 +3,10 @@ package export
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"math/rand"
"net/url"
"os"
"path/filepath"
"slices"
@ -39,6 +41,7 @@ import (
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/core/smartblock"
"github.com/anyproto/anytype-heart/pkg/lib/database"
"github.com/anyproto/anytype-heart/pkg/lib/gateway"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/addr"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
"github.com/anyproto/anytype-heart/pkg/lib/logging"
@ -63,12 +66,15 @@ const (
FilesObjects = "filesObjects"
Files = "files"
defaultFileName = "untitled"
)
var log = logging.Logger("anytype-mw-export")
type Export interface {
Export(ctx context.Context, req pb.RpcObjectListExportRequest) (path string, succeed int, err error)
ExportSingleInMemory(ctx context.Context, spaceId string, objectId string, format model.ExportFormat) (res string, err error)
app.Component
}
@ -81,6 +87,7 @@ type export struct {
accountService account.Service
notificationService notifications.Notifications
processService process.Service
gatewayService gateway.Gateway
}
func New() Export {
@ -96,6 +103,7 @@ func (e *export) Init(a *app.App) (err error) {
e.spaceService = app.MustComponent[space.Service](a)
e.accountService = app.MustComponent[account.Service](a)
e.notificationService = app.MustComponent[notifications.Notifications](a)
e.gatewayService, _ = app.GetComponent[gateway.Gateway](a)
return
}
@ -118,6 +126,20 @@ func (e *export) Export(ctx context.Context, req pb.RpcObjectListExportRequest)
return exportCtx.exportObjects(ctx, queue)
}
func (e *export) ExportSingleInMemory(ctx context.Context, spaceId string, objectId string, format model.ExportFormat) (res string, err error) {
req := pb.RpcObjectListExportRequest{
SpaceId: spaceId,
ObjectIds: []string{objectId},
IncludeFiles: true,
Format: format,
IncludeNested: true,
IncludeArchived: true,
}
exportCtx := newExportContext(e, req)
return exportCtx.exportObject(ctx, objectId)
}
func (e *export) finishWithNotification(spaceId string, exportFormat model.ExportFormat, queue process.Queue, err error) {
errCode := model.NotificationExport_NULL
if err != nil {
@ -168,7 +190,7 @@ type exportContext struct {
relations map[string]struct{}
setOfList map[string]struct{}
objectTypes map[string]struct{}
gatewayUrl string
*export
}
@ -190,8 +212,10 @@ func newExportContext(e *export, req pb.RpcObjectListExportRequest) *exportConte
setOfList: make(map[string]struct{}),
objectTypes: make(map[string]struct{}),
relations: make(map[string]struct{}),
export: e,
export: e,
}
if e.gatewayService != nil {
ec.gatewayUrl = "http://" + e.gatewayService.Addr()
}
return ec
}
@ -224,6 +248,41 @@ func (e *exportContext) getStateFilters(id string) *state.Filters {
return nil
}
// exportObject synchronously exports a single object and return the bytes slice
func (e *exportContext) exportObject(ctx context.Context, objectId string) (string, error) {
e.reqIds = []string{objectId}
e.includeArchive = true
err := e.docsForExport(ctx)
if err != nil {
return "", err
}
var docNamer Namer
if e.format == model.Export_Markdown && e.gatewayUrl != "" {
u, err := url.Parse(e.gatewayUrl)
if err != nil {
return "", err
}
docNamer = &deepLinkNamer{gatewayUrl: *u}
} else {
docNamer = newNamer()
}
inMemoryWriter := &InMemoryWriter{fn: docNamer}
err = e.writeDoc(ctx, inMemoryWriter, objectId, e.docs.transformToDetailsMap())
if err != nil {
return "", err
}
for _, v := range inMemoryWriter.data {
if e.format == model.Export_Protobuf {
return base64.StdEncoding.EncodeToString(v), nil
}
return string(v), nil
}
return "", fmt.Errorf("failed to find data in writer")
}
func (e *exportContext) exportObjects(ctx context.Context, queue process.Queue) (string, int, error) {
var (
err error
@ -1258,6 +1317,9 @@ func (fn *namer) Get(path, hash, title, ext string) (name string) {
title = slug.Make(strings.TrimSuffix(title, ext))
name = text.TruncateEllipsized(title, fileLenLimit)
name = strings.TrimSuffix(name, text.TruncateEllipsis)
if name == "" {
name = defaultFileName
}
var (
i = 0
b = 36

View file

@ -4,22 +4,28 @@ import (
"archive/zip"
"fmt"
"io"
"net/url"
"os"
"path"
"path/filepath"
"sync"
"time"
"github.com/anyproto/anytype-heart/pkg/lib/mill"
"github.com/anyproto/anytype-heart/util/anyerror"
)
type writer interface {
Path() string
Namer() *namer
Namer() Namer
WriteFile(filename string, r io.Reader, lastModifiedDate int64) (err error)
Close() (err error)
}
type Namer interface {
Get(path, hash, title, ext string) (name string)
}
func uniqName() string {
return time.Now().Format("Anytype.20060102.150405.99")
}
@ -44,7 +50,7 @@ type dirWriter struct {
m sync.Mutex
}
func (d *dirWriter) Namer() *namer {
func (d *dirWriter) Namer() Namer {
d.m.Lock()
defer d.m.Unlock()
if d.fn == nil {
@ -108,7 +114,7 @@ type zipWriter struct {
fn *namer
}
func (d *zipWriter) Namer() *namer {
func (d *zipWriter) Namer() Namer {
d.m.Lock()
defer d.m.Unlock()
if d.fn == nil {
@ -149,3 +155,66 @@ func (d *zipWriter) Close() (err error) {
func getZipName(path string) string {
return filepath.Join(path, uniqName()+".zip")
}
type InMemoryWriter struct {
data map[string][]byte
fn Namer
m sync.Mutex
}
func (d *InMemoryWriter) Namer() Namer {
return d.fn
}
func (d *InMemoryWriter) Path() string {
return ""
}
func (d *InMemoryWriter) WriteFile(filename string, r io.Reader, lastModifiedDate int64) (err error) {
d.m.Lock()
defer d.m.Unlock()
if d.data == nil {
d.data = make(map[string][]byte)
}
b, err := io.ReadAll(r)
if err != nil {
return
}
d.data[filename] = b
return
}
func (d *InMemoryWriter) Close() (err error) {
return nil
}
func (d *InMemoryWriter) GetData(id string) []byte {
d.m.Lock()
defer d.m.Unlock()
return d.data[id]
}
// deepLinkNamer used to render a single-object export, in md format
type deepLinkNamer struct {
gatewayUrl url.URL
}
func (fn *deepLinkNamer) Get(path, hash, title, ext string) (name string) {
if ext == ".md" {
// object links via deeplink to the app
return "anytype://object?objectId=" + hash
}
// files links via gateway
if fn.gatewayUrl.Host == "" {
return "anytype://object?objectId=" + hash
}
u := fn.gatewayUrl
if mill.IsImageExt(ext) {
u.Path = "image/" + hash
} else {
u.Path = "file/" + hash
}
return u.String()
}

View file

@ -75,3 +75,22 @@ func TestZipWriter_WriteFile(t *testing.T) {
}
assert.True(t, found)
}
func TestZipWriter_Get(t *testing.T) {
t.Run("file without name", func(t *testing.T) {
// given
path, err := ioutil.TempDir("", "")
require.NoError(t, err)
defer os.RemoveAll(path)
wr, err := newZipWriter(path, uniqName()+".zip")
require.NoError(t, err)
// when
name := wr.Namer().Get(Files, "hash", "", "")
// then
require.NoError(t, wr.Close())
assert.Equal(t, filepath.Join(Files, defaultFileName), name)
})
}

View file

@ -12,7 +12,6 @@ import (
"github.com/anyproto/anytype-heart/core/block/simple"
simpleDataview "github.com/anyproto/anytype-heart/core/block/simple/dataview"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
sb "github.com/anyproto/anytype-heart/pkg/lib/core/smartblock"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
@ -22,25 +21,52 @@ type ImportCollectionSetting struct {
collectionName string
targetObjects []string
icon string
fileKeys []*pb.ChangeFileKeys
needToAddDate, shouldBeFavorite, shouldAddRelations bool
}
func MakeImportCollectionSetting(
collectionName string,
targetObjects []string,
icon string,
fileKeys []*pb.ChangeFileKeys,
needToAddDate, shouldBeFavorite, shouldAddRelations bool,
) *ImportCollectionSetting {
return &ImportCollectionSetting{
collectionName: collectionName,
targetObjects: targetObjects,
icon: icon,
fileKeys: fileKeys,
needToAddDate: needToAddDate,
shouldBeFavorite: shouldBeFavorite,
shouldAddRelations: shouldAddRelations,
type ImportCollectionOption func(*ImportCollectionSetting)
func NewImportCollectionSetting(opts ...ImportCollectionOption) *ImportCollectionSetting {
s := &ImportCollectionSetting{}
for _, opt := range opts {
opt(s)
}
return s
}
func WithCollectionName(name string) ImportCollectionOption {
return func(s *ImportCollectionSetting) {
s.collectionName = name
}
}
func WithTargetObjects(objs []string) ImportCollectionOption {
return func(s *ImportCollectionSetting) {
s.targetObjects = objs
}
}
func WithIcon(icon string) ImportCollectionOption {
return func(s *ImportCollectionSetting) {
s.icon = icon
}
}
func WithAddDate() ImportCollectionOption {
return func(s *ImportCollectionSetting) {
s.needToAddDate = true
}
}
func WithFavorite() ImportCollectionOption {
return func(s *ImportCollectionSetting) {
s.shouldBeFavorite = true
}
}
func WithRelations() ImportCollectionOption {
return func(s *ImportCollectionSetting) {
s.shouldAddRelations = true
}
}
@ -75,14 +101,13 @@ func (r *ImportCollection) MakeImportCollection(req *ImportCollectionSetting) (*
detailsStruct = st.CombinedDetails().Merge(detailsStruct)
st.UpdateStoreSlice(template.CollectionStoreKey, req.targetObjects)
return r.getRootCollectionSnapshot(req.collectionName, st, detailsStruct, req.fileKeys), nil
return r.getRootCollectionSnapshot(req.collectionName, st, detailsStruct), nil
}
func (r *ImportCollection) getRootCollectionSnapshot(
collectionName string,
st *state.State,
detailsStruct *domain.Details,
fileKeys []*pb.ChangeFileKeys,
) *Snapshot {
if detailsStruct == nil {
detailsStruct = domain.NewDetails()
@ -100,7 +125,6 @@ func (r *ImportCollection) getRootCollectionSnapshot(
RelationLinks: st.GetRelationLinks(),
Collections: st.Store(),
},
FileKeys: fileKeys,
},
}
}

View file

@ -0,0 +1,61 @@
package common
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/anyproto/anytype-heart/core/block/collection"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
)
func TestMakeImportCollection(t *testing.T) {
tests := []struct {
name string
needToAddDate bool
shouldBeFavorite bool
shouldAddRelation bool
}{
{"all false", false, false, false},
{"add date", true, false, false},
{"add favorite", false, true, false},
{"add relations", false, false, true},
{"all True", true, true, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
importer := NewImportCollection(collection.New())
req := NewImportCollectionSetting(
WithCollectionName("My Collection"),
WithTargetObjects([]string{"obj1", "obj2"}),
WithIcon("icon.png"),
)
req.needToAddDate = tt.needToAddDate
req.shouldBeFavorite = tt.shouldBeFavorite
req.shouldAddRelations = tt.shouldAddRelation
root, err := importer.MakeImportCollection(req)
assert.NoError(t, err)
assert.NotNil(t, root)
if tt.needToAddDate {
assert.Contains(t, root.FileName, time.Now().Format("2006"))
} else {
assert.Equal(t, "My Collection", root.FileName)
}
if tt.shouldBeFavorite {
assert.Equal(t, domain.Bool(true), root.Snapshot.Data.Details.Get(bundle.RelationKeyIsFavorite))
} else {
assert.Equal(t, domain.Bool(false), root.Snapshot.Data.Details.Get(bundle.RelationKeyIsFavorite))
}
})
}
}

View file

@ -67,7 +67,12 @@ func (c *CSV) GetSnapshots(ctx context.Context, req *pb.RpcObjectImportRequest,
return nil, allErrors
}
rootCollection := common.NewImportCollection(c.collectionService)
settings := common.MakeImportCollectionSetting(rootCollectionName, result.objectIDs, "", nil, true, true, true)
settings := common.NewImportCollectionSetting(
common.WithCollectionName(rootCollectionName),
common.WithTargetObjects(result.objectIDs),
common.WithAddDate(),
common.WithRelations(),
)
rootCol, err := rootCollection.MakeImportCollection(settings)
if err != nil {
allErrors.Add(err)

View file

@ -66,7 +66,12 @@ func (h *HTML) GetSnapshots(ctx context.Context, req *pb.RpcObjectImportRequest,
return nil, allErrors
}
rootCollection := common.NewImportCollection(h.collectionService)
settings := common.MakeImportCollectionSetting(rootCollectionName, targetObjects, "", nil, true, true, true)
settings := common.NewImportCollectionSetting(
common.WithCollectionName(rootCollectionName),
common.WithTargetObjects(targetObjects),
common.WithAddDate(),
common.WithRelations(),
)
rootCollectionSnapshot, err := rootCollection.MakeImportCollection(settings)
if err != nil {
allErrors.Add(err)

View file

@ -70,6 +70,7 @@ type Import struct {
importCtx context.Context
importCtxCancel context.CancelFunc
spaceService space.Service
}
func New() Importer {
@ -82,6 +83,7 @@ func (i *Import) Init(a *app.App) (err error) {
i.s = app.MustComponent[*block.Service](a)
accountService := app.MustComponent[account.Service](a)
spaceService := app.MustComponent[space.Service](a)
i.spaceService = spaceService
col := app.MustComponent[*collection.Service](a)
i.tempDirProvider = app.MustComponent[core.TempDirProvider](a)
converters := []common.Converter{
@ -180,10 +182,33 @@ func (i *Import) importObjects(ctx context.Context, importRequest *ImportRequest
func (i *Import) onImportFinish(res *ImportResponse, req *ImportRequest, importId string) {
i.finishImportProcess(res.Err, req)
i.sendFileEvents(res.Err)
if res.RootCollectionId != "" {
i.addRootCollectionWidget(res, req)
}
i.recordEvent(&metrics.ImportFinishedEvent{ID: importId, ImportType: req.Type.String()})
i.sendImportFinishEventToClient(res.RootCollectionId, req.IsSync, res.ObjectsCount, req.Type)
}
func (i *Import) addRootCollectionWidget(res *ImportResponse, req *ImportRequest) {
spc, err := i.spaceService.Get(i.importCtx, req.SpaceId)
if err != nil {
log.Errorf("failed to create widget from root collection, error: %s", err.Error())
} else {
_, err = i.s.CreateWidgetBlock(nil, &pb.RpcBlockCreateWidgetRequest{
ContextId: spc.DerivedIDs().Widgets,
WidgetLayout: model.BlockContentWidget_CompactList,
Block: &model.Block{
Content: &model.BlockContentOfLink{Link: &model.BlockContentLink{
TargetBlockId: res.RootCollectionId,
}},
},
})
if err != nil {
log.Errorf("failed to create widget from root collection, error: %s", err.Error())
}
}
}
func (i *Import) sendFileEvents(returnedErr error) {
if returnedErr == nil {
i.fileSync.SendImportEvents()

View file

@ -16,6 +16,7 @@ import (
"github.com/anyproto/anytype-heart/core/block/import/web/parsers/mock_parsers"
"github.com/anyproto/anytype-heart/core/block/process"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/space/mock_space"
"github.com/anyproto/anytype-heart/core/block/import/common"
"github.com/anyproto/anytype-heart/core/block/import/common/mock_common"
@ -971,6 +972,9 @@ func Test_ImportRootCollectionInResponse(t *testing.T) {
fileSync.EXPECT().SendImportEvents().Return().Times(1)
fileSync.EXPECT().ClearImportEvents().Return().Times(1)
i.fileSync = fileSync
mockSpaceService := mock_space.NewMockService(t)
mockSpaceService.EXPECT().Get(nil, "space1").Return(nil, fmt.Errorf("not found"))
i.spaceService = mockSpaceService
// when
importRequest := &ImportRequest{

View file

@ -6,6 +6,7 @@ import (
"github.com/gogo/protobuf/types"
"github.com/anyproto/anytype-heart/pkg/lib/mill"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
@ -98,7 +99,6 @@ func ConvertTextToFile(filePath string) *model.BlockContentOfFile {
return nil
}
imageFormats := []string{"jpg", "jpeg", "png", "gif", "webp"}
videoFormats := []string{"mp4", "m4v", "mov"}
audioFormats := []string{"mp3", "ogg", "wav", "m4a", "flac"}
pdfFormat := "pdf"
@ -107,11 +107,8 @@ func ConvertTextToFile(filePath string) *model.BlockContentOfFile {
fileExt := filepath.Ext(filePath)
if fileExt != "" {
fileExt = fileExt[1:]
for _, ext := range imageFormats {
if strings.EqualFold(fileExt, ext) {
fileType = model.BlockContentFile_Image
break
}
if mill.IsImageExt(fileExt) {
fileType = model.BlockContentFile_Image
}
for _, ext := range videoFormats {

View file

@ -102,7 +102,12 @@ func (m *Markdown) processFiles(req *pb.RpcObjectImportRequest, progress process
func (m *Markdown) createRootCollection(allSnapshots []*common.Snapshot, allRootObjectsIds []string) ([]*common.Snapshot, string, error) {
rootCollection := common.NewImportCollection(m.service)
settings := common.MakeImportCollectionSetting(rootCollectionName, allRootObjectsIds, "", nil, true, true, true)
settings := common.NewImportCollectionSetting(
common.WithCollectionName(rootCollectionName),
common.WithTargetObjects(allRootObjectsIds),
common.WithAddDate(),
common.WithRelations(),
)
rootCol, err := rootCollection.MakeImportCollection(settings)
if err != nil {
return nil, "", err
@ -424,7 +429,10 @@ func (m *Markdown) createSnapshots(
func (m *Markdown) addCollectionSnapshot(fileName string, file *FileInfo, snapshots []*common.Snapshot) ([]*common.Snapshot, error) {
c := common.NewImportCollection(m.service)
settings := common.MakeImportCollectionSetting(file.Title, file.CollectionsObjectsIds, "", nil, false, false, false)
settings := common.NewImportCollectionSetting(
common.WithCollectionName(file.Title),
common.WithTargetObjects(file.CollectionsObjectsIds),
)
csvCollection, err := c.MakeImportCollection(settings)
if err != nil {
return nil, err

View file

@ -351,7 +351,12 @@ func (ds *Service) AddObjectsToNotionCollection(notionContext *api.NotionImportC
allObjects := ds.filterObjects(notionContext, notionDB, notionPages)
rootCollection := common.NewImportCollection(ds.collectionService)
settings := common.MakeImportCollectionSetting(rootCollectionName, allObjects, "", nil, true, true, true)
settings := common.NewImportCollectionSetting(
common.WithCollectionName(rootCollectionName),
common.WithTargetObjects(allObjects),
common.WithAddDate(),
common.WithRelations(),
)
rootCol, err := rootCollection.MakeImportCollection(settings)
if err != nil {
return nil, err

View file

@ -2,7 +2,6 @@ package pb
import (
"github.com/globalsign/mgo/bson"
"github.com/samber/lo"
"github.com/anyproto/anytype-heart/core/block/collection"
"github.com/anyproto/anytype-heart/core/block/editor/state"
@ -40,13 +39,9 @@ func (g *GalleryImport) ProvideCollection(snapshots []*common.Snapshot,
if widget != nil {
widgetObjects = g.getObjectsFromWidgets(widget)
}
var (
icon string
fileKeys []*pb.ChangeFileKeys
)
var icon string
if workspaceSnapshot != nil { // we use space icon for import collection
icon = workspaceSnapshot.Snapshot.Data.Details.GetString(bundle.RelationKeyIconImage)
fileKeys = lo.Filter(workspaceSnapshot.Snapshot.FileKeys, func(item *pb.ChangeFileKeys, index int) bool { return item.Hash == icon })
}
collectionName := params.GetCollectionTitle() // collection name should be the name of experience
if collectionName == "" {
@ -54,13 +49,18 @@ func (g *GalleryImport) ProvideCollection(snapshots []*common.Snapshot,
}
rootCollection := common.NewImportCollection(g.service)
if len(widgetObjects) > 0 {
collectionsSnapshots, err = g.getWidgetsCollection(collectionName, rootCollection, widgetObjects, icon, fileKeys, widget, collectionsSnapshots)
collectionsSnapshots, err = g.getWidgetsCollection(collectionName, rootCollection, widgetObjects, icon, widget, collectionsSnapshots)
if err != nil {
return nil, err
}
}
objectsIDs := g.getObjectsIDs(snapshots)
settings := common.MakeImportCollectionSetting(collectionName, objectsIDs, icon, fileKeys, false, true, true)
settings := common.NewImportCollectionSetting(
common.WithCollectionName(collectionName),
common.WithTargetObjects(objectsIDs),
common.WithIcon(icon),
common.WithRelations(),
)
objectsCollection, err := rootCollection.MakeImportCollection(settings)
if err != nil {
return nil, err
@ -73,12 +73,16 @@ func (g *GalleryImport) getWidgetsCollection(collectionName string,
rootCollection *common.ImportCollection,
widgetObjects []string,
icon string,
fileKeys []*pb.ChangeFileKeys,
widget *common.Snapshot,
collectionsSnapshots []*common.Snapshot,
) ([]*common.Snapshot, error) {
widgetCollectionName := collectionName + widgetCollectionPattern
settings := common.MakeImportCollectionSetting(widgetCollectionName, widgetObjects, icon, fileKeys, false, false, true)
settings := common.NewImportCollectionSetting(
common.WithCollectionName(widgetCollectionName),
common.WithTargetObjects(widgetObjects),
common.WithIcon(icon),
common.WithRelations(),
)
widgetsCollectionSnapshot, err := rootCollection.MakeImportCollection(settings)
if err != nil {
return nil, err

View file

@ -56,7 +56,12 @@ func (s *SpaceImport) ProvideCollection(snapshots []*common.Snapshot,
})
}
rootCollection := common.NewImportCollection(s.service)
settings := common.MakeImportCollectionSetting(rootCollectionName, rootObjects, "", nil, true, true, true)
settings := common.NewImportCollectionSetting(
common.WithCollectionName(rootCollectionName),
common.WithTargetObjects(rootObjects),
common.WithRelations(),
common.WithAddDate(),
)
rootCollectionSnapshot, err := rootCollection.MakeImportCollection(settings)
if err != nil {
return nil, err

View file

@ -56,7 +56,12 @@ func (t *TXT) GetSnapshots(ctx context.Context, req *pb.RpcObjectImportRequest,
return nil, allErrors
}
rootCollection := common.NewImportCollection(t.service)
settings := common.MakeImportCollectionSetting(rootCollectionName, targetObjects, "", nil, true, true, true)
settings := common.NewImportCollectionSetting(
common.WithCollectionName(rootCollectionName),
common.WithTargetObjects(targetObjects),
common.WithRelations(),
common.WithAddDate(),
)
rootCol, err := rootCollection.MakeImportCollection(settings)
if err != nil {
allErrors.Add(err)

View file

@ -95,8 +95,12 @@ func (s *Service) CreateTypeWidgetIfMissing(ctx context.Context, spaceId string,
return err
}
widgetObjectId := space.DerivedIDs().Widgets
widgetDetails, err := s.objectStore.SpaceIndex(space.Id()).GetDetails(widgetObjectId)
spaceIndex := s.objectStore.SpaceIndex(space.Id())
widgetDetails, err := spaceIndex.GetDetails(widgetObjectId)
if err == nil {
if widgetDetails.GetBool(bundle.RelationKeyAutoWidgetDisabled) {
return nil
}
keys := widgetDetails.Get(bundle.RelationKeyAutoWidgetTargets).StringList()
if slices.Contains(keys, typeId) {
// widget was created before
@ -104,7 +108,7 @@ func (s *Service) CreateTypeWidgetIfMissing(ctx context.Context, spaceId string,
}
}
// this is not optimal, maybe it should be some cheaper way
records, err := s.objectStore.SpaceIndex(space.Id()).QueryRaw(&database.Filters{FilterObj: database.FiltersAnd{
records, err := spaceIndex.QueryRaw(&database.Filters{FilterObj: database.FiltersAnd{
database.FilterEq{
Key: bundle.RelationKeyType,
Cond: model.BlockContentDataviewFilter_Equal,
@ -128,7 +132,16 @@ func (s *Service) CreateTypeWidgetIfMissing(ctx context.Context, spaceId string,
// only create widget if this was the first object of this type created
return nil
}
var targetName string
typeDetails, err := spaceIndex.GetDetails(typeId)
if err == nil {
targetName = typeDetails.Get(bundle.RelationKeyPluralName).String()
if targetName == "" {
targetName = typeDetails.Get(bundle.RelationKeyName).String()
}
}
return cache.DoState(s, widgetObjectId, func(st *state.State, w widget.Widget) (err error) {
return w.AddAutoWidget(st, typeId, key.String(), addr.ObjectTypeAllViewId, model.BlockContentWidget_View)
return w.AddAutoWidget(st, typeId, key.String(), addr.ObjectTypeAllViewId, model.BlockContentWidget_View, targetName)
})
}

View file

@ -223,7 +223,7 @@ func (c config) GetYamux() yamux.Config {
func (c config) GetQuic() quic.Config {
return quic.Config{
WriteTimeoutSec: 60,
WriteTimeoutSec: 1200,
DialTimeoutSec: 60,
KeepAlivePeriodSec: 120,
}

View file

@ -75,7 +75,7 @@ func (es *GrpcSender) sendEvent(server SessionServer, event *pb.Event) {
if s, ok := status.FromError(err); ok && s.Code() == codes.Unavailable {
es.shutdownCh <- server.Token
}
if strings.Contains(err.Error(), "rpc error: code = Unavailable desc = transport is closing") {
if strings.Contains(err.Error(), "transport is closing") {
log.Errorf("failed to send event: %v", err)
}
}

View file

@ -37,3 +37,31 @@ func (mw *Middleware) ObjectListExport(cctx context.Context, req *pb.RpcObjectLi
})
return response(path, succeed, err)
}
func (mw *Middleware) ObjectExport(cctx context.Context, req *pb.RpcObjectExportRequest) *pb.RpcObjectExportResponse {
response := func(result string, err error) (res *pb.RpcObjectExportResponse) {
res = &pb.RpcObjectExportResponse{
Error: &pb.RpcObjectExportResponseError{
Code: pb.RpcObjectExportResponseError_NULL,
},
}
if err != nil {
res.Error.Code = pb.RpcObjectExportResponseError_UNKNOWN_ERROR
res.Error.Description = getErrorDescription(err)
return
} else {
res.Result = result
}
return res
}
var (
result string
err error
)
err = mw.doBlockService(func(_ *block.Service) error {
es := mw.applicationService.GetApp().MustComponent(export.CName).(export.Export)
result, err = es.ExportSingleInMemory(cctx, req.SpaceId, req.ObjectId, req.Format)
return err
})
return response(result, err)
}

View file

@ -210,6 +210,9 @@ func logIndexLoop(err error) {
if errors.Is(err, rpcstore.ErrNoConnectionToAnyFileClient) {
return
}
if errors.Is(err, files.FailedProtoUnmarshallError) {
return
}
log.Errorf("index loop: %v", err)
}

View file

@ -42,7 +42,10 @@ const (
CName = "files"
)
var log = logging.Logger("anytype-files")
var (
log = logging.Logger("anytype-files")
FailedProtoUnmarshallError = errors.New("failed proto unmarshall")
)
var _ Service = (*service)(nil)
@ -306,7 +309,7 @@ func (s *service) fileInfoFromPath(ctx context.Context, spaceId string, fileId d
}
err = proto.Unmarshal(b, &file)
if err != nil || file.Hash == "" {
return nil, fmt.Errorf("failed to unmarshal not-encrypted file info: %w", err)
return nil, fmt.Errorf("failed to unmarshal not-encrypted file info: %w", FailedProtoUnmarshallError)
}
}

View file

@ -349,7 +349,6 @@ func (s *fileSync) uploadFile(ctx context.Context, spaceID string, fileId domain
if err != nil {
log.Warn("delete limit reached error logged", zap.String("fileId", fileId.String()), zap.Error(err))
}
log.Warn("done upload", zap.String("fileId", fileId.String()), zap.Int("bytesToUpload", blocksAvailability.bytesToUpload), zap.Int("bytesUploaded", totalBytesUploaded))
return nil
}

View file

@ -124,7 +124,7 @@ func uniqName() string {
return time.Now().Format("Anytype.WebPublish.20060102.150405.99")
}
func (s *service) exportToDir(ctx context.Context, spaceId, pageId string) (dirEntries []fs.DirEntry, exportPath string, err error) {
func (s *service) exportToDir(ctx context.Context, spaceId, pageId string, includeSpaceInfo bool) (dirEntries []fs.DirEntry, exportPath string, err error) {
tempDir := os.TempDir()
exportPath, _, err = s.exportService.Export(ctx, pb.RpcObjectListExportRequest{
SpaceId: spaceId,
@ -137,7 +137,7 @@ func (s *service) exportToDir(ctx context.Context, spaceId, pageId string) (dirE
NoProgress: true,
IncludeNested: true,
IncludeBacklinks: true,
IncludeSpace: true,
IncludeSpace: includeSpaceInfo,
LinksStateFilters: &pb.RpcObjectListExportStateFilters{
RelationsWhiteList: relationsWhiteListToPbModel(),
RemoveBlocks: true,
@ -155,7 +155,12 @@ func (s *service) exportToDir(ctx context.Context, spaceId, pageId string) (dirE
}
func (s *service) publishToPublishServer(ctx context.Context, spaceId, pageId, uri, globalName string, joinSpace bool) (err error) {
dirEntries, exportPath, err := s.exportToDir(ctx, spaceId, pageId)
spc, err := s.spaceService.Get(ctx, spaceId)
if err != nil {
return err
}
includeInviteLinkAndSpaceInfo := joinSpace && !spc.IsPersonal()
dirEntries, exportPath, err := s.exportToDir(ctx, spaceId, pageId, includeInviteLinkAndSpaceInfo)
if err != nil {
return err
}
@ -177,12 +182,7 @@ func (s *service) publishToPublishServer(ctx context.Context, spaceId, pageId, u
return err
}
spc, err := s.spaceService.Get(ctx, spaceId)
if err != nil {
return err
}
err = s.applyInviteLink(ctx, spc, &uberSnapshot, joinSpace)
err = s.applyInviteLink(ctx, spaceId, &uberSnapshot, includeInviteLinkAndSpaceInfo)
if err != nil {
return err
}
@ -210,12 +210,18 @@ func (s *service) publishToPublishServer(ctx context.Context, spaceId, pageId, u
return nil
}
func (s *service) applyInviteLink(ctx context.Context, spc clientspace.Space, snapshot *PublishingUberSnapshot, joinSpace bool) error {
inviteLink, err := s.extractInviteLink(ctx, spc.Id(), joinSpace, spc.IsPersonal())
func (s *service) applyInviteLink(ctx context.Context, spaceId string, snapshot *PublishingUberSnapshot, includeInviteLink bool) error {
if !includeInviteLink {
return nil
}
inviteInfo, err := s.inviteService.GetCurrent(ctx, spaceId)
if err != nil && errors.Is(err, inviteservice.ErrInviteNotExists) {
return nil
}
if err != nil {
return err
}
snapshot.Meta.InviteLink = inviteLink
snapshot.Meta.InviteLink = fmt.Sprintf(inviteLinkUrlTemplate, inviteInfo.InviteFileCid, inviteInfo.InviteFileKey)
return nil
}
@ -366,21 +372,6 @@ func (s *service) publishToServer(ctx context.Context, spaceId, pageId, uri, ver
return nil
}
func (s *service) extractInviteLink(ctx context.Context, spaceId string, joinSpace, isPersonal bool) (string, error) {
var inviteLink string
if joinSpace && !isPersonal {
inviteInfo, err := s.inviteService.GetCurrent(ctx, spaceId)
if err != nil && errors.Is(err, inviteservice.ErrInviteNotExists) {
return "", nil
}
if err != nil {
return "", err
}
inviteLink = fmt.Sprintf(inviteLinkUrlTemplate, inviteInfo.InviteFileCid, inviteInfo.InviteFileKey)
}
return inviteLink, nil
}
func (s *service) evaluateDocumentVersion(ctx context.Context, spc clientspace.Space, pageId string, joinSpace bool) (string, error) {
treeStorage, err := spc.Storage().TreeStorage(ctx, pageId)
if err != nil {

View file

@ -139,7 +139,9 @@ func TestPublish(t *testing.T) {
t.Run("success", func(t *testing.T) {
// given
isPersonal := true
spaceService, err := prepareSpaceService(t, isPersonal)
includeSpaceInfo := false
spaceService, err := prepareSpaceService(t, isPersonal, includeSpaceInfo)
objectTypeId := "customObjectType"
expectedUri := "test"
@ -159,7 +161,7 @@ func TestPublish(t *testing.T) {
identityService := mock_identity.NewMockService(t)
identityService.EXPECT().GetMyProfileDetails(context.Background()).Return("identity", nil, domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{}))
exp := prepareExporter(t, objectTypeId, spaceService)
exp := prepareExporter(t, objectTypeId, spaceService, false)
svc := &service{
spaceService: spaceService,
@ -169,7 +171,7 @@ func TestPublish(t *testing.T) {
}
// when
publish, err := svc.Publish(context.Background(), spaceId, objectId, expectedUri, false)
publish, err := svc.Publish(context.Background(), spaceId, objectId, expectedUri, includeSpaceInfo)
// then
assert.NoError(t, err)
@ -182,7 +184,9 @@ func TestPublish(t *testing.T) {
t.Run("success with space sharing", func(t *testing.T) {
// given
isPersonal := false
spaceService, err := prepareSpaceService(t, isPersonal)
includeSpaceInfo := true
spaceService, err := prepareSpaceService(t, isPersonal, includeSpaceInfo)
objectTypeId := "customObjectType"
expectedUri := "test"
@ -206,7 +210,7 @@ func TestPublish(t *testing.T) {
identityService := mock_identity.NewMockService(t)
identityService.EXPECT().GetMyProfileDetails(context.Background()).Return("identity", nil, domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{}))
exp := prepareExporter(t, objectTypeId, spaceService)
exp := prepareExporter(t, objectTypeId, spaceService, includeSpaceInfo)
inviteService := mock_inviteservice.NewMockInviteService(t)
inviteService.EXPECT().GetCurrent(context.Background(), "spaceId").Return(domain.InviteInfo{
@ -223,7 +227,7 @@ func TestPublish(t *testing.T) {
}
// when
publish, err := svc.Publish(context.Background(), spaceId, objectId, expectedUri, true)
publish, err := svc.Publish(context.Background(), spaceId, objectId, expectedUri, includeSpaceInfo)
// then
assert.NoError(t, err)
@ -235,7 +239,9 @@ func TestPublish(t *testing.T) {
})
t.Run("success with space sharing - invite not exists", func(t *testing.T) {
isPersonal := false
spaceService, err := prepareSpaceService(t, isPersonal)
includeSpaceInfo := true
spaceService, err := prepareSpaceService(t, isPersonal, includeSpaceInfo)
objectTypeId := "customObjectType"
expectedUri := "test"
@ -256,7 +262,7 @@ func TestPublish(t *testing.T) {
identityService := mock_identity.NewMockService(t)
identityService.EXPECT().GetMyProfileDetails(context.Background()).Return("identity", nil, domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{}))
exp := prepareExporter(t, objectTypeId, spaceService)
exp := prepareExporter(t, objectTypeId, spaceService, includeSpaceInfo)
inviteService := mock_inviteservice.NewMockInviteService(t)
inviteService.EXPECT().GetCurrent(context.Background(), "spaceId").Return(domain.InviteInfo{}, inviteservice.ErrInviteNotExists)
@ -270,7 +276,7 @@ func TestPublish(t *testing.T) {
}
// when
publish, err := svc.Publish(context.Background(), spaceId, objectId, expectedUri, true)
publish, err := svc.Publish(context.Background(), spaceId, objectId, expectedUri, includeSpaceInfo)
// then
assert.NoError(t, err)
@ -283,7 +289,8 @@ func TestPublish(t *testing.T) {
t.Run("success for member", func(t *testing.T) {
// given
isPersonal := false
spaceService, err := prepareSpaceService(t, isPersonal)
includeSpaceInfo := true
spaceService, err := prepareSpaceService(t, isPersonal, includeSpaceInfo)
objectTypeId := "customObjectType"
expectedUri := "test"
@ -311,7 +318,7 @@ func TestPublish(t *testing.T) {
},
}
exp := prepareExporter(t, objectTypeId, spaceService)
exp := prepareExporter(t, objectTypeId, spaceService, includeSpaceInfo)
inviteService := mock_inviteservice.NewMockInviteService(t)
inviteService.EXPECT().GetCurrent(context.Background(), "spaceId").Return(domain.InviteInfo{
@ -328,7 +335,7 @@ func TestPublish(t *testing.T) {
}
// when
publish, err := svc.Publish(context.Background(), spaceId, objectId, expectedUri, true)
publish, err := svc.Publish(context.Background(), spaceId, objectId, expectedUri, includeSpaceInfo)
// then
assert.NoError(t, err)
@ -341,7 +348,9 @@ func TestPublish(t *testing.T) {
t.Run("internal error", func(t *testing.T) {
// given
isPersonal := true
spaceService, err := prepareSpaceService(t, isPersonal)
includeSpaceInfo := true
spaceService, err := prepareSpaceService(t, isPersonal, includeSpaceInfo)
objectTypeId := "customObjectType"
expectedUri := "test"
@ -363,7 +372,7 @@ func TestPublish(t *testing.T) {
expectedErr: fmt.Errorf("internal error"),
}
exp := prepareExporter(t, objectTypeId, spaceService)
exp := prepareExporter(t, objectTypeId, spaceService, false)
svc := &service{
spaceService: spaceService,
@ -373,7 +382,7 @@ func TestPublish(t *testing.T) {
}
// when
publish, err := svc.Publish(context.Background(), spaceId, objectId, expectedUri, true)
publish, err := svc.Publish(context.Background(), spaceId, objectId, expectedUri, includeSpaceInfo)
// then
assert.Error(t, err)
@ -385,7 +394,14 @@ func TestPublish(t *testing.T) {
})
t.Run("limit error for members", func(t *testing.T) {
// given
isPersonal := false
includeSpaceInfo := true
spaceService := mock_space.NewMockService(t)
space := mock_clientspace.NewMockSpace(t)
space.EXPECT().DerivedIDs().Return(threads.DerivedSmartblockIds{Workspace: workspaceId})
space.EXPECT().IsPersonal().Return(isPersonal)
spaceService.EXPECT().Get(context.Background(), spaceId).Return(space, nil)
expectedUri := "test"
testFile := "test"
@ -415,7 +431,7 @@ func TestPublish(t *testing.T) {
}
// when
publish, err := svc.Publish(context.Background(), spaceId, objectId, expectedUri, true)
publish, err := svc.Publish(context.Background(), spaceId, objectId, expectedUri, includeSpaceInfo)
// then
assert.Error(t, err)
@ -424,7 +440,14 @@ func TestPublish(t *testing.T) {
})
t.Run("default limit error", func(t *testing.T) {
// given
isPersonal := false
includeSpaceInfo := true
spaceService := mock_space.NewMockService(t)
space := mock_clientspace.NewMockSpace(t)
space.EXPECT().DerivedIDs().Return(threads.DerivedSmartblockIds{Workspace: workspaceId})
space.EXPECT().IsPersonal().Return(isPersonal)
spaceService.EXPECT().Get(context.Background(), spaceId).Return(space, nil)
expectedUri := "test"
testFile := "test"
@ -451,7 +474,7 @@ func TestPublish(t *testing.T) {
}
// when
publish, err := svc.Publish(context.Background(), spaceId, objectId, expectedUri, true)
publish, err := svc.Publish(context.Background(), spaceId, objectId, expectedUri, includeSpaceInfo)
// then
assert.Error(t, err)
@ -666,24 +689,27 @@ func TestService_PublishingList(t *testing.T) {
var ctx = context.Background()
func prepareSpaceService(t *testing.T, isPersonal bool) (*mock_space.MockService, error) {
func prepareSpaceService(t *testing.T, isPersonal bool, includeSpaceInfo bool) (*mock_space.MockService, error) {
spaceService := mock_space.NewMockService(t)
space := mock_clientspace.NewMockSpace(t)
ctrl := gomock.NewController(t)
space.EXPECT().IsPersonal().Return(isPersonal)
space.EXPECT().Id().Return(spaceId)
st := mock_anystorage.NewMockClientSpaceStorage(t)
mockSt := mock_objecttree.NewMockStorage(ctrl)
st.EXPECT().TreeStorage(mock.Anything, mock.Anything).Return(mockSt, nil)
mockSt.EXPECT().Heads(gomock.Any()).Return([]string{"heads"}, nil)
space.EXPECT().Storage().Return(st)
space.EXPECT().DerivedIDs().Return(threads.DerivedSmartblockIds{Workspace: workspaceId})
if includeSpaceInfo && !isPersonal {
space.EXPECT().DerivedIDs().Return(threads.DerivedSmartblockIds{Workspace: workspaceId})
}
if includeSpaceInfo {
space.EXPECT().IsPersonal().Return(isPersonal)
}
spaceService.EXPECT().Get(context.Background(), spaceId).Return(space, nil)
return spaceService, nil
}
func prepareExporter(t *testing.T, objectTypeId string, spaceService *mock_space.MockService) export.Export {
func prepareExporter(t *testing.T, objectTypeId string, spaceService *mock_space.MockService, includeSpaceInfo bool) export.Export {
storeFixture := objectstore.NewStoreFixture(t)
objectTypeUniqueKey, err := domain.NewUniqueKey(smartblock.SmartBlockTypeObjectType, objectTypeId)
assert.Nil(t, err)
@ -740,23 +766,25 @@ func prepareExporter(t *testing.T, objectTypeId string, spaceService *mock_space
objectType.Doc = objectTypeDoc
objectType.SetType(smartblock.SmartBlockTypeObjectType)
workspaceTest := smarttest.New(workspaceId)
workspaceDoc := workspaceTest.NewState().SetDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
bundle.RelationKeyId: domain.String(workspaceId),
bundle.RelationKeyType: domain.String(objectTypeId),
}))
workspaceDoc.AddRelationLinks(&model.RelationLink{
Key: bundle.RelationKeyId.String(),
Format: model.RelationFormat_longtext,
}, &model.RelationLink{
Key: bundle.RelationKeyType.String(),
Format: model.RelationFormat_longtext,
})
workspaceTest.Doc = workspaceDoc
if includeSpaceInfo {
workspaceTest := smarttest.New(workspaceId)
workspaceDoc := workspaceTest.NewState().SetDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
bundle.RelationKeyId: domain.String(workspaceId),
bundle.RelationKeyType: domain.String(objectTypeId),
}))
workspaceDoc.AddRelationLinks(&model.RelationLink{
Key: bundle.RelationKeyId.String(),
Format: model.RelationFormat_longtext,
}, &model.RelationLink{
Key: bundle.RelationKeyType.String(),
Format: model.RelationFormat_longtext,
})
workspaceTest.Doc = workspaceDoc
objectGetter.EXPECT().GetObject(context.Background(), workspaceId).Return(workspaceTest, nil)
}
objectGetter.EXPECT().GetObject(context.Background(), objectId).Return(smartBlockTest, nil)
objectGetter.EXPECT().GetObject(context.Background(), objectTypeId).Return(objectType, nil)
objectGetter.EXPECT().GetObject(context.Background(), workspaceId).Return(workspaceTest, nil)
a := &app.App{}
mockSender := mock_event.NewMockSender(t)

View file

@ -909,6 +909,10 @@
- [Rpc.Object.Duplicate.Request](#anytype-Rpc-Object-Duplicate-Request)
- [Rpc.Object.Duplicate.Response](#anytype-Rpc-Object-Duplicate-Response)
- [Rpc.Object.Duplicate.Response.Error](#anytype-Rpc-Object-Duplicate-Response-Error)
- [Rpc.Object.Export](#anytype-Rpc-Object-Export)
- [Rpc.Object.Export.Request](#anytype-Rpc-Object-Export-Request)
- [Rpc.Object.Export.Response](#anytype-Rpc-Object-Export-Response)
- [Rpc.Object.Export.Response.Error](#anytype-Rpc-Object-Export-Response-Error)
- [Rpc.Object.Graph](#anytype-Rpc-Object-Graph)
- [Rpc.Object.Graph.Edge](#anytype-Rpc-Object-Graph-Edge)
- [Rpc.Object.Graph.Request](#anytype-Rpc-Object-Graph-Request)
@ -1557,6 +1561,7 @@
- [Rpc.Object.CrossSpaceSearchUnsubscribe.Response.Error.Code](#anytype-Rpc-Object-CrossSpaceSearchUnsubscribe-Response-Error-Code)
- [Rpc.Object.DateByTimestamp.Response.Error.Code](#anytype-Rpc-Object-DateByTimestamp-Response-Error-Code)
- [Rpc.Object.Duplicate.Response.Error.Code](#anytype-Rpc-Object-Duplicate-Response-Error-Code)
- [Rpc.Object.Export.Response.Error.Code](#anytype-Rpc-Object-Export-Response-Error-Code)
- [Rpc.Object.Graph.Edge.Type](#anytype-Rpc-Object-Graph-Edge-Type)
- [Rpc.Object.Graph.Response.Error.Code](#anytype-Rpc-Object-Graph-Response-Error-Code)
- [Rpc.Object.GroupsSubscribe.Response.Error.Code](#anytype-Rpc-Object-GroupsSubscribe-Response-Error-Code)
@ -1859,6 +1864,7 @@
- [Event.Process.New](#anytype-Event-Process-New)
- [Event.Process.Update](#anytype-Event-Process-Update)
- [Event.Space](#anytype-Event-Space)
- [Event.Space.AutoWidgetAdded](#anytype-Event-Space-AutoWidgetAdded)
- [Event.Space.SyncStatus](#anytype-Event-Space-SyncStatus)
- [Event.Space.SyncStatus.Update](#anytype-Event-Space-SyncStatus-Update)
- [Event.Status](#anytype-Event-Status)
@ -2190,6 +2196,7 @@
| ObjectUndo | [Rpc.Object.Undo.Request](#anytype-Rpc-Object-Undo-Request) | [Rpc.Object.Undo.Response](#anytype-Rpc-Object-Undo-Response) | |
| ObjectRedo | [Rpc.Object.Redo.Request](#anytype-Rpc-Object-Redo-Request) | [Rpc.Object.Redo.Response](#anytype-Rpc-Object-Redo-Response) | |
| ObjectListExport | [Rpc.Object.ListExport.Request](#anytype-Rpc-Object-ListExport-Request) | [Rpc.Object.ListExport.Response](#anytype-Rpc-Object-ListExport-Response) | |
| ObjectExport | [Rpc.Object.Export.Request](#anytype-Rpc-Object-Export-Request) | [Rpc.Object.Export.Response](#anytype-Rpc-Object-Export-Response) | |
| ObjectBookmarkFetch | [Rpc.Object.BookmarkFetch.Request](#anytype-Rpc-Object-BookmarkFetch-Request) | [Rpc.Object.BookmarkFetch.Response](#anytype-Rpc-Object-BookmarkFetch-Response) | |
| ObjectToBookmark | [Rpc.Object.ToBookmark.Request](#anytype-Rpc-Object-ToBookmark-Request) | [Rpc.Object.ToBookmark.Response](#anytype-Rpc-Object-ToBookmark-Response) | |
| ObjectImport | [Rpc.Object.Import.Request](#anytype-Rpc-Object-Import-Request) | [Rpc.Object.Import.Response](#anytype-Rpc-Object-Import-Response) | |
@ -15542,6 +15549,66 @@ Get the info for page alongside with info for all inbound and outbound links fro
<a name="anytype-Rpc-Object-Export"></a>
### Rpc.Object.Export
<a name="anytype-Rpc-Object-Export-Request"></a>
### Rpc.Object.Export.Request
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| spaceId | [string](#string) | | |
| objectId | [string](#string) | | ids of documents for export, when empty - will export all available docs |
| format | [model.Export.Format](#anytype-model-Export-Format) | | export format |
<a name="anytype-Rpc-Object-Export-Response"></a>
### Rpc.Object.Export.Response
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| error | [Rpc.Object.Export.Response.Error](#anytype-Rpc-Object-Export-Response-Error) | | |
| result | [string](#string) | | |
| event | [ResponseEvent](#anytype-ResponseEvent) | | |
<a name="anytype-Rpc-Object-Export-Response-Error"></a>
### Rpc.Object.Export.Response.Error
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| code | [Rpc.Object.Export.Response.Error.Code](#anytype-Rpc-Object-Export-Response-Error-Code) | | |
| description | [string](#string) | | |
<a name="anytype-Rpc-Object-Graph"></a>
### Rpc.Object.Graph
@ -24808,6 +24875,19 @@ Middleware-to-front-end response, that can contain a NULL error or a non-NULL er
<a name="anytype-Rpc-Object-Export-Response-Error-Code"></a>
### Rpc.Object.Export.Response.Error.Code
| Name | Number | Description |
| ---- | ------ | ----------- |
| NULL | 0 | |
| UNKNOWN_ERROR | 1 | |
| BAD_INPUT | 2 | ... |
<a name="anytype-Rpc-Object-Graph-Edge-Type"></a>
### Rpc.Object.Graph.Edge.Type
@ -28889,6 +28969,7 @@ Precondition: user A opened a block
| payloadBroadcast | [Event.Payload.Broadcast](#anytype-Event-Payload-Broadcast) | | |
| membershipUpdate | [Event.Membership.Update](#anytype-Event-Membership-Update) | | |
| spaceSyncStatusUpdate | [Event.Space.SyncStatus.Update](#anytype-Event-Space-SyncStatus-Update) | | |
| spaceAutoWidgetAdded | [Event.Space.AutoWidgetAdded](#anytype-Event-Space-AutoWidgetAdded) | | |
| p2pStatusUpdate | [Event.P2PStatus.Update](#anytype-Event-P2PStatus-Update) | | |
| importFinish | [Event.Import.Finish](#anytype-Event-Import-Finish) | | |
| chatAdd | [Event.Chat.Add](#anytype-Event-Chat-Add) | | |
@ -29356,6 +29437,23 @@ Removes document from subscription
<a name="anytype-Event-Space-AutoWidgetAdded"></a>
### Event.Space.AutoWidgetAdded
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| targetId | [string](#string) | | |
| targetName | [string](#string) | | pluralName (if exists) for types, fallback to name. Special cases for &#34;bin&#34; and &#34;favorites&#34; |
| widgetBlockId | [string](#string) | | |
<a name="anytype-Event-Space-SyncStatus"></a>
### Event.Space.SyncStatus
@ -30720,6 +30818,7 @@ Link: block to link some content from an external sources.
| layout | [Block.Content.Widget.Layout](#anytype-model-Block-Content-Widget-Layout) | | |
| limit | [int32](#int32) | | |
| viewId | [string](#string) | | |
| autoAdded | [bool](#bool) | | |

20
go.mod
View file

@ -52,6 +52,7 @@ require (
github.com/hashicorp/yamux v0.1.2
github.com/hbagdi/go-unsplash v0.0.0-20230414214043-474fc02c9119
github.com/huandu/skiplist v1.2.1
github.com/iancoleman/strcase v0.3.0
github.com/improbable-eng/grpc-web v0.15.0
github.com/ipfs/boxo v0.29.1
github.com/ipfs/go-block-format v0.2.0
@ -95,7 +96,7 @@ require (
github.com/stretchr/testify v1.10.0
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.4
github.com/swaggo/swag/v2 v2.0.0-rc4
github.com/uber/jaeger-client-go v2.30.0+incompatible
github.com/valyala/fastjson v1.6.4
github.com/vektra/mockery/v2 v2.47.0
@ -109,7 +110,7 @@ require (
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
golang.org/x/image v0.25.0
golang.org/x/mobile v0.0.0-20250218173827-cd096645fcd3
golang.org/x/net v0.37.0
golang.org/x/net v0.38.0
golang.org/x/oauth2 v0.28.0
golang.org/x/sys v0.31.0
golang.org/x/text v0.23.0
@ -128,8 +129,6 @@ require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
github.com/alexbrainman/goissue34681 v0.0.0-20191006012335-3fc7a47baff5 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
@ -170,10 +169,10 @@ require (
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/spec v0.20.9 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-pkgz/expirable-cache/v3 v3.0.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
@ -199,7 +198,6 @@ require (
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/holiman/uint256 v1.2.4 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/iancoleman/strcase v0.3.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/ipfs/bbloom v0.0.4 // indirect
@ -219,7 +217,7 @@ require (
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
github.com/libp2p/go-libp2p v0.41.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.63 // indirect
@ -262,6 +260,8 @@ require (
github.com/spf13/viper v1.15.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/sv-tools/openapi v0.2.1 // indirect
github.com/swaggo/swag v1.16.4 // indirect
github.com/tetratelabs/wazero v1.8.1 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect

39
go.sum
View file

@ -56,10 +56,6 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/PuerkitoBio/goquery v1.10.2 h1:7fh2BdHcG6VFZsK7toXBT/Bh1z5Wmy8Q9MV9HqT2AM8=
github.com/PuerkitoBio/goquery v1.10.2/go.mod h1:0guWGjcLu9AYC7C1GHnpysHy056u9aEkUHwhdnePMCU=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow=
@ -84,12 +80,10 @@ github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kk
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/anyproto/any-store v0.1.12 h1:VgTyfxM4BvFnYMbwWxV9FQmXWNlwp9xriWFh6VOjcm0=
github.com/anyproto/any-store v0.1.12/go.mod h1:T6WNuCHcuXIRiaZ+QAcBHdxIbPbgNCMIf1u3P9jvAyU=
github.com/anyproto/any-sync v0.6.10-0.20250328101049-7b02c1945e6d h1:AU7TEdUV4NZSNilcWfAhGVdZUsZutfDx3Q0cJ11S4A4=
github.com/anyproto/any-sync v0.6.10-0.20250328101049-7b02c1945e6d/go.mod h1:TSKgCoTV40Bt8AfCh3RxPUUAfYGrhc8Mzh8/AiVlvX4=
github.com/anyproto/any-sync v0.6.10 h1:TV3yLkp5NK7FkddUvVtfxT01t/Xi+DlKbvisGEQwmC0=
github.com/anyproto/any-sync v0.6.10/go.mod h1:TSKgCoTV40Bt8AfCh3RxPUUAfYGrhc8Mzh8/AiVlvX4=
github.com/anyproto/anytype-publish-server/publishclient v0.0.0-20250131145601-de288583ff2a h1:ZZM+0OUCQMWSLSflpkf0ZMVo3V76qEDDIXPpQOClNs0=
github.com/anyproto/anytype-publish-server/publishclient v0.0.0-20250131145601-de288583ff2a/go.mod h1:4fkueCZcGniSMXkrwESO8zzERrh/L7WHimRNWecfGM0=
github.com/anyproto/anytype-push-server/pushclient v0.0.0-20250319134422-fd3f261ea023 h1:c7zj0NKKxFxqmK2PjNofpNB2yRCOzbA0gMLCiaEuLeY=
github.com/anyproto/anytype-push-server/pushclient v0.0.0-20250319134422-fd3f261ea023/go.mod h1:IjVnEyh6QiLsPPfZinTTnOtxe79uJUKZyC1z65b2Yuc=
github.com/anyproto/badger/v4 v4.2.1-0.20240110160636-80743fa3d580 h1:Ba80IlCCxkZ9H1GF+7vFu/TSpPvbpDCxXJ5ogc4euYc=
github.com/anyproto/badger/v4 v4.2.1-0.20240110160636-80743fa3d580/go.mod h1:T/uWAYxrXdaXw64ihI++9RMbKTCpKd/yE9+saARew7k=
github.com/anyproto/go-chash v0.1.0 h1:I9meTPjXFRfXZHRJzjOHC/XF7Q5vzysKkiT/grsogXY=
@ -351,15 +345,18 @@ github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiU
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8=
github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-pkgz/expirable-cache/v3 v3.0.0 h1:u3/gcu3sabLYiTCevoRKv+WzjIn5oo7P8XtiXBeRDLw=
github.com/go-pkgz/expirable-cache/v3 v3.0.0/go.mod h1:2OQiDyEGQalYecLWmXprm3maPXeVb5/6/X7yRPYTzec=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
@ -674,6 +671,7 @@ github.com/kovidgoyal/imaging v1.6.4/go.mod h1:bEIgsaZmXlvFfkv/CUxr9rJook6AQkJnp
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@ -714,8 +712,9 @@ github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matishsiao/goInfo v0.0.0-20240924010139-10388a85396f h1:XDrsC/9hdgiU9ecceSmYsS2E3fBtFiYc34dAMFgegnM=
github.com/matishsiao/goInfo v0.0.0-20240924010139-10388a85396f/go.mod h1:aEt7p9Rvh67BYApmZwNDPpgircTO2kgdmDUoF/1QmwA=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
@ -1048,12 +1047,16 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/sv-tools/openapi v0.2.1 h1:ES1tMQMJFGibWndMagvdoo34T1Vllxr1Nlm5wz6b1aA=
github.com/sv-tools/openapi v0.2.1/go.mod h1:k5VuZamTw1HuiS9p2Wl5YIDWzYnHG6/FgPOSFXLAhGg=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
github.com/swaggo/swag/v2 v2.0.0-rc4 h1:SZ8cK68gcV6cslwrJMIOqPkJELRwq4gmjvk77MrvHvY=
github.com/swaggo/swag/v2 v2.0.0-rc4/go.mod h1:Ow7Y8gF16BTCDn8YxZbyKn8FkMLRUHekv1kROJZpbvE=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
github.com/tetratelabs/wazero v1.8.1 h1:NrcgVbWfkWvVc4UtT4LRLDf91PsOzDzefMdwhLfA550=
github.com/tetratelabs/wazero v1.8.1/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
@ -1290,7 +1293,6 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@ -1305,8 +1307,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -1396,7 +1398,6 @@ golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210426080607-c94f62235c83/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View file

@ -9,3 +9,9 @@ lint:
@npx prettier --check "./**" 2> /dev/null || true
@echo 'Linting with golint...'
@golint `go list ./... | grep -v /vendor/`
swagger:
@echo 'Generating swagger docs...'
@swag init --v3.1 -q -d core/api -g service.go -o core/api/docs
@echo 'Formatting swagger docs...'
@swag fmt -d core/api

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -2908,6 +2908,35 @@ message Rpc {
}
message Export {
message Request {
string spaceId = 10;
// ids of documents for export, when empty - will export all available docs
string objectId = 2;
// export format
anytype.model.Export.Format format = 3;
}
message Response {
Error error = 1;
string result = 2;
ResponseEvent event = 3;
message Error {
Code code = 1;
string description = 2;
enum Code {
NULL = 0;
UNKNOWN_ERROR = 1;
BAD_INPUT = 2;
// ...
}
}
}
}
message Import {
message Request {
option (no_auth) = true;

View file

@ -108,6 +108,7 @@ message Event {
Membership.Update membershipUpdate = 117;
Space.SyncStatus.Update spaceSyncStatusUpdate = 119;
Space.AutoWidgetAdded spaceAutoWidgetAdded = 122;
P2PStatus.Update p2pStatusUpdate = 120;
@ -1155,6 +1156,12 @@ message Event {
IncompatibleVersion = 2;
NetworkError = 3;
}
message AutoWidgetAdded {
string targetId = 1;
string targetName = 2; // pluralName (if exists) for types, fallback to name. Special cases for "bin" and "favorites"
string widgetBlockId = 3;
}
}
message P2PStatus {
message Update {

View file

@ -127,6 +127,7 @@ service ClientCommands {
rpc ObjectUndo (anytype.Rpc.Object.Undo.Request) returns (anytype.Rpc.Object.Undo.Response);
rpc ObjectRedo (anytype.Rpc.Object.Redo.Request) returns (anytype.Rpc.Object.Redo.Response);
rpc ObjectListExport (anytype.Rpc.Object.ListExport.Request) returns (anytype.Rpc.Object.ListExport.Response);
rpc ObjectExport (anytype.Rpc.Object.Export.Request) returns (anytype.Rpc.Object.Export.Response);
rpc ObjectBookmarkFetch (anytype.Rpc.Object.BookmarkFetch.Request) returns (anytype.Rpc.Object.BookmarkFetch.Response);
rpc ObjectToBookmark (anytype.Rpc.Object.ToBookmark.Request) returns (anytype.Rpc.Object.ToBookmark.Response);
rpc ObjectImport (anytype.Rpc.Object.Import.Request) returns (anytype.Rpc.Object.Import.Response);

File diff suppressed because it is too large Load diff

View file

@ -26,366 +26,365 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
func init() { proto.RegisterFile("pb/protos/service/service.proto", fileDescriptor_93a29dc403579097) }
var fileDescriptor_93a29dc403579097 = []byte{
// 5733 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x9d, 0x5f, 0x6f, 0x1d, 0x49,
0x56, 0xc0, 0xc7, 0x2f, 0x0c, 0xf4, 0xb2, 0x03, 0xdc, 0x81, 0x61, 0x76, 0xd8, 0x4d, 0x32, 0x99,
0xc4, 0x4e, 0xe2, 0xf8, 0x3a, 0x93, 0xcc, 0x3f, 0x76, 0x91, 0xe0, 0xc6, 0x8e, 0x3d, 0x77, 0xc7,
0x76, 0x8c, 0xef, 0x75, 0x22, 0x46, 0x42, 0xa2, 0x7d, 0xbb, 0x7c, 0xdd, 0xb8, 0x6f, 0x57, 0x6f,
0x77, 0x5d, 0x27, 0x77, 0x11, 0x08, 0x04, 0x02, 0x81, 0x16, 0xb1, 0xe2, 0x9f, 0xe0, 0x09, 0x89,
0x4f, 0xc0, 0xc7, 0xe0, 0x71, 0x1f, 0x79, 0x44, 0x33, 0xdf, 0x03, 0xa1, 0xae, 0xaa, 0xae, 0x3f,
0xa7, 0xcf, 0xa9, 0x6e, 0xcf, 0x3e, 0x25, 0xf2, 0xf9, 0x9d, 0x73, 0xaa, 0xba, 0x4e, 0x55, 0x9d,
0xaa, 0xae, 0xae, 0x1b, 0xdd, 0x2c, 0xce, 0xb6, 0x8b, 0x92, 0x0b, 0x5e, 0x6d, 0x57, 0xac, 0xbc,
0x4a, 0x67, 0xac, 0xf9, 0x77, 0x28, 0xff, 0x3c, 0x78, 0x33, 0xce, 0x57, 0x62, 0x55, 0xb0, 0xf7,
0xde, 0xb5, 0xe4, 0x8c, 0x2f, 0x16, 0x71, 0x9e, 0x54, 0x0a, 0x79, 0xef, 0x1d, 0x2b, 0x61, 0x57,
0x2c, 0x17, 0xfa, 0xef, 0x8f, 0x7f, 0xf2, 0x7f, 0x6b, 0xd1, 0x5b, 0x3b, 0x59, 0xca, 0x72, 0xb1,
0xa3, 0x35, 0x06, 0x5f, 0x46, 0xdf, 0x1e, 0x15, 0xc5, 0x3e, 0x13, 0x2f, 0x58, 0x59, 0xa5, 0x3c,
0x1f, 0x7c, 0x30, 0xd4, 0x0e, 0x86, 0x27, 0xc5, 0x6c, 0x38, 0x2a, 0x8a, 0xa1, 0x15, 0x0e, 0x4f,
0xd8, 0x8f, 0x96, 0xac, 0x12, 0xef, 0xdd, 0x09, 0x43, 0x55, 0xc1, 0xf3, 0x8a, 0x0d, 0xce, 0xa3,
0x5f, 0x1b, 0x15, 0xc5, 0x84, 0x89, 0x5d, 0x56, 0x57, 0x60, 0x22, 0x62, 0xc1, 0x06, 0x1b, 0x2d,
0x55, 0x1f, 0x30, 0x3e, 0xee, 0x75, 0x83, 0xda, 0xcf, 0x34, 0xfa, 0x56, 0xed, 0xe7, 0x62, 0x29,
0x12, 0xfe, 0x2a, 0x1f, 0xbc, 0xdf, 0x56, 0xd4, 0x22, 0x63, 0xfb, 0x76, 0x08, 0xd1, 0x56, 0x5f,
0x46, 0xbf, 0xfc, 0x32, 0xce, 0x32, 0x26, 0x76, 0x4a, 0x56, 0x17, 0xdc, 0xd7, 0x51, 0xa2, 0xa1,
0x92, 0x19, 0xbb, 0x1f, 0x04, 0x19, 0x6d, 0xf8, 0xcb, 0xe8, 0xdb, 0x4a, 0x72, 0xc2, 0x66, 0xfc,
0x8a, 0x95, 0x03, 0x54, 0x4b, 0x0b, 0x89, 0x47, 0xde, 0x82, 0xa0, 0xed, 0x1d, 0x9e, 0x5f, 0xb1,
0x52, 0xe0, 0xb6, 0xb5, 0x30, 0x6c, 0xdb, 0x42, 0xda, 0xf6, 0xdf, 0xae, 0x45, 0xdf, 0x1d, 0xcd,
0x66, 0x7c, 0x99, 0x8b, 0x03, 0x3e, 0x8b, 0xb3, 0x83, 0x34, 0xbf, 0x3c, 0x62, 0xaf, 0x76, 0x2e,
0x6a, 0x3e, 0x9f, 0xb3, 0xc1, 0x13, 0xff, 0xa9, 0x2a, 0x74, 0x68, 0xd8, 0xa1, 0x0b, 0x1b, 0xdf,
0x1f, 0x5d, 0x4f, 0x49, 0x97, 0xe5, 0x1f, 0xd6, 0xa2, 0x1b, 0xb0, 0x2c, 0x13, 0x9e, 0x5d, 0x31,
0x5b, 0x9a, 0x8f, 0x3b, 0x0c, 0xfb, 0xb8, 0x29, 0xcf, 0x27, 0xd7, 0x55, 0xd3, 0x25, 0xca, 0xa2,
0xb7, 0xdd, 0x70, 0x99, 0xb0, 0x4a, 0x76, 0xa7, 0xfb, 0x74, 0x44, 0x68, 0xc4, 0x78, 0x7e, 0xd0,
0x07, 0xd5, 0xde, 0xd2, 0x68, 0xa0, 0xbd, 0x65, 0xbc, 0x32, 0xce, 0xee, 0xa1, 0x16, 0x1c, 0xc2,
0xf8, 0xba, 0xdf, 0x83, 0xd4, 0xae, 0xfe, 0x28, 0xfa, 0x95, 0x97, 0xbc, 0xbc, 0xac, 0x8a, 0x78,
0xc6, 0x74, 0x57, 0xb8, 0xeb, 0x6b, 0x37, 0x52, 0xd8, 0x1b, 0xd6, 0xbb, 0x30, 0x27, 0x68, 0x1b,
0xe1, 0xf3, 0x82, 0xc1, 0x31, 0xc8, 0x2a, 0xd6, 0x42, 0x2a, 0x68, 0x21, 0xa4, 0x6d, 0x5f, 0x46,
0x03, 0x6b, 0xfb, 0xec, 0x8f, 0xd9, 0x4c, 0x8c, 0x92, 0x04, 0xb6, 0x8a, 0xd5, 0x95, 0xc4, 0x70,
0x94, 0x24, 0x54, 0xab, 0xe0, 0xa8, 0x76, 0xf6, 0x2a, 0x7a, 0x07, 0x38, 0x3b, 0x48, 0x2b, 0xe9,
0x70, 0x2b, 0x6c, 0x45, 0x63, 0xc6, 0xe9, 0xb0, 0x2f, 0xae, 0x1d, 0xff, 0xf9, 0x5a, 0xf4, 0x1d,
0xc4, 0xf3, 0x09, 0x5b, 0xf0, 0x2b, 0x36, 0x78, 0xd4, 0x6d, 0x4d, 0x91, 0xc6, 0xff, 0x87, 0xd7,
0xd0, 0x40, 0xc2, 0x64, 0xc2, 0x32, 0x36, 0x13, 0x64, 0x98, 0x28, 0x71, 0x67, 0x98, 0x18, 0xcc,
0xe9, 0x61, 0x8d, 0x70, 0x9f, 0x89, 0x9d, 0x65, 0x59, 0xb2, 0x5c, 0x90, 0x6d, 0x69, 0x91, 0xce,
0xb6, 0xf4, 0x50, 0xa4, 0x3e, 0xfb, 0x4c, 0x8c, 0xb2, 0x8c, 0xac, 0x8f, 0x12, 0x77, 0xd6, 0xc7,
0x60, 0xda, 0xc3, 0x2c, 0xfa, 0x55, 0xe7, 0x89, 0x89, 0x71, 0x7e, 0xce, 0x07, 0xf4, 0xb3, 0x90,
0x72, 0xe3, 0x63, 0xa3, 0x93, 0x43, 0xaa, 0xf1, 0xec, 0x75, 0xc1, 0x4b, 0xba, 0x59, 0x94, 0xb8,
0xb3, 0x1a, 0x06, 0xd3, 0x1e, 0xfe, 0x30, 0x7a, 0x4b, 0x8f, 0x92, 0xcd, 0x7c, 0x76, 0x07, 0x1d,
0x42, 0xe1, 0x84, 0x76, 0xb7, 0x83, 0x6a, 0x99, 0x3f, 0x4c, 0xe7, 0x65, 0x3d, 0xfa, 0xe0, 0xe6,
0xb5, 0xb4, 0xc3, 0xbc, 0xa5, 0xb4, 0x79, 0x1e, 0xfd, 0xba, 0x6f, 0x7e, 0x27, 0xce, 0x67, 0x2c,
0x1b, 0x3c, 0x08, 0xa9, 0x2b, 0xc6, 0xb8, 0xda, 0xec, 0xc5, 0xda, 0xc1, 0x4e, 0x13, 0x7a, 0x30,
0xfd, 0x00, 0xd5, 0x06, 0x43, 0xe9, 0x9d, 0x30, 0xd4, 0xb2, 0xbd, 0xcb, 0x32, 0x46, 0xda, 0x56,
0xc2, 0x0e, 0xdb, 0x06, 0xd2, 0xb6, 0xcb, 0xe8, 0x37, 0x4c, 0x33, 0xd7, 0x79, 0x81, 0x94, 0xd7,
0x93, 0xce, 0x26, 0xd1, 0x8e, 0x2e, 0x64, 0x7c, 0x3d, 0xec, 0x07, 0xb7, 0xea, 0xa3, 0x47, 0x14,
0xbc, 0x3e, 0x60, 0x3c, 0xb9, 0x13, 0x86, 0xb4, 0xed, 0xbf, 0x5b, 0x8b, 0xbe, 0xa7, 0x65, 0xcf,
0xf2, 0xf8, 0x2c, 0x63, 0x72, 0x8a, 0x3f, 0x62, 0xe2, 0x15, 0x2f, 0x2f, 0x27, 0xab, 0x7c, 0x46,
0xa4, 0x33, 0x38, 0xdc, 0x91, 0xce, 0x90, 0x4a, 0xba, 0x30, 0x7f, 0x12, 0xbd, 0xdb, 0x04, 0xc5,
0x45, 0x9c, 0xcf, 0xd9, 0x0f, 0x2b, 0x9e, 0x8f, 0x8a, 0x74, 0x94, 0x24, 0xe5, 0x60, 0x88, 0x37,
0x3d, 0xe4, 0x4c, 0x09, 0xb6, 0x7b, 0xf3, 0x4e, 0xfa, 0xac, 0x9f, 0xb2, 0xe0, 0x05, 0x4c, 0x9f,
0x9b, 0xc7, 0x27, 0x78, 0x41, 0xa5, 0xcf, 0x3e, 0xd2, 0xb2, 0x7a, 0x58, 0xcf, 0x41, 0xb8, 0xd5,
0x43, 0x77, 0xd2, 0xb9, 0x1d, 0x42, 0xec, 0x1c, 0xd0, 0x3c, 0x28, 0x9e, 0x9f, 0xa7, 0xf3, 0xd3,
0x22, 0xa9, 0xfb, 0xd0, 0x7d, 0xbc, 0xce, 0x0e, 0x42, 0xcc, 0x01, 0x04, 0xaa, 0xbd, 0xfd, 0xbd,
0xcd, 0x32, 0xf5, 0xb8, 0xb4, 0x57, 0xf2, 0xc5, 0x01, 0x9b, 0xc7, 0xb3, 0x95, 0x1e, 0x4c, 0x3f,
0x0a, 0x8d, 0x62, 0x90, 0x36, 0x85, 0xf8, 0xf8, 0x9a, 0x5a, 0xba, 0x3c, 0xff, 0xb1, 0x16, 0xdd,
0xf1, 0xe2, 0x44, 0x07, 0x93, 0x2a, 0xfd, 0x28, 0x4f, 0x4e, 0x58, 0x25, 0xe2, 0x52, 0x0c, 0xbe,
0x1f, 0x88, 0x01, 0x42, 0xc7, 0x94, 0xed, 0x07, 0xdf, 0x48, 0xd7, 0xb6, 0xfa, 0xa4, 0x9e, 0x25,
0xf4, 0xf8, 0xe3, 0xb7, 0xba, 0x94, 0xc0, 0xd1, 0xe7, 0x76, 0x08, 0xb1, 0xad, 0x2e, 0x05, 0xe3,
0xfc, 0x2a, 0x15, 0x6c, 0x9f, 0xe5, 0xac, 0x6c, 0xb7, 0xba, 0x52, 0xf5, 0x11, 0xa2, 0xd5, 0x09,
0xd4, 0x8e, 0x74, 0x9e, 0x37, 0x93, 0x69, 0x6c, 0x06, 0x8c, 0xb4, 0x72, 0x8d, 0x87, 0xfd, 0x60,
0xa2, 0x86, 0x62, 0xbf, 0x36, 0x12, 0xac, 0xa1, 0x42, 0x7a, 0xd5, 0xd0, 0xa0, 0x76, 0x61, 0xee,
0x78, 0x3b, 0x61, 0x57, 0xfc, 0x12, 0x2e, 0xcc, 0x5d, 0x03, 0x0a, 0x20, 0x16, 0xe6, 0x28, 0x68,
0x93, 0x0f, 0xc7, 0xcf, 0x8b, 0x94, 0xbd, 0x02, 0xc9, 0x87, 0xab, 0x5c, 0x8b, 0x89, 0xe4, 0x03,
0xc1, 0xb4, 0x87, 0xa3, 0xe8, 0x97, 0xa4, 0xf0, 0x87, 0x3c, 0xcd, 0x07, 0x37, 0x11, 0xa5, 0x5a,
0x60, 0xac, 0xde, 0xa2, 0x01, 0x50, 0xe2, 0xfa, 0xaf, 0x3a, 0x13, 0xb8, 0x4b, 0x28, 0x81, 0x24,
0x60, 0xbd, 0x0b, 0xb3, 0x59, 0x9f, 0x14, 0xd6, 0xa3, 0xe5, 0xe4, 0x22, 0x2e, 0xd3, 0x7c, 0x3e,
0xc0, 0x74, 0x1d, 0x39, 0x91, 0xf5, 0x61, 0x1c, 0x08, 0x27, 0xad, 0x38, 0x2a, 0x8a, 0xb2, 0x1e,
0x84, 0xb1, 0x70, 0xf2, 0x91, 0x60, 0x38, 0xb5, 0x50, 0xdc, 0xdb, 0x2e, 0x9b, 0x65, 0x69, 0x1e,
0xf4, 0xa6, 0x91, 0x3e, 0xde, 0x2c, 0x0a, 0x82, 0xf7, 0x80, 0xc5, 0x57, 0xac, 0xa9, 0x19, 0xf6,
0x64, 0x5c, 0x20, 0x18, 0xbc, 0x00, 0xb4, 0x4b, 0x6c, 0x29, 0x3e, 0x8c, 0x2f, 0x59, 0xfd, 0x80,
0x59, 0x3d, 0x85, 0x0f, 0x30, 0x7d, 0x8f, 0x20, 0x96, 0xd8, 0x38, 0xa9, 0x5d, 0x2d, 0xa3, 0x77,
0xa4, 0xfc, 0x38, 0x2e, 0x45, 0x3a, 0x4b, 0x8b, 0x38, 0x6f, 0x96, 0x6e, 0xd8, 0x28, 0xd2, 0xa2,
0x8c, 0xcb, 0xad, 0x9e, 0xb4, 0x76, 0xfb, 0xaf, 0x6b, 0xd1, 0xfb, 0xd0, 0xef, 0x31, 0x2b, 0x17,
0xa9, 0xdc, 0x01, 0xa8, 0xd4, 0x90, 0x3f, 0xf8, 0x34, 0x6c, 0xb4, 0xa5, 0x60, 0x4a, 0xf3, 0xd9,
0xf5, 0x15, 0x6d, 0xde, 0x37, 0xd1, 0xab, 0xa2, 0xe7, 0x65, 0xd2, 0xda, 0x21, 0x9b, 0x34, 0x4b,
0x1d, 0x29, 0x24, 0xf2, 0xbe, 0x16, 0x04, 0x7a, 0xf8, 0x69, 0x5e, 0x35, 0xd6, 0xb1, 0x1e, 0x6e,
0xc5, 0xc1, 0x1e, 0xee, 0x61, 0xb6, 0x87, 0x1f, 0x2f, 0xcf, 0xb2, 0xb4, 0xba, 0x48, 0xf3, 0xb9,
0x4e, 0xf2, 0x7d, 0x5d, 0x2b, 0x86, 0x79, 0xfe, 0x46, 0x27, 0x87, 0x39, 0xd1, 0xc1, 0x42, 0x3a,
0x01, 0x61, 0xb2, 0xd1, 0xc9, 0xd9, 0xb5, 0x97, 0x95, 0xd6, 0x8b, 0x7e, 0xb0, 0xf6, 0x72, 0x54,
0x6b, 0x29, 0xb1, 0xf6, 0x6a, 0x53, 0x76, 0xed, 0xe5, 0xd6, 0xa1, 0xe2, 0xd9, 0x15, 0x3b, 0x2d,
0x53, 0xb0, 0xf6, 0xf2, 0xca, 0xd7, 0x30, 0xc4, 0xda, 0x8b, 0x62, 0xed, 0x40, 0x65, 0x89, 0x7d,
0x26, 0x26, 0x22, 0x16, 0xcb, 0x0a, 0x0c, 0x54, 0x8e, 0x0d, 0x83, 0x10, 0x03, 0x15, 0x81, 0x6a,
0x6f, 0xbf, 0x1f, 0x45, 0x6a, 0xbf, 0x44, 0xee, 0x69, 0xf9, 0x73, 0x8f, 0xde, 0x48, 0xf1, 0x36,
0xb4, 0xde, 0x0f, 0x10, 0x36, 0xbd, 0x52, 0x7f, 0x97, 0x5b, 0x75, 0x03, 0x54, 0x43, 0x8a, 0x88,
0xf4, 0x0a, 0x20, 0xb0, 0xa0, 0x93, 0x0b, 0xfe, 0x0a, 0x2f, 0x68, 0x2d, 0x09, 0x17, 0x54, 0x13,
0x76, 0xf3, 0x5c, 0x17, 0x14, 0xdb, 0x3c, 0x6f, 0x8a, 0x11, 0xda, 0x3c, 0x87, 0x8c, 0x8d, 0x19,
0xd7, 0xf0, 0x53, 0xce, 0x2f, 0x17, 0x71, 0x79, 0x09, 0x62, 0xc6, 0x53, 0x6e, 0x18, 0x22, 0x66,
0x28, 0xd6, 0xc6, 0x8c, 0xeb, 0xb0, 0x4e, 0xce, 0x4f, 0xcb, 0x0c, 0xc4, 0x8c, 0x67, 0x43, 0x23,
0x44, 0xcc, 0x10, 0xa8, 0x1d, 0x9d, 0x5c, 0x6f, 0x13, 0x06, 0xb7, 0x6b, 0x3c, 0xf5, 0x09, 0xa3,
0xb6, 0x6b, 0x10, 0x0c, 0x86, 0xd0, 0x7e, 0x19, 0x17, 0x17, 0x78, 0x08, 0x49, 0x51, 0x38, 0x84,
0x1a, 0x04, 0xb6, 0xf7, 0x84, 0xc5, 0xe5, 0xec, 0x02, 0x6f, 0x6f, 0x25, 0x0b, 0xb7, 0xb7, 0x61,
0x60, 0x7b, 0x2b, 0xc1, 0xcb, 0x54, 0x5c, 0x1c, 0x32, 0x11, 0xe3, 0xed, 0xed, 0x33, 0xe1, 0xf6,
0x6e, 0xb1, 0x36, 0xfb, 0x77, 0x1d, 0x4e, 0x96, 0x67, 0xd5, 0xac, 0x4c, 0xcf, 0xd8, 0x20, 0x60,
0xc5, 0x40, 0x44, 0xf6, 0x4f, 0xc2, 0xda, 0xe7, 0x4f, 0xd7, 0xa2, 0x9b, 0x4d, 0xb3, 0xf3, 0xaa,
0xd2, 0x73, 0x9f, 0xef, 0xfe, 0x63, 0xbc, 0x7d, 0x09, 0x9c, 0x78, 0x9d, 0xd1, 0x43, 0xcd, 0xc9,
0x0d, 0xf0, 0x22, 0x9d, 0xe6, 0x95, 0x29, 0xd4, 0xa7, 0x7d, 0xac, 0x3b, 0x0a, 0x44, 0x6e, 0xd0,
0x4b, 0xd1, 0xa6, 0x65, 0xba, 0x7d, 0x1a, 0xd9, 0x38, 0xa9, 0x40, 0x5a, 0xd6, 0x3c, 0x6f, 0x87,
0x20, 0xd2, 0x32, 0x9c, 0x84, 0xa1, 0xb0, 0x5f, 0xf2, 0x65, 0x51, 0x75, 0x84, 0x02, 0x80, 0xc2,
0xa1, 0xd0, 0x86, 0xb5, 0xcf, 0xd7, 0xd1, 0x6f, 0xba, 0xe1, 0xe7, 0x3e, 0xec, 0x2d, 0x3a, 0xa6,
0xb0, 0x47, 0x3c, 0xec, 0x8b, 0xdb, 0x8c, 0xa2, 0xf1, 0x2c, 0x76, 0x99, 0x88, 0xd3, 0xac, 0x1a,
0xac, 0xe3, 0x36, 0x1a, 0x39, 0x91, 0x51, 0x60, 0x1c, 0x1c, 0xdf, 0x76, 0x97, 0x45, 0x96, 0xce,
0xda, 0x2f, 0x93, 0xb4, 0xae, 0x11, 0x87, 0xc7, 0x37, 0x17, 0x83, 0xe3, 0x75, 0x9d, 0xfa, 0xc9,
0xff, 0x4c, 0x57, 0x05, 0xc3, 0xc7, 0x6b, 0x0f, 0x09, 0x8f, 0xd7, 0x10, 0x85, 0xf5, 0x99, 0x30,
0x71, 0x10, 0xaf, 0xf8, 0x92, 0x18, 0xaf, 0x8d, 0x38, 0x5c, 0x1f, 0x17, 0xb3, 0x6b, 0x03, 0xe3,
0x61, 0x9c, 0x0b, 0x56, 0xe6, 0x71, 0xb6, 0x97, 0xc5, 0xf3, 0x6a, 0x40, 0x8c, 0x31, 0x3e, 0x45,
0xac, 0x0d, 0x68, 0x1a, 0x79, 0x8c, 0xe3, 0x6a, 0x2f, 0xbe, 0xe2, 0x65, 0x2a, 0xe8, 0xc7, 0x68,
0x91, 0xce, 0xc7, 0xe8, 0xa1, 0xa8, 0xb7, 0x51, 0x39, 0xbb, 0x48, 0xaf, 0x58, 0x12, 0xf0, 0xd6,
0x20, 0x3d, 0xbc, 0x39, 0x28, 0xd2, 0x68, 0x13, 0xbe, 0x2c, 0x67, 0x8c, 0x6c, 0x34, 0x25, 0xee,
0x6c, 0x34, 0x83, 0x69, 0x0f, 0x7f, 0xb5, 0x16, 0xfd, 0x96, 0x92, 0xba, 0x6f, 0x78, 0x76, 0xe3,
0xea, 0xe2, 0x8c, 0xc7, 0x65, 0x32, 0xf8, 0x10, 0xb3, 0x83, 0xa2, 0xc6, 0xf5, 0xe3, 0xeb, 0xa8,
0xc0, 0xc7, 0x5a, 0xe7, 0xdd, 0xb6, 0xc7, 0xa1, 0x8f, 0xd5, 0x43, 0xc2, 0x8f, 0x15, 0xa2, 0x70,
0x00, 0x91, 0x72, 0xb5, 0x01, 0xb8, 0x4e, 0xea, 0xfb, 0xbb, 0x80, 0x1b, 0x9d, 0x1c, 0x1c, 0x1f,
0x6b, 0xa1, 0x1f, 0x2d, 0x5b, 0x94, 0x0d, 0x3c, 0x62, 0x86, 0x7d, 0x71, 0xd2, 0xb3, 0xe9, 0x15,
0x61, 0xcf, 0xad, 0x9e, 0x31, 0xec, 0x8b, 0x13, 0x9e, 0x9d, 0x61, 0x2d, 0xe4, 0x19, 0x19, 0xda,
0x86, 0x7d, 0x71, 0x98, 0x7d, 0x69, 0xa6, 0x99, 0x17, 0x1e, 0x04, 0xec, 0xc0, 0xb9, 0x61, 0xb3,
0x17, 0xab, 0x1d, 0xfe, 0xcd, 0x5a, 0xf4, 0x5d, 0xeb, 0xf1, 0x90, 0x27, 0xe9, 0xf9, 0x4a, 0x41,
0x2f, 0xe2, 0x6c, 0xc9, 0xaa, 0xc1, 0x63, 0xca, 0x5a, 0x9b, 0x35, 0x25, 0x78, 0x72, 0x2d, 0x1d,
0xd8, 0x77, 0x46, 0x45, 0x91, 0xad, 0xa6, 0x6c, 0x51, 0x64, 0x64, 0xdf, 0xf1, 0x90, 0x70, 0xdf,
0x81, 0x28, 0xcc, 0xca, 0xa7, 0xbc, 0xce, 0xf9, 0xd1, 0xac, 0x5c, 0x8a, 0xc2, 0x59, 0x79, 0x83,
0xc0, 0x5c, 0x69, 0xca, 0x77, 0x78, 0x96, 0xb1, 0x99, 0x68, 0x9f, 0x12, 0x31, 0x9a, 0x96, 0x08,
0xe7, 0x4a, 0x80, 0xb4, 0xbb, 0x72, 0xcd, 0x1a, 0x32, 0x2e, 0xd9, 0xd3, 0xd5, 0x41, 0x9a, 0x5f,
0x0e, 0xf0, 0xb4, 0xc0, 0x02, 0xc4, 0xae, 0x1c, 0x0a, 0xc2, 0xb5, 0xea, 0x69, 0x9e, 0x70, 0x7c,
0xad, 0x5a, 0x4b, 0xc2, 0x6b, 0x55, 0x4d, 0x40, 0x93, 0x27, 0x8c, 0x32, 0x59, 0x4b, 0xc2, 0x26,
0x35, 0x81, 0x0d, 0x85, 0xfa, 0x4d, 0x11, 0x39, 0x14, 0x82, 0x77, 0x43, 0x1b, 0x9d, 0x1c, 0x8c,
0xd0, 0x66, 0xd1, 0xba, 0xc7, 0xc4, 0xec, 0x02, 0x8f, 0x50, 0x0f, 0x09, 0x47, 0x28, 0x44, 0x61,
0x95, 0xa6, 0xdc, 0x2c, 0xba, 0xd7, 0xf1, 0xf8, 0x68, 0x2d, 0xb8, 0x37, 0x3a, 0x39, 0xb8, 0x8c,
0x1c, 0x2f, 0xe4, 0x33, 0x43, 0x83, 0x5c, 0xc9, 0xc2, 0xcb, 0x48, 0xc3, 0xc0, 0xd2, 0x2b, 0x81,
0xdc, 0xcb, 0x5a, 0xa7, 0x15, 0xbd, 0xdd, 0xac, 0x8d, 0x4e, 0x4e, 0x3b, 0xf9, 0x67, 0xb3, 0x8c,
0x53, 0xd2, 0x23, 0x5e, 0xf7, 0x91, 0x17, 0x71, 0x96, 0x26, 0xb1, 0x60, 0x53, 0x7e, 0xc9, 0x72,
0x7c, 0xc5, 0xa4, 0x4b, 0xab, 0xf8, 0xa1, 0xa7, 0x10, 0x5e, 0x31, 0x85, 0x15, 0x61, 0x9c, 0x28,
0xfa, 0xb4, 0x62, 0x3b, 0x71, 0x45, 0x8c, 0x64, 0x1e, 0x12, 0x8e, 0x13, 0x88, 0xc2, 0x7c, 0x55,
0xc9, 0x9f, 0xbd, 0x2e, 0x58, 0x99, 0xb2, 0x7c, 0xc6, 0xf0, 0x7c, 0x15, 0x52, 0xe1, 0x7c, 0x15,
0xa1, 0xe1, 0x5a, 0x6d, 0x37, 0x16, 0xec, 0xe9, 0x6a, 0x9a, 0x2e, 0x58, 0x25, 0xe2, 0x45, 0x81,
0xaf, 0xd5, 0x00, 0x14, 0x5e, 0xab, 0xb5, 0xe1, 0xd6, 0xd6, 0x90, 0x19, 0x10, 0xdb, 0x87, 0xcb,
0x20, 0x11, 0x38, 0x5c, 0x46, 0xa0, 0xf0, 0xc1, 0x5a, 0x00, 0x7d, 0x49, 0xd0, 0xb2, 0x12, 0x7c,
0x49, 0x40, 0xd3, 0xad, 0x0d, 0x37, 0xc3, 0x4c, 0xea, 0xae, 0xd9, 0x51, 0xf4, 0x89, 0xdb, 0x45,
0x37, 0x7b, 0xb1, 0xf8, 0x0e, 0xdf, 0x09, 0xcb, 0x62, 0x39, 0x6d, 0x05, 0xb6, 0xd1, 0x1a, 0xa6,
0xcf, 0x0e, 0x9f, 0xc3, 0x6a, 0x87, 0x7f, 0xb1, 0x16, 0xbd, 0x87, 0x79, 0x7c, 0x5e, 0x48, 0xbf,
0x8f, 0xba, 0x6d, 0x29, 0x92, 0x38, 0x3d, 0x17, 0xd6, 0xb0, 0x07, 0x40, 0x1a, 0x91, 0x3d, 0x5c,
0xa7, 0x0b, 0xe0, 0x27, 0x6d, 0xa6, 0xfc, 0x90, 0x23, 0x0e, 0x80, 0x84, 0x78, 0xbb, 0x1e, 0xf2,
0xcb, 0x55, 0x81, 0xf5, 0x90, 0xb1, 0xa1, 0xc5, 0xc4, 0x7a, 0x08, 0xc1, 0x6c, 0xef, 0x74, 0xab,
0xf7, 0x32, 0x15, 0x17, 0x32, 0xdf, 0x02, 0xbd, 0xd3, 0x2b, 0xab, 0x81, 0x88, 0xde, 0x49, 0xc2,
0x30, 0x23, 0x69, 0xc0, 0xba, 0x6f, 0x62, 0x63, 0xb9, 0x31, 0xe4, 0xf6, 0xcc, 0x7b, 0xdd, 0x20,
0x8c, 0xd7, 0x46, 0xac, 0x97, 0x3e, 0x0f, 0x42, 0x16, 0xc0, 0xf2, 0x67, 0xb3, 0x17, 0xab, 0x1d,
0xfe, 0x59, 0xf4, 0x9d, 0x56, 0xc5, 0xf6, 0x58, 0x2c, 0x96, 0x25, 0x4b, 0x06, 0xdb, 0x1d, 0xe5,
0x6e, 0x40, 0xe3, 0xfa, 0x51, 0x7f, 0x85, 0x56, 0x8e, 0xde, 0x70, 0x2a, 0xac, 0x4c, 0x19, 0x1e,
0x87, 0x4c, 0xfa, 0x6c, 0x30, 0x47, 0xa7, 0x75, 0x5a, 0xcb, 0x6c, 0x37, 0xba, 0x46, 0x57, 0x71,
0x9a, 0xc9, 0x97, 0xb5, 0x1f, 0x86, 0x8c, 0x7a, 0x68, 0x70, 0x99, 0x4d, 0xaa, 0xb4, 0x46, 0x66,
0xd9, 0xc7, 0x9d, 0xe5, 0xd9, 0x43, 0x7a, 0x24, 0x40, 0x56, 0x67, 0x5b, 0x3d, 0x69, 0xed, 0x56,
0x34, 0x53, 0x5e, 0xfd, 0x67, 0x37, 0xc8, 0x31, 0xaf, 0x5a, 0x15, 0x89, 0xf4, 0xad, 0x9e, 0xb4,
0xf6, 0xfa, 0xa7, 0xd1, 0xbb, 0x6d, 0xaf, 0x7a, 0x22, 0xda, 0xee, 0x34, 0x05, 0xe6, 0xa2, 0x47,
0xfd, 0x15, 0xb4, 0xfb, 0x7f, 0x33, 0xfb, 0xd2, 0xca, 0xff, 0x8c, 0x2f, 0x16, 0x2c, 0x4f, 0x58,
0xd2, 0x68, 0x54, 0xf5, 0xfa, 0xe9, 0x33, 0xda, 0xae, 0x51, 0x18, 0xba, 0x1a, 0xa6, 0x44, 0xbf,
0xfd, 0x0d, 0x34, 0x75, 0xd1, 0xfe, 0x6b, 0x2d, 0xba, 0x8f, 0x16, 0xad, 0x09, 0x5c, 0xaf, 0x88,
0xbf, 0xd7, 0xc7, 0x11, 0xa6, 0x69, 0x8a, 0x3a, 0xfa, 0x39, 0x2c, 0xe8, 0x22, 0xff, 0xfb, 0x5a,
0x74, 0xdb, 0x2a, 0xd6, 0xe1, 0xbd, 0xc3, 0xf3, 0xf3, 0x2c, 0x9d, 0x09, 0xf9, 0x46, 0x56, 0xab,
0xd0, 0x8f, 0x93, 0xd2, 0xe8, 0x7e, 0x9c, 0x01, 0x4d, 0xbb, 0x78, 0xfd, 0x3c, 0xad, 0x04, 0x2f,
0x57, 0x93, 0x0b, 0xfe, 0xaa, 0xf9, 0x3c, 0xc9, 0x1f, 0x97, 0x35, 0x30, 0x74, 0x08, 0x62, 0xf1,
0x8a, 0x93, 0x2d, 0x57, 0xf6, 0x33, 0xa6, 0x8a, 0x70, 0xe5, 0x10, 0x1d, 0xae, 0x7c, 0xd2, 0xce,
0x4a, 0x4d, 0xad, 0xec, 0x37, 0x57, 0x1b, 0x78, 0x51, 0xdb, 0xdf, 0x5d, 0xdd, 0xeb, 0x06, 0x6d,
0x6e, 0xaa, 0xc5, 0xbb, 0xe9, 0xf9, 0xb9, 0xa9, 0x13, 0x5e, 0x52, 0x17, 0x21, 0x72, 0x53, 0x02,
0xb5, 0xcb, 0xab, 0xbd, 0x34, 0x63, 0xf2, 0xdd, 0xcd, 0xf3, 0xf3, 0xf3, 0x8c, 0xc7, 0x09, 0x58,
0x5e, 0xd5, 0xe2, 0xa1, 0x2b, 0x27, 0x96, 0x57, 0x18, 0x67, 0x4f, 0x85, 0xd4, 0xd2, 0x3a, 0xba,
0xf3, 0x59, 0x9a, 0xc1, 0xd3, 0xcd, 0x52, 0xd3, 0x08, 0x89, 0x53, 0x21, 0x2d, 0xc8, 0xa6, 0x40,
0xb5, 0xa8, 0x8e, 0xca, 0xa6, 0xfc, 0x77, 0xdb, 0x8a, 0x8e, 0x98, 0x48, 0x81, 0x10, 0xcc, 0xee,
0x32, 0xd4, 0xc2, 0xd3, 0x42, 0x1a, 0xbf, 0xd5, 0xd6, 0x52, 0x12, 0x62, 0x97, 0xc1, 0x27, 0xec,
0x6a, 0xb9, 0xfe, 0xfb, 0x2e, 0x7f, 0x95, 0x4b, 0xa3, 0xb7, 0xdb, 0x2a, 0x8d, 0x8c, 0x58, 0x2d,
0x43, 0x46, 0x1b, 0xfe, 0x22, 0xfa, 0x45, 0x69, 0xb8, 0xe4, 0xc5, 0xe0, 0x06, 0xa2, 0x50, 0x3a,
0x67, 0x81, 0x6f, 0x92, 0x72, 0x7b, 0x88, 0xc4, 0xc4, 0xc6, 0x69, 0x15, 0xcf, 0xe1, 0x01, 0x7e,
0xdb, 0xe2, 0x52, 0x4a, 0x1c, 0x22, 0x69, 0x53, 0x7e, 0x54, 0x1c, 0xf1, 0x44, 0x5b, 0x47, 0x6a,
0x68, 0x84, 0xa1, 0xa8, 0x70, 0x21, 0x9b, 0xb6, 0x1e, 0xc5, 0x57, 0xe9, 0xdc, 0xa4, 0x16, 0x6a,
0x00, 0xab, 0x40, 0xda, 0x6a, 0x99, 0xa1, 0x03, 0x11, 0x69, 0x2b, 0x09, 0x6b, 0x9f, 0xff, 0xb4,
0x16, 0xdd, 0xb2, 0xcc, 0x7e, 0xb3, 0x2f, 0x3b, 0xce, 0xcf, 0x79, 0x9d, 0xe4, 0x1e, 0xa4, 0xf9,
0x65, 0x35, 0xf8, 0x84, 0x32, 0x89, 0xf3, 0xa6, 0x28, 0x9f, 0x5e, 0x5b, 0xcf, 0xae, 0x4f, 0x9a,
0x4d, 0x4b, 0x7b, 0x72, 0x41, 0x69, 0x80, 0xf5, 0x89, 0xd9, 0xdb, 0x84, 0x1c, 0xb1, 0x3e, 0x09,
0xf1, 0xb6, 0x89, 0x8d, 0xf3, 0x8c, 0xe7, 0xb0, 0x89, 0xad, 0x85, 0x5a, 0x48, 0x34, 0x71, 0x0b,
0xb2, 0xe3, 0x71, 0x23, 0x52, 0xfb, 0x6b, 0xa3, 0x2c, 0x03, 0xe3, 0xb1, 0x51, 0x35, 0x00, 0x31,
0x1e, 0xa3, 0xa0, 0xf6, 0x73, 0x12, 0x7d, 0xab, 0x7e, 0xa4, 0xc7, 0x25, 0xbb, 0x4a, 0x19, 0x3c,
0x64, 0xe3, 0x48, 0x88, 0xfe, 0xef, 0x13, 0xb6, 0x67, 0x9d, 0xe6, 0x55, 0x91, 0xc5, 0xd5, 0x85,
0x3e, 0x76, 0xe1, 0xd7, 0xb9, 0x11, 0xc2, 0x83, 0x17, 0x77, 0x3b, 0x28, 0x3b, 0xa8, 0x37, 0x32,
0x33, 0xc4, 0xac, 0xe3, 0xaa, 0xad, 0x61, 0x66, 0xa3, 0x93, 0xb3, 0xef, 0x36, 0xf6, 0xe3, 0x2c,
0x63, 0xe5, 0xaa, 0x91, 0x1d, 0xc6, 0x79, 0x7a, 0xce, 0x2a, 0x01, 0xde, 0x6d, 0x68, 0x6a, 0x08,
0x31, 0xe2, 0xdd, 0x46, 0x00, 0xb7, 0xeb, 0x36, 0xe0, 0x79, 0x9c, 0x27, 0xec, 0x35, 0x58, 0xb7,
0x41, 0x3b, 0x92, 0x21, 0xd6, 0x6d, 0x14, 0x6b, 0xf7, 0xf8, 0x9f, 0x66, 0x7c, 0x76, 0xa9, 0xa7,
0x00, 0xbf, 0x81, 0xa5, 0x04, 0xce, 0x01, 0xb7, 0x43, 0x88, 0x9d, 0x04, 0xa4, 0xe0, 0x84, 0x15,
0x59, 0x3c, 0x83, 0x27, 0xad, 0x94, 0x8e, 0x96, 0x11, 0x93, 0x00, 0x64, 0x40, 0x71, 0xf5, 0x09,
0x2e, 0xac, 0xb8, 0xe0, 0x00, 0xd7, 0xed, 0x10, 0x62, 0xa7, 0x41, 0x29, 0x98, 0x14, 0x59, 0x2a,
0x40, 0x37, 0x50, 0x1a, 0x52, 0x42, 0x74, 0x03, 0x9f, 0x00, 0x26, 0x0f, 0x59, 0x39, 0x67, 0xa8,
0x49, 0x29, 0x09, 0x9a, 0x6c, 0x08, 0x7b, 0xac, 0x5c, 0xd5, 0x9d, 0x17, 0x2b, 0x70, 0xac, 0x5c,
0x57, 0x8b, 0x17, 0x2b, 0xe2, 0x58, 0xb9, 0x07, 0x80, 0x22, 0x1e, 0xc7, 0x95, 0xc0, 0x8b, 0x28,
0x25, 0xc1, 0x22, 0x36, 0x84, 0x9d, 0xa3, 0x55, 0x11, 0x97, 0x02, 0xcc, 0xd1, 0xba, 0x00, 0xce,
0x59, 0x83, 0x9b, 0xa4, 0xdc, 0x8e, 0x24, 0xaa, 0x55, 0x98, 0xd8, 0x4b, 0x59, 0x96, 0x54, 0x60,
0x24, 0xd1, 0xcf, 0xbd, 0x91, 0x12, 0x23, 0x49, 0x9b, 0x02, 0xa1, 0xa4, 0xdf, 0x84, 0x60, 0xb5,
0x03, 0x2f, 0x41, 0x6e, 0x87, 0x10, 0x3b, 0x3e, 0x35, 0x85, 0xde, 0x89, 0xcb, 0x32, 0xad, 0x27,
0xff, 0x75, 0xbc, 0x40, 0x8d, 0x9c, 0x18, 0x9f, 0x30, 0x0e, 0x74, 0xaf, 0x66, 0xe0, 0xc6, 0x0a,
0x06, 0x87, 0xee, 0x0f, 0x82, 0x8c, 0xcd, 0x38, 0xa5, 0xc4, 0x79, 0x59, 0x8e, 0x3d, 0x4d, 0xe4,
0x5d, 0xf9, 0x7a, 0x17, 0xe6, 0x7c, 0xe1, 0x66, 0x5c, 0x1c, 0xf2, 0x2b, 0x36, 0xe5, 0xcf, 0x5e,
0xa7, 0x55, 0xbd, 0xdc, 0xd2, 0x33, 0xf7, 0x13, 0xc2, 0x12, 0x06, 0x13, 0x5f, 0xb8, 0x75, 0x2a,
0xd9, 0x04, 0x02, 0x94, 0xe5, 0x88, 0xbd, 0x42, 0x13, 0x08, 0x68, 0xd1, 0x70, 0x44, 0x02, 0x11,
0xe2, 0xed, 0x8e, 0x99, 0x71, 0xae, 0xaf, 0x35, 0x98, 0xf2, 0x26, 0x97, 0xa3, 0xac, 0x41, 0x90,
0xd8, 0xb4, 0x08, 0x2a, 0xd8, 0xf5, 0xa5, 0xf1, 0x6f, 0xbb, 0xd8, 0x3d, 0xc2, 0x4e, 0xbb, 0x9b,
0xdd, 0xef, 0x41, 0x22, 0xae, 0xec, 0x89, 0x0f, 0xca, 0x55, 0xfb, 0xc0, 0xc7, 0xfd, 0x1e, 0xa4,
0xb3, 0xfb, 0xe6, 0x56, 0xeb, 0x69, 0x3c, 0xbb, 0x9c, 0x97, 0x7c, 0x99, 0x27, 0x3b, 0x3c, 0xe3,
0x25, 0xd8, 0x7d, 0xf3, 0x4a, 0x0d, 0x50, 0x62, 0xf7, 0xad, 0x43, 0xc5, 0x66, 0x70, 0x6e, 0x29,
0x46, 0x59, 0x3a, 0x87, 0x2b, 0x6a, 0xcf, 0x90, 0x04, 0x88, 0x0c, 0x0e, 0x05, 0x91, 0x20, 0x52,
0x2b, 0x6e, 0x91, 0xce, 0xe2, 0x4c, 0xf9, 0xdb, 0xa6, 0xcd, 0x78, 0x60, 0x67, 0x10, 0x21, 0x0a,
0x48, 0x3d, 0xa7, 0xcb, 0x32, 0x1f, 0xe7, 0x82, 0x93, 0xf5, 0x6c, 0x80, 0xce, 0x7a, 0x3a, 0x20,
0x18, 0x56, 0xa7, 0xec, 0x75, 0x5d, 0x9a, 0xfa, 0x1f, 0x6c, 0x58, 0xad, 0xff, 0x3e, 0xd4, 0xf2,
0xd0, 0xb0, 0x0a, 0x38, 0x50, 0x19, 0xed, 0x44, 0x05, 0x4c, 0x40, 0xdb, 0x0f, 0x93, 0x7b, 0xdd,
0x20, 0xee, 0x67, 0x22, 0x56, 0x19, 0x0b, 0xf9, 0x91, 0x40, 0x1f, 0x3f, 0x0d, 0x68, 0xb7, 0x5b,
0xbc, 0xfa, 0x5c, 0xb0, 0xd9, 0x65, 0xeb, 0x00, 0x9b, 0x5f, 0x50, 0x85, 0x10, 0xdb, 0x2d, 0x04,
0x8a, 0x37, 0xd1, 0x78, 0xc6, 0xf3, 0x50, 0x13, 0xd5, 0xf2, 0x3e, 0x4d, 0xa4, 0x39, 0xbb, 0xf8,
0x35, 0x52, 0x1d, 0x99, 0xaa, 0x99, 0x36, 0x09, 0x0b, 0x2e, 0x44, 0x2c, 0x7e, 0x49, 0xd8, 0xe6,
0xe4, 0xd0, 0xe7, 0x61, 0xfb, 0x74, 0x7f, 0xcb, 0xca, 0x21, 0x7d, 0xba, 0x9f, 0x62, 0xe9, 0x4a,
0xaa, 0x18, 0xe9, 0xb0, 0xe2, 0xc7, 0xc9, 0xc3, 0x7e, 0xb0, 0x5d, 0xf2, 0x78, 0x3e, 0x77, 0x32,
0x16, 0x97, 0xca, 0xeb, 0x56, 0xc0, 0x90, 0xc5, 0x88, 0x25, 0x4f, 0x00, 0x07, 0x43, 0x98, 0xe7,
0x79, 0x87, 0xe7, 0x82, 0xe5, 0x02, 0x1b, 0xc2, 0x7c, 0x63, 0x1a, 0x0c, 0x0d, 0x61, 0x94, 0x02,
0x88, 0x5b, 0xb9, 0x1f, 0xc4, 0xc4, 0x51, 0xbc, 0x40, 0x33, 0x36, 0xb5, 0xd7, 0xa3, 0xe4, 0xa1,
0xb8, 0x05, 0x9c, 0xf3, 0x3a, 0xd7, 0xf5, 0x32, 0x8d, 0xcb, 0xb9, 0xd9, 0xdd, 0x48, 0x06, 0x8f,
0x68, 0x3b, 0x3e, 0x49, 0xbc, 0xce, 0x0d, 0x6b, 0x80, 0x61, 0x67, 0xbc, 0x88, 0xe7, 0xa6, 0xa6,
0x48, 0x0d, 0xa4, 0xbc, 0x55, 0xd5, 0x7b, 0xdd, 0x20, 0xf0, 0xf3, 0x22, 0x4d, 0x18, 0x0f, 0xf8,
0x91, 0xf2, 0x3e, 0x7e, 0x20, 0x08, 0xb2, 0xb7, 0xba, 0xde, 0x6a, 0x45, 0x37, 0xca, 0x13, 0xbd,
0x8e, 0x1d, 0x12, 0x8f, 0x07, 0x70, 0xa1, 0xec, 0x8d, 0xe0, 0x41, 0x1f, 0x6d, 0x36, 0x68, 0x43,
0x7d, 0xd4, 0xec, 0xbf, 0xf6, 0xe9, 0xa3, 0x18, 0xac, 0x7d, 0xfe, 0x58, 0xf7, 0xd1, 0xdd, 0x58,
0xc4, 0x75, 0xde, 0xfe, 0x22, 0x65, 0xaf, 0xf4, 0x42, 0x18, 0xa9, 0x6f, 0x43, 0x0d, 0xe5, 0xc7,
0xc9, 0x60, 0x55, 0xbc, 0xdd, 0x9b, 0x0f, 0xf8, 0xd6, 0x2b, 0x84, 0x4e, 0xdf, 0x60, 0xa9, 0xb0,
0xdd, 0x9b, 0x0f, 0xf8, 0xd6, 0x77, 0x2c, 0x74, 0xfa, 0x06, 0x17, 0x2d, 0x6c, 0xf7, 0xe6, 0xb5,
0xef, 0xbf, 0x6c, 0x3a, 0xae, 0xeb, 0xbc, 0xce, 0xc3, 0x66, 0x22, 0xbd, 0x62, 0x58, 0x3a, 0xe9,
0xdb, 0x33, 0x68, 0x28, 0x9d, 0xa4, 0x55, 0x9c, 0x5b, 0xce, 0xb0, 0x52, 0x1c, 0xf3, 0x2a, 0x95,
0xc7, 0x31, 0x9e, 0xf4, 0x30, 0xda, 0xc0, 0xa1, 0x45, 0x53, 0x48, 0xc9, 0xbe, 0x58, 0xf6, 0x50,
0x7b, 0x5e, 0xfd, 0x61, 0xc0, 0x5e, 0xfb, 0xd8, 0xfa, 0x56, 0x4f, 0xda, 0xbe, 0xe2, 0xf5, 0x98,
0xe6, 0xe5, 0xdc, 0x84, 0xa1, 0xb3, 0x84, 0x31, 0x65, 0x5e, 0xda, 0xba, 0x6f, 0x29, 0x1f, 0xf5,
0x57, 0xe8, 0x70, 0x3f, 0x4a, 0x92, 0x7e, 0xee, 0xdd, 0xb7, 0xdb, 0x8f, 0xfa, 0x2b, 0x68, 0xf7,
0x7f, 0xdd, 0x2c, 0x6b, 0xa0, 0x7f, 0xdd, 0x07, 0x1f, 0xf7, 0xb1, 0x08, 0xfa, 0xe1, 0x93, 0x6b,
0xe9, 0xe8, 0x82, 0xfc, 0xa4, 0x59, 0xbf, 0x37, 0xa8, 0xfc, 0x68, 0x48, 0x7e, 0x6c, 0xac, 0xbb,
0x64, 0x28, 0xaa, 0x2c, 0x0c, 0x3b, 0xe6, 0xc7, 0xd7, 0xd4, 0x72, 0xae, 0xdc, 0xf3, 0x60, 0xfd,
0x71, 0xab, 0x53, 0x9e, 0x90, 0x65, 0x87, 0x86, 0x05, 0xfa, 0xe4, 0xba, 0x6a, 0x54, 0x57, 0x75,
0x60, 0x79, 0xe9, 0xcc, 0x93, 0x9e, 0x86, 0xbd, 0x6b, 0x68, 0x3e, 0xba, 0x9e, 0x92, 0x2e, 0xcb,
0x7f, 0xae, 0x45, 0x77, 0x3d, 0xd6, 0xbe, 0xce, 0x00, 0x9b, 0x2e, 0x3f, 0x08, 0xd8, 0xa7, 0x94,
0x4c, 0xe1, 0x7e, 0xe7, 0x9b, 0x29, 0xdb, 0xfb, 0xe9, 0x3c, 0x95, 0xbd, 0x34, 0x13, 0xac, 0x6c,
0xdf, 0x4f, 0xe7, 0xdb, 0x55, 0xd4, 0x90, 0xbe, 0x9f, 0x2e, 0x80, 0x3b, 0xf7, 0xd3, 0x21, 0x9e,
0xd1, 0xfb, 0xe9, 0x50, 0x6b, 0xc1, 0xfb, 0xe9, 0xc2, 0x1a, 0xd4, 0xec, 0xd2, 0x14, 0x41, 0x6d,
0x9b, 0xf7, 0xb2, 0xe8, 0xef, 0xa2, 0x3f, 0xbe, 0x8e, 0x0a, 0x31, 0xbf, 0x2a, 0x4e, 0x1e, 0xa8,
0xec, 0xf1, 0x4c, 0xbd, 0x43, 0x95, 0xdb, 0xbd, 0x79, 0xed, 0xfb, 0x47, 0x7a, 0x71, 0x65, 0x66,
0x13, 0x5e, 0xca, 0xbb, 0x09, 0x37, 0x43, 0xb3, 0x43, 0x6d, 0xc1, 0x6d, 0xf9, 0x87, 0xfd, 0x60,
0xa2, 0xba, 0x35, 0xa1, 0x1b, 0x7d, 0xd8, 0x65, 0x08, 0x34, 0xf9, 0x76, 0x6f, 0x9e, 0x98, 0x46,
0x94, 0x6f, 0xd5, 0xda, 0x3d, 0x8c, 0xf9, 0x6d, 0xfd, 0xa8, 0xbf, 0x82, 0x76, 0x7f, 0xa5, 0xb3,
0x56, 0xd7, 0xbd, 0x6c, 0xe7, 0xad, 0x2e, 0x53, 0x13, 0xaf, 0x99, 0x87, 0x7d, 0xf1, 0x50, 0xfe,
0xe2, 0x4e, 0xa1, 0x5d, 0xf9, 0x0b, 0x3a, 0x8d, 0x7e, 0x74, 0x3d, 0x25, 0x5d, 0x96, 0x7f, 0x5c,
0x8b, 0x6e, 0x92, 0x65, 0xd1, 0x71, 0xf0, 0x49, 0x5f, 0xcb, 0x20, 0x1e, 0x3e, 0xbd, 0xb6, 0x9e,
0x2e, 0xd4, 0xbf, 0xac, 0x45, 0xb7, 0x02, 0x85, 0x52, 0x01, 0x72, 0x0d, 0xeb, 0x7e, 0xa0, 0x7c,
0x76, 0x7d, 0x45, 0x6a, 0xba, 0x77, 0xf1, 0x49, 0xfb, 0xae, 0xb1, 0x80, 0xed, 0x09, 0x7d, 0xd7,
0x58, 0xb7, 0x16, 0xdc, 0x63, 0x8a, 0xcf, 0x9a, 0x35, 0x1f, 0xba, 0xc7, 0x24, 0x8f, 0x42, 0x06,
0x6f, 0x31, 0xc1, 0x38, 0xcc, 0xc9, 0xb3, 0xd7, 0x45, 0x9c, 0x27, 0xb4, 0x13, 0x25, 0xef, 0x76,
0x62, 0x38, 0xb8, 0x37, 0x57, 0x4b, 0x4f, 0x78, 0xb3, 0x8e, 0xbb, 0x4f, 0xe9, 0x1b, 0x24, 0xb8,
0x37, 0xd7, 0x42, 0x09, 0x6f, 0x3a, 0x6b, 0x0c, 0x79, 0x03, 0xc9, 0xe2, 0x83, 0x3e, 0x28, 0x58,
0x21, 0x18, 0x6f, 0x66, 0xcb, 0xff, 0x61, 0xc8, 0x4a, 0x6b, 0xdb, 0x7f, 0xab, 0x27, 0x4d, 0xb8,
0x9d, 0x30, 0xf1, 0x39, 0x8b, 0x13, 0x56, 0x06, 0xdd, 0x1a, 0xaa, 0x97, 0x5b, 0x97, 0xc6, 0xdc,
0xee, 0xf0, 0x6c, 0xb9, 0xc8, 0x75, 0x63, 0x92, 0x6e, 0x5d, 0xaa, 0xdb, 0x2d, 0xa0, 0xe1, 0xae,
0xa4, 0x75, 0x2b, 0xd3, 0xcb, 0x07, 0x61, 0x33, 0x5e, 0x56, 0xb9, 0xd9, 0x8b, 0xa5, 0xeb, 0xa9,
0xc3, 0xa8, 0xa3, 0x9e, 0x20, 0x92, 0xb6, 0x7a, 0xd2, 0x70, 0x7b, 0xd0, 0x71, 0x6b, 0xe2, 0x69,
0xbb, 0xc3, 0x56, 0x2b, 0xa4, 0x1e, 0xf5, 0x57, 0x80, 0x9b, 0xb1, 0x3a, 0xaa, 0x0e, 0xd2, 0x4a,
0xec, 0xa5, 0x59, 0x36, 0xd8, 0x0c, 0x84, 0x49, 0x03, 0x05, 0x37, 0x63, 0x11, 0x98, 0x88, 0xe4,
0x66, 0xf3, 0x32, 0x1f, 0x74, 0xd9, 0x91, 0x54, 0xaf, 0x48, 0x76, 0x69, 0xb0, 0xa1, 0xe6, 0x3c,
0x6a, 0x53, 0xdb, 0x61, 0xf8, 0xc1, 0xb5, 0x2a, 0xbc, 0xdd, 0x9b, 0x07, 0x6f, 0xfb, 0x25, 0x25,
0x67, 0x96, 0x3b, 0x94, 0x09, 0x6f, 0x26, 0xb9, 0xdb, 0x41, 0x81, 0x4d, 0x49, 0xd5, 0x8d, 0x5e,
0xa6, 0xc9, 0x9c, 0x09, 0xf4, 0x45, 0x95, 0x0b, 0x04, 0x5f, 0x54, 0x01, 0x10, 0x34, 0x9d, 0xfa,
0xbb, 0xd9, 0x8d, 0x1d, 0x27, 0x58, 0xd3, 0x69, 0x65, 0x87, 0x0a, 0x35, 0x1d, 0x4a, 0x83, 0xd1,
0xc0, 0xb8, 0xd5, 0xf7, 0x3e, 0x3c, 0x08, 0x99, 0x01, 0x97, 0x3f, 0x6c, 0xf6, 0x62, 0xc1, 0x8c,
0x62, 0x1d, 0xa6, 0x8b, 0x54, 0x60, 0x33, 0x8a, 0x63, 0xa3, 0x46, 0x42, 0x33, 0x4a, 0x1b, 0xa5,
0xaa, 0x57, 0xe7, 0x08, 0xe3, 0x24, 0x5c, 0x3d, 0xc5, 0xf4, 0xab, 0x9e, 0x61, 0x5b, 0xef, 0x55,
0x73, 0x13, 0x32, 0xe2, 0x42, 0x2f, 0x96, 0x91, 0xd8, 0x96, 0xdf, 0x03, 0x43, 0x30, 0x34, 0xea,
0x50, 0x0a, 0xf0, 0x7d, 0x41, 0xcd, 0x35, 0xaf, 0x7e, 0x8b, 0x82, 0xc5, 0x65, 0x9c, 0xcf, 0xd0,
0xc5, 0xa9, 0x34, 0xd8, 0x22, 0x43, 0x8b, 0x53, 0x52, 0x03, 0xbc, 0xb5, 0xf7, 0xbf, 0xe4, 0x45,
0xba, 0x82, 0xf9, 0x64, 0xd6, 0xff, 0x90, 0xf7, 0x7e, 0x0f, 0x12, 0xbe, 0xb5, 0x6f, 0x00, 0xb3,
0xef, 0xae, 0x9c, 0x7e, 0x18, 0x30, 0xe5, 0xa3, 0xa1, 0x85, 0x30, 0xad, 0x02, 0x82, 0xda, 0xd9,
0x5b, 0xfc, 0x82, 0xad, 0xb0, 0xa0, 0x76, 0x37, 0x09, 0xbf, 0x60, 0xab, 0x50, 0x50, 0xb7, 0x51,
0x90, 0x67, 0xba, 0xeb, 0xa0, 0xf5, 0x80, 0xbe, 0xbb, 0xf4, 0xd9, 0xe8, 0xe4, 0x40, 0xcf, 0xd9,
0x4d, 0xaf, 0xbc, 0xd7, 0x14, 0x48, 0x41, 0x77, 0xd3, 0x2b, 0xfc, 0x2d, 0xc5, 0x66, 0x2f, 0x16,
0x9e, 0x08, 0x88, 0x05, 0x7b, 0xdd, 0xbc, 0xaa, 0x47, 0x8a, 0x2b, 0xe5, 0xad, 0x77, 0xf5, 0xf7,
0xba, 0x41, 0x7b, 0xfe, 0xf6, 0xb8, 0xe4, 0x33, 0x56, 0x55, 0xfa, 0x4a, 0x54, 0xff, 0x80, 0x93,
0x96, 0x0d, 0xc1, 0x85, 0xa8, 0x77, 0xc2, 0x90, 0x73, 0x8f, 0xa1, 0x12, 0xd9, 0xeb, 0x95, 0xd6,
0x51, 0xcd, 0xf6, 0xcd, 0x4a, 0x1b, 0x9d, 0x9c, 0xed, 0x5e, 0x5a, 0xea, 0xde, 0xa7, 0x74, 0x0f,
0x55, 0xc7, 0xae, 0x52, 0xba, 0xdf, 0x83, 0xd4, 0xae, 0x3e, 0x8f, 0xde, 0x3c, 0xe0, 0xf3, 0x09,
0xcb, 0x93, 0xc1, 0xf7, 0xfc, 0x13, 0xbc, 0x7c, 0x3e, 0xac, 0xff, 0x6c, 0x8c, 0xde, 0xa0, 0xc4,
0xf6, 0x0c, 0xe2, 0x2e, 0x3b, 0x5b, 0xce, 0x27, 0x22, 0x16, 0xe0, 0x0c, 0xa2, 0xfc, 0xfb, 0xb0,
0x16, 0x10, 0x67, 0x10, 0x3d, 0x00, 0xd8, 0x9b, 0x96, 0x8c, 0xa1, 0xf6, 0x6a, 0x41, 0xd0, 0x9e,
0x06, 0x6c, 0x16, 0x61, 0xec, 0xd5, 0x89, 0x3a, 0x3c, 0x33, 0x68, 0x75, 0xa4, 0x94, 0xc8, 0x22,
0xda, 0x94, 0x0d, 0x6e, 0x55, 0x7d, 0x79, 0xbd, 0xcd, 0x72, 0xb1, 0x88, 0xcb, 0x15, 0x08, 0x6e,
0x5d, 0x4b, 0x07, 0x20, 0x82, 0x1b, 0x05, 0x6d, 0xaf, 0x6d, 0x1e, 0xf3, 0xec, 0x72, 0x9f, 0x97,
0x7c, 0x29, 0xd2, 0x9c, 0xc1, 0x2b, 0x4e, 0xcc, 0x03, 0x75, 0x19, 0xa2, 0xd7, 0x52, 0xac, 0xcd,
0x72, 0x25, 0xa1, 0x8e, 0x33, 0xca, 0x3b, 0xe1, 0x2b, 0xc1, 0x4b, 0xf8, 0x3a, 0x53, 0x59, 0x81,
0x10, 0x91, 0xe5, 0x92, 0x30, 0x68, 0xfb, 0xe3, 0x34, 0x9f, 0xa3, 0x6d, 0x7f, 0xec, 0x5e, 0x33,
0x7c, 0x8b, 0x06, 0x6c, 0x87, 0x52, 0x0f, 0x4d, 0x75, 0x00, 0xfd, 0xd1, 0x30, 0xfa, 0xd0, 0x5d,
0x82, 0xe8, 0x50, 0x38, 0x09, 0x5c, 0x3d, 0x2f, 0x58, 0xce, 0x92, 0xe6, 0xd0, 0x1e, 0xe6, 0xca,
0x23, 0x82, 0xae, 0x20, 0x69, 0xc7, 0x22, 0x29, 0x3f, 0x59, 0xe6, 0xc7, 0x25, 0x3f, 0x4f, 0x33,
0x56, 0x82, 0xb1, 0x48, 0xa9, 0x3b, 0x72, 0x62, 0x2c, 0xc2, 0x38, 0x7b, 0xfa, 0x43, 0x4a, 0xbd,
0x1f, 0x36, 0x98, 0x96, 0xf1, 0x0c, 0x9e, 0xfe, 0x50, 0x36, 0xda, 0x18, 0xb1, 0x33, 0x18, 0xc0,
0x9d, 0x44, 0x47, 0xb9, 0xce, 0x57, 0x32, 0x3e, 0xf4, 0x47, 0xab, 0xf2, 0xf2, 0xdd, 0x0a, 0x24,
0x3a, 0xda, 0x1c, 0x46, 0x12, 0x89, 0x4e, 0x58, 0xc3, 0x4e, 0x25, 0x92, 0x3b, 0xd2, 0xa7, 0x9a,
0xc0, 0x54, 0xa2, 0x6c, 0x34, 0x42, 0x62, 0x2a, 0x69, 0x41, 0x60, 0x40, 0x6a, 0xba, 0xc1, 0x1c,
0x1d, 0x90, 0x8c, 0x34, 0x38, 0x20, 0xb9, 0x94, 0x1d, 0x28, 0xc6, 0x79, 0x2a, 0xd2, 0x38, 0x9b,
0x30, 0x71, 0x1c, 0x97, 0xf1, 0x82, 0x09, 0x56, 0xc2, 0x81, 0x42, 0x23, 0x43, 0x8f, 0x21, 0x06,
0x0a, 0x8a, 0xd5, 0x0e, 0x7f, 0x37, 0x7a, 0xbb, 0x9e, 0xf7, 0x59, 0xae, 0x7f, 0x92, 0xe9, 0x99,
0xfc, 0x2d, 0xb7, 0xc1, 0x3b, 0xc6, 0xc6, 0x44, 0x94, 0x2c, 0x5e, 0x34, 0xb6, 0xdf, 0x32, 0x7f,
0x97, 0xe0, 0xa3, 0xb5, 0x3a, 0x9e, 0x8f, 0xb8, 0x48, 0xcf, 0xeb, 0x65, 0xb6, 0xfe, 0x80, 0x09,
0xc4, 0xb3, 0x2b, 0x1e, 0x06, 0x2e, 0x3d, 0xc1, 0x38, 0x3b, 0x4e, 0xbb, 0xd2, 0x13, 0x56, 0x64,
0x70, 0x9c, 0xf6, 0xb4, 0x25, 0x40, 0x8c, 0xd3, 0x28, 0x68, 0x3b, 0xa7, 0x2b, 0x9e, 0xb2, 0x70,
0x65, 0xa6, 0xac, 0x5f, 0x65, 0xa6, 0xde, 0x37, 0x21, 0x59, 0xf4, 0xf6, 0x21, 0x5b, 0x9c, 0xb1,
0xb2, 0xba, 0x48, 0x0b, 0xea, 0x82, 0x60, 0x4b, 0x74, 0x5e, 0x10, 0x4c, 0xa0, 0x76, 0x26, 0xb0,
0xc0, 0xb8, 0x3a, 0x8a, 0x17, 0x4c, 0x5e, 0xe1, 0x02, 0x66, 0x02, 0xc7, 0x88, 0x03, 0x11, 0x33,
0x01, 0x09, 0x3b, 0x9f, 0x97, 0x59, 0xe6, 0x84, 0xcd, 0xeb, 0x08, 0x2b, 0x8f, 0xe3, 0xd5, 0x82,
0xe5, 0x42, 0x9b, 0x04, 0x7b, 0xf2, 0x8e, 0x49, 0x9c, 0x27, 0xf6, 0xe4, 0xfb, 0xe8, 0x39, 0x43,
0x93, 0xf7, 0xe0, 0x8f, 0x79, 0x29, 0xd4, 0x0f, 0xae, 0x9d, 0x96, 0x19, 0x18, 0x9a, 0xfc, 0x87,
0xea, 0x91, 0xc4, 0xd0, 0x14, 0xd6, 0x70, 0x7e, 0x5c, 0xc3, 0x2b, 0xc3, 0x0b, 0x56, 0x9a, 0x38,
0x79, 0xb6, 0x88, 0xd3, 0x4c, 0x47, 0xc3, 0xf7, 0x03, 0xb6, 0x09, 0x1d, 0xe2, 0xc7, 0x35, 0xfa,
0xea, 0x3a, 0x3f, 0x47, 0x12, 0x2e, 0x21, 0x78, 0x45, 0xd0, 0x61, 0x9f, 0x78, 0x45, 0xd0, 0xad,
0x65, 0x57, 0xee, 0x96, 0x95, 0xdc, 0x4a, 0x12, 0x3b, 0x3c, 0x81, 0xfb, 0x85, 0x8e, 0x4d, 0x00,
0x12, 0x2b, 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, 0xa1,
0xff, 0x5e, 0xe0, 0xb9, 0x49, 0xa2, 0xdb, 0x95, 0x43, 0x3a, 0x97, 0x01, 0xc3, 0xc7, 0x3a, 0x2a,
0x8a, 0x49, 0x3d, 0xab, 0x9e, 0xb0, 0x19, 0x4b, 0x0b, 0x31, 0xf8, 0x38, 0xfc, 0xac, 0x00, 0x4e,
0x1c, 0xb4, 0xe8, 0xa1, 0xe6, 0xbc, 0xbe, 0xaf, 0xc7, 0x92, 0x89, 0xfa, 0x25, 0xd2, 0xd3, 0x8a,
0x95, 0x3a, 0xd1, 0xd8, 0x67, 0x02, 0xf4, 0x4e, 0x87, 0x1b, 0x3a, 0x60, 0x5d, 0x51, 0xa2, 0x77,
0x86, 0x35, 0xec, 0x66, 0x9f, 0xc3, 0xe9, 0xcb, 0xdd, 0xe5, 0x71, 0xc7, 0x87, 0xa4, 0x31, 0x87,
0x22, 0x36, 0xfb, 0x68, 0xda, 0x66, 0x6b, 0x6d, 0xb7, 0xa3, 0x7c, 0x35, 0x86, 0x47, 0x26, 0x10,
0x4b, 0x12, 0x23, 0xb2, 0xb5, 0x00, 0xee, 0x6c, 0x86, 0x97, 0x3c, 0x4e, 0x66, 0x71, 0x25, 0x8e,
0xe3, 0x55, 0xc6, 0xe3, 0x44, 0xce, 0xeb, 0x70, 0x33, 0xbc, 0x61, 0x86, 0x2e, 0x44, 0x6d, 0x86,
0x53, 0xb0, 0x9b, 0x9d, 0xc9, 0x1f, 0x58, 0xd5, 0x47, 0x49, 0x61, 0x76, 0x26, 0xcb, 0x0b, 0x8f,
0x91, 0xde, 0x09, 0x43, 0xf6, 0x13, 0x38, 0x25, 0x92, 0x69, 0xc8, 0x2d, 0x4c, 0xc7, 0x4b, 0x40,
0xde, 0x0f, 0x10, 0xf6, 0x02, 0x14, 0xf5, 0xf7, 0xe6, 0x37, 0xb5, 0x84, 0xbe, 0x32, 0xfd, 0x21,
0xa6, 0xeb, 0x42, 0xde, 0x09, 0xb5, 0xad, 0x9e, 0xb4, 0x4d, 0x33, 0x77, 0x2e, 0x62, 0x31, 0x4a,
0x92, 0x43, 0x56, 0x21, 0xdf, 0xb3, 0xd7, 0xc2, 0xa1, 0x95, 0x12, 0x69, 0x66, 0x9b, 0xb2, 0x81,
0x5e, 0xcb, 0x9e, 0x25, 0xa9, 0xd0, 0xb2, 0xe6, 0x80, 0xf6, 0xc3, 0xb6, 0x81, 0x36, 0x45, 0xd4,
0x8a, 0xa6, 0xed, 0x58, 0x5e, 0x33, 0x53, 0x3e, 0x9f, 0x67, 0x4c, 0x43, 0x27, 0x2c, 0x56, 0x37,
0x46, 0x6e, 0xb7, 0x6d, 0xa1, 0x20, 0x31, 0x96, 0x07, 0x15, 0x6c, 0x1a, 0x59, 0x63, 0xea, 0x95,
0x54, 0xf3, 0x60, 0x37, 0xda, 0x66, 0x3c, 0x80, 0x48, 0x23, 0x51, 0xd0, 0x7e, 0x76, 0x57, 0x8b,
0xf7, 0x59, 0xf3, 0x24, 0xe0, 0x5d, 0x57, 0x52, 0xd9, 0x11, 0x13, 0x9f, 0xdd, 0x21, 0x98, 0x5d,
0x27, 0x00, 0x0f, 0x4f, 0x57, 0xe3, 0x04, 0xae, 0x13, 0xa0, 0xbe, 0x64, 0x88, 0x75, 0x02, 0xc5,
0xfa, 0x4d, 0x67, 0xf6, 0xbd, 0x0e, 0xe2, 0xca, 0x56, 0x0e, 0x69, 0x3a, 0x14, 0x0c, 0x35, 0x1d,
0xa5, 0xe0, 0x3f, 0x52, 0x77, 0x6b, 0x0d, 0x79, 0xa4, 0xd8, 0xbe, 0xda, 0x7a, 0x17, 0x66, 0x73,
0xff, 0x5a, 0x78, 0xc2, 0xe2, 0xc4, 0x54, 0x0c, 0xd1, 0x75, 0xe5, 0x44, 0xee, 0x8f, 0x71, 0xda,
0xc9, 0x1f, 0x44, 0x03, 0x55, 0x8d, 0xd2, 0x75, 0x73, 0x0b, 0x2b, 0x62, 0x4d, 0x10, 0x03, 0x95,
0x4f, 0x38, 0x89, 0x9b, 0xd7, 0x44, 0x53, 0xae, 0x1d, 0xe8, 0xcf, 0x42, 0x2b, 0x90, 0xb8, 0xf9,
0x8f, 0xbd, 0x45, 0x13, 0x89, 0x5b, 0xb7, 0x96, 0x73, 0xed, 0x0f, 0x68, 0xb2, 0xbd, 0x92, 0x2f,
0x60, 0x99, 0x3e, 0x0b, 0x36, 0x0f, 0xa2, 0x41, 0x5c, 0xfb, 0xd3, 0x4f, 0xd3, 0xce, 0x41, 0x66,
0xef, 0x40, 0x1e, 0x4f, 0xc3, 0x7f, 0x16, 0x44, 0x09, 0x89, 0x39, 0xa8, 0x05, 0x39, 0xbf, 0x25,
0x3a, 0x7e, 0x59, 0xa6, 0x22, 0xcd, 0xe7, 0x53, 0xce, 0x33, 0xb8, 0x65, 0x39, 0x1a, 0x0f, 0x5d,
0x29, 0xf5, 0x5b, 0xa2, 0x2d, 0xca, 0x4e, 0x71, 0xa3, 0xf1, 0x68, 0x29, 0xf8, 0x79, 0x9a, 0x65,
0x20, 0x72, 0x46, 0xe3, 0x61, 0x23, 0x21, 0x22, 0xc7, 0x27, 0x9c, 0x5f, 0xc0, 0x1c, 0xcb, 0xdd,
0x7f, 0xbd, 0x03, 0xfa, 0x01, 0xd4, 0x71, 0x84, 0xd4, 0x2f, 0x60, 0x42, 0xc8, 0xf9, 0x45, 0xcf,
0x31, 0xf6, 0xdb, 0x26, 0x9b, 0x50, 0x1d, 0x81, 0xa8, 0x5f, 0xf4, 0xa4, 0x60, 0xe7, 0x9b, 0xe4,
0xe3, 0x65, 0x75, 0xe1, 0x6f, 0x19, 0xa8, 0xc5, 0xa1, 0xba, 0x20, 0xf5, 0x09, 0xf8, 0x85, 0x1d,
0x9f, 0x1d, 0x7a, 0x30, 0x71, 0x3c, 0xad, 0x53, 0x49, 0x15, 0xe6, 0xe9, 0xfb, 0xff, 0xfd, 0xd5,
0x8d, 0xb5, 0x9f, 0x7d, 0x75, 0x63, 0xed, 0x7f, 0xbf, 0xba, 0xb1, 0xf6, 0xd3, 0xaf, 0x6f, 0xbc,
0xf1, 0xb3, 0xaf, 0x6f, 0xbc, 0xf1, 0x3f, 0x5f, 0xdf, 0x78, 0xe3, 0xcb, 0x37, 0xf5, 0x6f, 0xee,
0x9f, 0xfd, 0x82, 0xfc, 0xe5, 0xfc, 0x27, 0xff, 0x1f, 0x00, 0x00, 0xff, 0xff, 0x89, 0x4b, 0xe1,
0x93, 0x97, 0x7f, 0x00, 0x00,
// 5713 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, 0x78, 0x8f, 0x68, 0xf7, 0xef, 0x40, 0x42, 0x95, 0x95, 0x9f,
0x51, 0x11, 0x59, 0xe5, 0xbd, 0xa7, 0x19, 0x39, 0x7e, 0x11, 0x91, 0x1f, 0x91, 0x99, 0x91, 0x59,
0x59, 0xd5, 0xd1, 0xcd, 0xe2, 0x6c, 0xbb, 0x28, 0xb9, 0xe0, 0xd5, 0x76, 0xc5, 0xca, 0xab, 0x74,
0xc6, 0xf4, 0xbf, 0x43, 0xf9, 0xe7, 0xc1, 0x9b, 0x71, 0xbe, 0x12, 0xab, 0x82, 0xbd, 0xf7, 0xae,
0x25, 0x67, 0x7c, 0xb1, 0x88, 0xf3, 0xa4, 0x6a, 0x90, 0xf7, 0xde, 0xb1, 0x12, 0x76, 0xc5, 0x72,
0xa1, 0xfe, 0xfe, 0xe4, 0x67, 0xff, 0xb7, 0x16, 0xbd, 0xb5, 0x93, 0xa5, 0x2c, 0x17, 0x3b, 0x4a,
0x63, 0xf0, 0x65, 0xf4, 0xed, 0x51, 0x51, 0xec, 0x33, 0xf1, 0x82, 0x95, 0x55, 0xca, 0xf3, 0xc1,
0x9d, 0xa1, 0x72, 0x30, 0x3c, 0x29, 0x66, 0xc3, 0x51, 0x51, 0x0c, 0xad, 0x70, 0x78, 0xc2, 0x7e,
0xb4, 0x64, 0x95, 0x78, 0xef, 0x6e, 0x18, 0xaa, 0x0a, 0x9e, 0x57, 0x6c, 0x70, 0x1e, 0xfd, 0xda,
0xa8, 0x28, 0x26, 0x4c, 0xec, 0xb2, 0xba, 0x02, 0x13, 0x11, 0x0b, 0x36, 0xd8, 0x68, 0xa9, 0xfa,
0x80, 0xf1, 0x71, 0xbf, 0x1b, 0x54, 0x7e, 0xa6, 0xd1, 0xb7, 0x6a, 0x3f, 0x17, 0x4b, 0x91, 0xf0,
0x57, 0xf9, 0xe0, 0xfd, 0xb6, 0xa2, 0x12, 0x19, 0xdb, 0xb7, 0x43, 0x88, 0xb2, 0xfa, 0x32, 0xfa,
0xe5, 0x97, 0x71, 0x96, 0x31, 0xb1, 0x53, 0xb2, 0xba, 0xe0, 0xbe, 0x4e, 0x23, 0x1a, 0x36, 0x32,
0x63, 0xf7, 0x4e, 0x90, 0x51, 0x86, 0xbf, 0x8c, 0xbe, 0xdd, 0x48, 0x4e, 0xd8, 0x8c, 0x5f, 0xb1,
0x72, 0x80, 0x6a, 0x29, 0x21, 0xd1, 0xe4, 0x2d, 0x08, 0xda, 0xde, 0xe1, 0xf9, 0x15, 0x2b, 0x05,
0x6e, 0x5b, 0x09, 0xc3, 0xb6, 0x2d, 0xa4, 0x6c, 0xff, 0xed, 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, 0xb5, 0x41, 0x87, 0x86, 0x1d, 0xba, 0xb0, 0xf1, 0xfd, 0xe1,
0xf5, 0x94, 0x54, 0x59, 0xfe, 0x71, 0x2d, 0xba, 0x01, 0xcb, 0x32, 0xe1, 0xd9, 0x15, 0xb3, 0xa5,
0xf9, 0xa8, 0xc3, 0xb0, 0x8f, 0x9b, 0xf2, 0x7c, 0x7c, 0x5d, 0x35, 0x55, 0xa2, 0x2c, 0x7a, 0xdb,
0x0d, 0x97, 0x09, 0xab, 0xe4, 0x70, 0x7a, 0x40, 0x47, 0x84, 0x42, 0x8c, 0xe7, 0x87, 0x7d, 0x50,
0xe5, 0x2d, 0x8d, 0x06, 0xca, 0x5b, 0xc6, 0x2b, 0xe3, 0xec, 0x3e, 0x6a, 0xc1, 0x21, 0x8c, 0xaf,
0x07, 0x3d, 0x48, 0xe5, 0xea, 0x8f, 0xa2, 0x5f, 0x79, 0xc9, 0xcb, 0xcb, 0xaa, 0x88, 0x67, 0x4c,
0x0d, 0x85, 0x7b, 0xbe, 0xb6, 0x96, 0xc2, 0xd1, 0xb0, 0xde, 0x85, 0x39, 0x41, 0xab, 0x85, 0x5f,
0x14, 0x0c, 0xce, 0x41, 0x56, 0xb1, 0x16, 0x52, 0x41, 0x0b, 0x21, 0x65, 0xfb, 0x32, 0x1a, 0x58,
0xdb, 0x67, 0x7f, 0xcc, 0x66, 0x62, 0x94, 0x24, 0xb0, 0x57, 0xac, 0xae, 0x24, 0x86, 0xa3, 0x24,
0xa1, 0x7a, 0x05, 0x47, 0x95, 0xb3, 0x57, 0xd1, 0x3b, 0xc0, 0xd9, 0x41, 0x5a, 0x49, 0x87, 0x5b,
0x61, 0x2b, 0x0a, 0x33, 0x4e, 0x87, 0x7d, 0x71, 0xe5, 0xf8, 0xcf, 0xd7, 0xa2, 0xef, 0x20, 0x9e,
0x4f, 0xd8, 0x82, 0x5f, 0xb1, 0xc1, 0xe3, 0x6e, 0x6b, 0x0d, 0x69, 0xfc, 0x7f, 0x70, 0x0d, 0x0d,
0x24, 0x4c, 0x26, 0x2c, 0x63, 0x33, 0x41, 0x86, 0x49, 0x23, 0xee, 0x0c, 0x13, 0x83, 0x39, 0x23,
0x4c, 0x0b, 0xf7, 0x99, 0xd8, 0x59, 0x96, 0x25, 0xcb, 0x05, 0xd9, 0x97, 0x16, 0xe9, 0xec, 0x4b,
0x0f, 0x45, 0xea, 0xb3, 0xcf, 0xc4, 0x28, 0xcb, 0xc8, 0xfa, 0x34, 0xe2, 0xce, 0xfa, 0x18, 0x4c,
0x79, 0x98, 0x45, 0xbf, 0xea, 0xb4, 0x98, 0x18, 0xe7, 0xe7, 0x7c, 0x40, 0xb7, 0x85, 0x94, 0x1b,
0x1f, 0x1b, 0x9d, 0x1c, 0x52, 0x8d, 0xe7, 0xaf, 0x0b, 0x5e, 0xd2, 0xdd, 0xd2, 0x88, 0x3b, 0xab,
0x61, 0x30, 0xe5, 0xe1, 0x0f, 0xa3, 0xb7, 0xd4, 0x2c, 0xa9, 0xd7, 0xb3, 0xbb, 0xe8, 0x14, 0x0a,
0x17, 0xb4, 0x7b, 0x1d, 0x54, 0xcb, 0xfc, 0x61, 0x3a, 0x2f, 0xeb, 0xd9, 0x07, 0x37, 0xaf, 0xa4,
0x1d, 0xe6, 0x2d, 0xa5, 0xcc, 0xf3, 0xe8, 0xd7, 0x7d, 0xf3, 0x3b, 0x71, 0x3e, 0x63, 0xd9, 0xe0,
0x61, 0x48, 0xbd, 0x61, 0x8c, 0xab, 0xcd, 0x5e, 0xac, 0x9d, 0xec, 0x14, 0xa1, 0x26, 0xd3, 0x3b,
0xa8, 0x36, 0x98, 0x4a, 0xef, 0x86, 0xa1, 0x96, 0xed, 0x5d, 0x96, 0x31, 0xd2, 0x76, 0x23, 0xec,
0xb0, 0x6d, 0x20, 0x65, 0xbb, 0x8c, 0x7e, 0xc3, 0x74, 0x73, 0x9d, 0x17, 0x48, 0x79, 0xbd, 0xe8,
0x6c, 0x12, 0xfd, 0xe8, 0x42, 0xc6, 0xd7, 0xa3, 0x7e, 0x70, 0xab, 0x3e, 0x6a, 0x46, 0xc1, 0xeb,
0x03, 0xe6, 0x93, 0xbb, 0x61, 0x48, 0xd9, 0xfe, 0xbb, 0xb5, 0xe8, 0x7b, 0x4a, 0xf6, 0x3c, 0x8f,
0xcf, 0x32, 0x26, 0x97, 0xf8, 0x23, 0x26, 0x5e, 0xf1, 0xf2, 0x72, 0xb2, 0xca, 0x67, 0x44, 0x3a,
0x83, 0xc3, 0x1d, 0xe9, 0x0c, 0xa9, 0xa4, 0x0a, 0xf3, 0x27, 0xd1, 0xbb, 0x3a, 0x28, 0x2e, 0xe2,
0x7c, 0xce, 0x7e, 0x58, 0xf1, 0x7c, 0x54, 0xa4, 0xa3, 0x24, 0x29, 0x07, 0x43, 0xbc, 0xeb, 0x21,
0x67, 0x4a, 0xb0, 0xdd, 0x9b, 0x77, 0xd2, 0x67, 0xd5, 0xca, 0x82, 0x17, 0x30, 0x7d, 0xd6, 0xcd,
0x27, 0x78, 0x41, 0xa5, 0xcf, 0x3e, 0xd2, 0xb2, 0x7a, 0x58, 0xaf, 0x41, 0xb8, 0xd5, 0x43, 0x77,
0xd1, 0xb9, 0x1d, 0x42, 0xec, 0x1a, 0xa0, 0x1b, 0x8a, 0xe7, 0xe7, 0xe9, 0xfc, 0xb4, 0x48, 0xea,
0x31, 0xf4, 0x00, 0xaf, 0xb3, 0x83, 0x10, 0x6b, 0x00, 0x81, 0x2a, 0x6f, 0xff, 0x60, 0xb3, 0x4c,
0x35, 0x2f, 0xed, 0x95, 0x7c, 0x71, 0xc0, 0xe6, 0xf1, 0x6c, 0xa5, 0x26, 0xd3, 0x0f, 0x43, 0xb3,
0x18, 0xa4, 0x4d, 0x21, 0x3e, 0xba, 0xa6, 0x96, 0x2a, 0xcf, 0x7f, 0xae, 0x45, 0x77, 0xbd, 0x38,
0x51, 0xc1, 0xd4, 0x94, 0x7e, 0x94, 0x27, 0x27, 0xac, 0x12, 0x71, 0x29, 0x06, 0xdf, 0x0f, 0xc4,
0x00, 0xa1, 0x63, 0xca, 0xf6, 0x83, 0x6f, 0xa4, 0x6b, 0x7b, 0x7d, 0x52, 0xaf, 0x12, 0x6a, 0xfe,
0xf1, 0x7b, 0x5d, 0x4a, 0xe0, 0xec, 0x73, 0x3b, 0x84, 0xd8, 0x5e, 0x97, 0x82, 0x71, 0x7e, 0x95,
0x0a, 0xb6, 0xcf, 0x72, 0x56, 0xb6, 0x7b, 0xbd, 0x51, 0xf5, 0x11, 0xa2, 0xd7, 0x09, 0xd4, 0xce,
0x74, 0x9e, 0x37, 0x93, 0x69, 0x6c, 0x06, 0x8c, 0xb4, 0x72, 0x8d, 0x47, 0xfd, 0x60, 0xa2, 0x86,
0x62, 0xbf, 0x36, 0x12, 0xac, 0x61, 0x83, 0xf4, 0xaa, 0xa1, 0x41, 0xed, 0xc6, 0xdc, 0xf1, 0x76,
0xc2, 0xae, 0xf8, 0x25, 0xdc, 0x98, 0xbb, 0x06, 0x1a, 0x80, 0xd8, 0x98, 0xa3, 0xa0, 0x4d, 0x3e,
0x1c, 0x3f, 0x2f, 0x52, 0xf6, 0x0a, 0x24, 0x1f, 0xae, 0x72, 0x2d, 0x26, 0x92, 0x0f, 0x04, 0x53,
0x1e, 0x8e, 0xa2, 0x5f, 0x92, 0xc2, 0x1f, 0xf2, 0x34, 0x1f, 0xdc, 0x44, 0x94, 0x6a, 0x81, 0xb1,
0x7a, 0x8b, 0x06, 0x40, 0x89, 0xeb, 0xbf, 0xaa, 0x4c, 0xe0, 0x1e, 0xa1, 0x04, 0x92, 0x80, 0xf5,
0x2e, 0xcc, 0x66, 0x7d, 0x52, 0x58, 0xcf, 0x96, 0x93, 0x8b, 0xb8, 0x4c, 0xf3, 0xf9, 0x00, 0xd3,
0x75, 0xe4, 0x44, 0xd6, 0x87, 0x71, 0x20, 0x9c, 0x94, 0xe2, 0xa8, 0x28, 0xca, 0x7a, 0x12, 0xc6,
0xc2, 0xc9, 0x47, 0x82, 0xe1, 0xd4, 0x42, 0x71, 0x6f, 0xbb, 0x6c, 0x96, 0xa5, 0x79, 0xd0, 0x9b,
0x42, 0xfa, 0x78, 0xb3, 0x28, 0x08, 0xde, 0x03, 0x16, 0x5f, 0x31, 0x5d, 0x33, 0xac, 0x65, 0x5c,
0x20, 0x18, 0xbc, 0x00, 0xb4, 0x5b, 0x6c, 0x29, 0x3e, 0x8c, 0x2f, 0x59, 0xdd, 0xc0, 0xac, 0x5e,
0xc2, 0x07, 0x98, 0xbe, 0x47, 0x10, 0x5b, 0x6c, 0x9c, 0x54, 0xae, 0x96, 0xd1, 0x3b, 0x52, 0x7e,
0x1c, 0x97, 0x22, 0x9d, 0xa5, 0x45, 0x9c, 0xeb, 0xad, 0x1b, 0x36, 0x8b, 0xb4, 0x28, 0xe3, 0x72,
0xab, 0x27, 0xad, 0xdc, 0xfe, 0xdb, 0x5a, 0xf4, 0x3e, 0xf4, 0x7b, 0xcc, 0xca, 0x45, 0x2a, 0x4f,
0x00, 0xaa, 0x66, 0xca, 0x1f, 0x7c, 0x12, 0x36, 0xda, 0x52, 0x30, 0xa5, 0xf9, 0xf4, 0xfa, 0x8a,
0x36, 0xef, 0x9b, 0xa8, 0x5d, 0xd1, 0x17, 0x65, 0xd2, 0x3a, 0x21, 0x9b, 0xe8, 0xad, 0x8e, 0x14,
0x12, 0x79, 0x5f, 0x0b, 0x02, 0x23, 0xfc, 0x34, 0xaf, 0xb4, 0x75, 0x6c, 0x84, 0x5b, 0x71, 0x70,
0x84, 0x7b, 0x98, 0x1d, 0xe1, 0xc7, 0xcb, 0xb3, 0x2c, 0xad, 0x2e, 0xd2, 0x7c, 0xae, 0x92, 0x7c,
0x5f, 0xd7, 0x8a, 0x61, 0x9e, 0xbf, 0xd1, 0xc9, 0x61, 0x4e, 0x54, 0xb0, 0x90, 0x4e, 0x40, 0x98,
0x6c, 0x74, 0x72, 0x76, 0xef, 0x65, 0xa5, 0xf5, 0xa6, 0x1f, 0xec, 0xbd, 0x1c, 0xd5, 0x5a, 0x4a,
0xec, 0xbd, 0xda, 0x94, 0xdd, 0x7b, 0xb9, 0x75, 0xa8, 0x78, 0x76, 0xc5, 0x4e, 0xcb, 0x14, 0xec,
0xbd, 0xbc, 0xf2, 0x69, 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, 0xe5, 0xed, 0xf7,
0xa3, 0xa8, 0x39, 0x2f, 0x91, 0x67, 0x5a, 0xfe, 0xda, 0xa3, 0x0e, 0x52, 0xbc, 0x03, 0xad, 0xf7,
0x03, 0x84, 0x4d, 0xaf, 0x9a, 0xbf, 0xcb, 0xa3, 0xba, 0x01, 0xaa, 0x21, 0x45, 0x44, 0x7a, 0x05,
0x10, 0x58, 0xd0, 0xc9, 0x05, 0x7f, 0x85, 0x17, 0xb4, 0x96, 0x84, 0x0b, 0xaa, 0x08, 0x7b, 0x78,
0xae, 0x0a, 0x8a, 0x1d, 0x9e, 0xeb, 0x62, 0x84, 0x0e, 0xcf, 0x21, 0x63, 0x63, 0xc6, 0x35, 0xfc,
0x8c, 0xf3, 0xcb, 0x45, 0x5c, 0x5e, 0x82, 0x98, 0xf1, 0x94, 0x35, 0x43, 0xc4, 0x0c, 0xc5, 0xda,
0x98, 0x71, 0x1d, 0xd6, 0xc9, 0xf9, 0x69, 0x99, 0x81, 0x98, 0xf1, 0x6c, 0x28, 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, 0x90, 0x46, 0x60,
0x7f, 0x4f, 0x58, 0x5c, 0xce, 0x2e, 0xf0, 0xfe, 0x6e, 0x64, 0xe1, 0xfe, 0x36, 0x0c, 0xec, 0xef,
0x46, 0xf0, 0x32, 0x15, 0x17, 0x87, 0x4c, 0xc4, 0x78, 0x7f, 0xfb, 0x4c, 0xb8, 0xbf, 0x5b, 0xac,
0xcd, 0xfe, 0x5d, 0x87, 0x93, 0xe5, 0x59, 0x35, 0x2b, 0xd3, 0x33, 0x36, 0x08, 0x58, 0x31, 0x10,
0x91, 0xfd, 0x93, 0xb0, 0xf2, 0xf9, 0x93, 0xb5, 0xe8, 0xa6, 0xee, 0x76, 0x5e, 0x55, 0x6a, 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, 0xf5, 0x8f, 0x96, 0x8d, 0x93, 0x0a, 0xa4, 0x65, 0xba, 0xbd, 0x1d, 0x82, 0x48, 0xcb,
0x70, 0x12, 0x86, 0xc2, 0x7e, 0xc9, 0x97, 0x45, 0xd5, 0x11, 0x0a, 0x00, 0x0a, 0x87, 0x42, 0x1b,
0x56, 0x3e, 0x5f, 0x47, 0xbf, 0xe9, 0x86, 0x9f, 0xdb, 0xd8, 0x5b, 0x74, 0x4c, 0x61, 0x4d, 0x3c,
0xec, 0x8b, 0xdb, 0x8c, 0x42, 0x7b, 0x16, 0xbb, 0x4c, 0xc4, 0x69, 0x56, 0x0d, 0xd6, 0x71, 0x1b,
0x5a, 0x4e, 0x64, 0x14, 0x18, 0x07, 0xe7, 0xb7, 0xdd, 0x65, 0x91, 0xa5, 0xb3, 0xf6, 0xc3, 0x24,
0xa5, 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, 0x37, 0x8d, 0xf4, 0xf0, 0xe6,
0xa0, 0x48, 0xa7, 0x4d, 0xf8, 0xb2, 0x9c, 0x31, 0xb2, 0xd3, 0x1a, 0x71, 0x67, 0xa7, 0x19, 0x4c,
0x79, 0xf8, 0xab, 0xb5, 0xe8, 0xb7, 0x1a, 0xa9, 0xfb, 0x84, 0x67, 0x37, 0xae, 0x2e, 0xce, 0x78,
0x5c, 0x26, 0x83, 0x0f, 0x30, 0x3b, 0x28, 0x6a, 0x5c, 0x3f, 0xb9, 0x8e, 0x0a, 0x6c, 0xd6, 0x3a,
0xef, 0xb6, 0x23, 0x0e, 0x6d, 0x56, 0x0f, 0x09, 0x37, 0x2b, 0x44, 0xe1, 0x04, 0x22, 0xe5, 0xcd,
0x01, 0xe0, 0x3a, 0xa9, 0xef, 0x9f, 0x02, 0x6e, 0x74, 0x72, 0x70, 0x7e, 0xac, 0x85, 0x7e, 0xb4,
0x6c, 0x51, 0x36, 0xf0, 0x88, 0x19, 0xf6, 0xc5, 0x49, 0xcf, 0x66, 0x54, 0x84, 0x3d, 0xb7, 0x46,
0xc6, 0xb0, 0x2f, 0x4e, 0x78, 0x76, 0xa6, 0xb5, 0x90, 0x67, 0x64, 0x6a, 0x1b, 0xf6, 0xc5, 0x61,
0xf6, 0xa5, 0x18, 0xbd, 0x2e, 0x3c, 0x0c, 0xd8, 0x81, 0x6b, 0xc3, 0x66, 0x2f, 0x56, 0x39, 0xfc,
0x9b, 0xb5, 0xe8, 0xbb, 0xd6, 0xe3, 0x21, 0x4f, 0xd2, 0xf3, 0x55, 0x03, 0xbd, 0x88, 0xb3, 0x25,
0xab, 0x06, 0x4f, 0x28, 0x6b, 0x6d, 0xd6, 0x94, 0xe0, 0xe9, 0xb5, 0x74, 0xe0, 0xd8, 0x19, 0x15,
0x45, 0xb6, 0x9a, 0xb2, 0x45, 0x91, 0x91, 0x63, 0xc7, 0x43, 0xc2, 0x63, 0x07, 0xa2, 0x30, 0x2b,
0x9f, 0xf2, 0x3a, 0xe7, 0x47, 0xb3, 0x72, 0x29, 0x0a, 0x67, 0xe5, 0x1a, 0x81, 0xb9, 0xd2, 0x94,
0xef, 0xf0, 0x2c, 0x63, 0x33, 0xd1, 0xbe, 0x25, 0x62, 0x34, 0x2d, 0x11, 0xce, 0x95, 0x00, 0x69,
0x4f, 0xe5, 0xf4, 0x1e, 0x32, 0x2e, 0xd9, 0xb3, 0xd5, 0x41, 0x9a, 0x5f, 0x0e, 0xf0, 0xb4, 0xc0,
0x02, 0xc4, 0xa9, 0x1c, 0x0a, 0xc2, 0xbd, 0xea, 0x69, 0x9e, 0x70, 0x7c, 0xaf, 0x5a, 0x4b, 0xc2,
0x7b, 0x55, 0x45, 0x40, 0x93, 0x27, 0x8c, 0x32, 0x59, 0x4b, 0xc2, 0x26, 0x15, 0x81, 0x4d, 0x85,
0xea, 0x49, 0x11, 0x39, 0x15, 0x82, 0x67, 0x43, 0x1b, 0x9d, 0x1c, 0xdc, 0x73, 0x29, 0x07, 0x68,
0x44, 0x00, 0xe3, 0x77, 0x82, 0x0c, 0x0c, 0x7d, 0xbd, 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, 0x8d, 0x2c, 0xdc, 0x56, 0x86,
0x81, 0xa5, 0x6f, 0x04, 0xf2, 0x90, 0x6c, 0x9d, 0x56, 0xf4, 0x8e, 0xc9, 0x36, 0x3a, 0x39, 0xe5,
0xe4, 0x5f, 0xcc, 0xfe, 0xb0, 0x91, 0x1e, 0xf1, 0x7a, 0xf0, 0xbd, 0x88, 0xb3, 0x34, 0x89, 0x05,
0x9b, 0xf2, 0x4b, 0x96, 0xe3, 0x5b, 0x31, 0x55, 0xda, 0x86, 0x1f, 0x7a, 0x0a, 0xe1, 0xad, 0x58,
0x58, 0x11, 0xc6, 0x49, 0x43, 0x9f, 0x56, 0x6c, 0x27, 0xae, 0x88, 0x29, 0xd2, 0x43, 0xc2, 0x71,
0x02, 0x51, 0x98, 0x08, 0x37, 0xf2, 0xe7, 0xaf, 0x0b, 0x56, 0xa6, 0x2c, 0x9f, 0x31, 0x3c, 0x11,
0x86, 0x54, 0x38, 0x11, 0x46, 0x68, 0xb8, 0x09, 0xdc, 0x8d, 0x05, 0x7b, 0xb6, 0x9a, 0xa6, 0x0b,
0x56, 0x89, 0x78, 0x51, 0xe0, 0x9b, 0x40, 0x00, 0x85, 0x37, 0x81, 0x6d, 0xb8, 0x75, 0xe6, 0x64,
0x66, 0xda, 0xf6, 0xad, 0x35, 0x48, 0x04, 0x6e, 0xad, 0x11, 0x28, 0x6c, 0x58, 0x0b, 0xa0, 0x4f,
0x1f, 0x5a, 0x56, 0x82, 0x4f, 0x1f, 0x68, 0xba, 0x75, 0x92, 0x67, 0x98, 0x49, 0x3d, 0x34, 0x3b,
0x8a, 0x3e, 0x71, 0x87, 0xe8, 0x66, 0x2f, 0x16, 0x3f, 0x3a, 0x3c, 0x61, 0x59, 0x2c, 0xd7, 0xc3,
0xc0, 0xf9, 0x9c, 0x66, 0xfa, 0x1c, 0x1d, 0x3a, 0xac, 0x72, 0xf8, 0x17, 0x6b, 0xd1, 0x7b, 0x98,
0xc7, 0x2f, 0x0a, 0xe9, 0xf7, 0x71, 0xb7, 0xad, 0x86, 0x24, 0xae, 0xe5, 0x85, 0x35, 0xec, 0xcd,
0x12, 0x2d, 0xb2, 0xb7, 0xf6, 0x54, 0x01, 0xfc, 0x6c, 0xd0, 0x94, 0x1f, 0x72, 0xc4, 0xcd, 0x92,
0x10, 0x6f, 0x37, 0x5a, 0x7e, 0xb9, 0x2a, 0xb0, 0xd1, 0x32, 0x36, 0x94, 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, 0x1a, 0xac, 0xc7, 0x26, 0x36, 0x97, 0x1b, 0x43, 0xee,
0xc8, 0xbc, 0xdf, 0x0d, 0xc2, 0x78, 0xd5, 0x62, 0xb5, 0xa7, 0x7a, 0x18, 0xb2, 0x00, 0xf6, 0x55,
0x9b, 0xbd, 0x58, 0xe5, 0xf0, 0xcf, 0xa2, 0xef, 0xb4, 0x2a, 0xb6, 0xc7, 0x62, 0xb1, 0x2c, 0x59,
0x32, 0xd8, 0xee, 0x28, 0xb7, 0x06, 0x8d, 0xeb, 0xc7, 0xfd, 0x15, 0x5a, 0xc9, 0xbf, 0xe6, 0x9a,
0xb0, 0x32, 0x65, 0x78, 0x12, 0x32, 0xe9, 0xb3, 0xc1, 0xe4, 0x9f, 0xd6, 0x69, 0xed, 0xdf, 0xdd,
0xe8, 0x1a, 0x5d, 0xc5, 0x69, 0x26, 0x9f, 0x02, 0x7f, 0x10, 0x32, 0xea, 0xa1, 0xc1, 0xfd, 0x3b,
0xa9, 0xd2, 0x9a, 0x99, 0xe5, 0x18, 0x77, 0xf6, 0x7d, 0x8f, 0xe8, 0x99, 0x00, 0xd9, 0xf6, 0x6d,
0xf5, 0xa4, 0x95, 0x5b, 0xa1, 0x97, 0xbc, 0xfa, 0xcf, 0x6e, 0x90, 0x63, 0x5e, 0x95, 0x2a, 0x12,
0xe9, 0x5b, 0x3d, 0x69, 0xe5, 0xf5, 0x4f, 0xa3, 0x77, 0xdb, 0x5e, 0xd5, 0x42, 0xb4, 0xdd, 0x69,
0x0a, 0xac, 0x45, 0x8f, 0xfb, 0x2b, 0x28, 0xf7, 0xff, 0x6e, 0x0e, 0xbc, 0x1b, 0xff, 0x33, 0xbe,
0x58, 0xb0, 0x3c, 0x61, 0x89, 0xd6, 0xa8, 0xea, 0x8d, 0xd9, 0xa7, 0xb4, 0x5d, 0xa3, 0x30, 0x74,
0x35, 0x4c, 0x89, 0x7e, 0xfb, 0x1b, 0x68, 0xaa, 0xa2, 0xfd, 0xf7, 0x5a, 0xf4, 0x00, 0x2d, 0x9a,
0x0e, 0x5c, 0xaf, 0x88, 0xbf, 0xd7, 0xc7, 0x11, 0xa6, 0x69, 0x8a, 0x3a, 0xfa, 0x39, 0x2c, 0xa8,
0x22, 0xff, 0xc7, 0x5a, 0x74, 0xdb, 0x2a, 0xd6, 0xe1, 0xbd, 0xc3, 0xf3, 0xf3, 0x2c, 0x9d, 0x09,
0xf9, 0xa8, 0x57, 0xa9, 0xd0, 0xcd, 0x49, 0x69, 0x74, 0x37, 0x67, 0x40, 0xd3, 0xee, 0x8a, 0x3f,
0x4b, 0x2b, 0xc1, 0xcb, 0xd5, 0xe4, 0x82, 0xbf, 0xd2, 0xef, 0x3d, 0xf9, 0xf3, 0xb2, 0x02, 0x86,
0x0e, 0x41, 0xec, 0x8a, 0x71, 0xb2, 0xe5, 0xca, 0xbe, 0x1f, 0x55, 0x11, 0xae, 0x1c, 0xa2, 0xc3,
0x95, 0x4f, 0xda, 0x55, 0x49, 0xd7, 0xca, 0xbe, 0xcc, 0xb5, 0x81, 0x17, 0xb5, 0xfd, 0x42, 0xd7,
0xfd, 0x6e, 0xd0, 0xe6, 0xa6, 0x4a, 0xbc, 0x9b, 0x9e, 0x9f, 0x9b, 0x3a, 0xe1, 0x25, 0x75, 0x11,
0x22, 0x37, 0x25, 0x50, 0xbb, 0xbd, 0xda, 0x4b, 0x33, 0x26, 0x1f, 0x0a, 0x7d, 0x71, 0x7e, 0x9e,
0xf1, 0x38, 0x01, 0xdb, 0xab, 0x5a, 0x3c, 0x74, 0xe5, 0xc4, 0xf6, 0x0a, 0xe3, 0xec, 0x75, 0x93,
0x5a, 0x5a, 0x47, 0x77, 0x3e, 0x4b, 0x33, 0x78, 0x6d, 0x5a, 0x6a, 0x1a, 0x21, 0x71, 0xdd, 0xa4,
0x05, 0xd9, 0x14, 0xa8, 0x16, 0xd5, 0x51, 0xa9, 0xcb, 0x7f, 0xaf, 0xad, 0xe8, 0x88, 0x89, 0x14,
0x08, 0xc1, 0xec, 0xf1, 0x45, 0x2d, 0x3c, 0x2d, 0xa4, 0xf1, 0x5b, 0x6d, 0xad, 0x46, 0x42, 0x1c,
0x5f, 0xf8, 0x84, 0xdd, 0x2d, 0xd7, 0x7f, 0xdf, 0xe5, 0xaf, 0x72, 0x69, 0xf4, 0x76, 0x5b, 0x45,
0xcb, 0x88, 0xdd, 0x32, 0x64, 0x94, 0xe1, 0xcf, 0xa3, 0x5f, 0x94, 0x86, 0x4b, 0x5e, 0x0c, 0x6e,
0x20, 0x0a, 0xa5, 0x73, 0xc9, 0xf8, 0x26, 0x29, 0xb7, 0xb7, 0x53, 0x4c, 0x6c, 0x9c, 0x56, 0xf1,
0x1c, 0xbe, 0x19, 0x60, 0x7b, 0x5c, 0x4a, 0x89, 0xdb, 0x29, 0x6d, 0xca, 0x8f, 0x8a, 0x23, 0x9e,
0x28, 0xeb, 0x48, 0x0d, 0x8d, 0x30, 0x14, 0x15, 0x2e, 0x64, 0xd3, 0xd6, 0xa3, 0xf8, 0x2a, 0x9d,
0x9b, 0xd4, 0xa2, 0x99, 0xc0, 0x2a, 0x90, 0xb6, 0x5a, 0x66, 0xe8, 0x40, 0x44, 0xda, 0x4a, 0xc2,
0xca, 0xe7, 0x3f, 0xaf, 0x45, 0xb7, 0x2c, 0xb3, 0xaf, 0x0f, 0x7c, 0xc7, 0xf9, 0x39, 0xaf, 0x93,
0xdc, 0x83, 0x34, 0xbf, 0xac, 0x06, 0x1f, 0x53, 0x26, 0x71, 0xde, 0x14, 0xe5, 0x93, 0x6b, 0xeb,
0xd9, 0xfd, 0x89, 0x3e, 0x0d, 0xb5, 0x57, 0x22, 0x1a, 0x0d, 0xb0, 0x3f, 0x31, 0x87, 0xa6, 0x90,
0x23, 0xf6, 0x27, 0x21, 0xde, 0x76, 0xb1, 0x71, 0x9e, 0xf1, 0x1c, 0x76, 0xb1, 0xb5, 0x50, 0x0b,
0x89, 0x2e, 0x6e, 0x41, 0x76, 0x3e, 0xd6, 0xa2, 0xe6, 0x7c, 0x6d, 0x94, 0x65, 0x60, 0x3e, 0x36,
0xaa, 0x06, 0x20, 0xe6, 0x63, 0x14, 0x54, 0x7e, 0x4e, 0xa2, 0x6f, 0xd5, 0x4d, 0x7a, 0x5c, 0xb2,
0xab, 0x94, 0xc1, 0xdb, 0x3b, 0x8e, 0x84, 0x18, 0xff, 0x3e, 0x61, 0x47, 0xd6, 0x69, 0x5e, 0x15,
0x59, 0x5c, 0x5d, 0xa8, 0xfb, 0x1c, 0x7e, 0x9d, 0xb5, 0x10, 0xde, 0xe8, 0xb8, 0xd7, 0x41, 0xd9,
0x49, 0x5d, 0xcb, 0xcc, 0x14, 0xb3, 0x8e, 0xab, 0xb6, 0xa6, 0x99, 0x8d, 0x4e, 0xce, 0x3e, 0x34,
0xd9, 0x8f, 0xb3, 0x8c, 0x95, 0x2b, 0x2d, 0x3b, 0x8c, 0xf3, 0xf4, 0x9c, 0x55, 0x02, 0x3c, 0x34,
0x51, 0xd4, 0x10, 0x62, 0xc4, 0x43, 0x93, 0x00, 0x6e, 0xf7, 0x6d, 0xc0, 0xf3, 0x38, 0x4f, 0xd8,
0x6b, 0xb0, 0x6f, 0x83, 0x76, 0x24, 0x43, 0xec, 0xdb, 0x28, 0xd6, 0x3e, 0x3c, 0x78, 0x96, 0xf1,
0xd9, 0xa5, 0x5a, 0x02, 0xfc, 0x0e, 0x96, 0x12, 0xb8, 0x06, 0xdc, 0x0e, 0x21, 0x76, 0x11, 0x90,
0x82, 0x13, 0x56, 0x64, 0xf1, 0x0c, 0x5e, 0xe1, 0x6a, 0x74, 0x94, 0x8c, 0x58, 0x04, 0x20, 0x03,
0x8a, 0xab, 0xae, 0x86, 0x61, 0xc5, 0x05, 0x37, 0xc3, 0x6e, 0x87, 0x10, 0xbb, 0x0c, 0x4a, 0xc1,
0xa4, 0xc8, 0x52, 0x01, 0x86, 0x41, 0xa3, 0x21, 0x25, 0xc4, 0x30, 0xf0, 0x09, 0x60, 0xf2, 0x90,
0x95, 0x73, 0x86, 0x9a, 0x94, 0x92, 0xa0, 0x49, 0x4d, 0xd8, 0xfb, 0xea, 0x4d, 0xdd, 0x79, 0xb1,
0x02, 0xf7, 0xd5, 0x55, 0xb5, 0x78, 0xb1, 0x22, 0xee, 0xab, 0x7b, 0x00, 0x28, 0xe2, 0x71, 0x5c,
0x09, 0xbc, 0x88, 0x52, 0x12, 0x2c, 0xa2, 0x26, 0xec, 0x1a, 0xdd, 0x14, 0x71, 0x29, 0xc0, 0x1a,
0xad, 0x0a, 0xe0, 0x5c, 0x62, 0xb8, 0x49, 0xca, 0xed, 0x4c, 0xd2, 0xf4, 0x0a, 0x13, 0x7b, 0x29,
0xcb, 0x92, 0x0a, 0xcc, 0x24, 0xaa, 0xdd, 0xb5, 0x94, 0x98, 0x49, 0xda, 0x14, 0x08, 0x25, 0xf5,
0x04, 0x04, 0xab, 0x1d, 0x78, 0x00, 0x72, 0x3b, 0x84, 0xd8, 0xf9, 0x49, 0x17, 0x7a, 0x27, 0x2e,
0xcb, 0xb4, 0x5e, 0xfc, 0xd7, 0xf1, 0x02, 0x69, 0x39, 0x31, 0x3f, 0x61, 0x1c, 0x18, 0x5e, 0x7a,
0xe2, 0xc6, 0x0a, 0x06, 0xa7, 0xee, 0x3b, 0x41, 0xc6, 0x66, 0x9c, 0x52, 0xe2, 0x3c, 0x85, 0xc7,
0x5a, 0x13, 0x79, 0x08, 0xbf, 0xde, 0x85, 0x39, 0xaf, 0xce, 0x19, 0x17, 0x87, 0xfc, 0x8a, 0x4d,
0xf9, 0xf3, 0xd7, 0x69, 0x55, 0x6f, 0xb7, 0xd4, 0xca, 0xfd, 0x94, 0xb0, 0x84, 0xc1, 0xc4, 0xab,
0x73, 0x9d, 0x4a, 0x36, 0x81, 0x00, 0x65, 0x39, 0x62, 0xaf, 0xd0, 0x04, 0x02, 0x5a, 0x34, 0x1c,
0x91, 0x40, 0x84, 0x78, 0x7b, 0x62, 0x66, 0x9c, 0xab, 0xef, 0x25, 0x4c, 0xb9, 0xce, 0xe5, 0x28,
0x6b, 0x10, 0x24, 0x0e, 0x2d, 0x82, 0x0a, 0x76, 0x7f, 0x69, 0xfc, 0xdb, 0x21, 0x76, 0x9f, 0xb0,
0xd3, 0x1e, 0x66, 0x0f, 0x7a, 0x90, 0x88, 0x2b, 0x7b, 0x95, 0x84, 0x72, 0xd5, 0xbe, 0x49, 0xf2,
0xa0, 0x07, 0xe9, 0x9c, 0xbe, 0xb9, 0xd5, 0x7a, 0x16, 0xcf, 0x2e, 0xe7, 0x25, 0x5f, 0xe6, 0xc9,
0x0e, 0xcf, 0x78, 0x09, 0x4e, 0xdf, 0xbc, 0x52, 0x03, 0x94, 0x38, 0x7d, 0xeb, 0x50, 0xb1, 0x19,
0x9c, 0x5b, 0x8a, 0x51, 0x96, 0xce, 0xe1, 0x8e, 0xda, 0x33, 0x24, 0x01, 0x22, 0x83, 0x43, 0x41,
0x24, 0x88, 0x9a, 0x1d, 0xb7, 0x48, 0x67, 0x71, 0xd6, 0xf8, 0xdb, 0xa6, 0xcd, 0x78, 0x60, 0x67,
0x10, 0x21, 0x0a, 0x48, 0x3d, 0xa7, 0xcb, 0x32, 0x1f, 0xe7, 0x82, 0x93, 0xf5, 0xd4, 0x40, 0x67,
0x3d, 0x1d, 0x10, 0x4c, 0xab, 0x53, 0xf6, 0xba, 0x2e, 0x4d, 0xfd, 0x0f, 0x36, 0xad, 0xd6, 0x7f,
0x1f, 0x2a, 0x79, 0x68, 0x5a, 0x05, 0x1c, 0xa8, 0x8c, 0x72, 0xd2, 0x04, 0x4c, 0x40, 0xdb, 0x0f,
0x93, 0xfb, 0xdd, 0x20, 0xee, 0x67, 0x22, 0x56, 0x19, 0x0b, 0xf9, 0x91, 0x40, 0x1f, 0x3f, 0x1a,
0xb4, 0xc7, 0x2d, 0x5e, 0x7d, 0x2e, 0xd8, 0xec, 0xb2, 0x75, 0x33, 0xce, 0x2f, 0x68, 0x83, 0x10,
0xc7, 0x2d, 0x04, 0x8a, 0x77, 0xd1, 0x78, 0xc6, 0xf3, 0x50, 0x17, 0xd5, 0xf2, 0x3e, 0x5d, 0xa4,
0x38, 0xbb, 0xf9, 0x35, 0x52, 0x15, 0x99, 0x4d, 0x37, 0x6d, 0x12, 0x16, 0x5c, 0x88, 0xd8, 0xfc,
0x92, 0xb0, 0xcd, 0xc9, 0xa1, 0xcf, 0xc3, 0xf6, 0x6b, 0x03, 0x2d, 0x2b, 0x87, 0xf4, 0x6b, 0x03,
0x14, 0x4b, 0x57, 0xb2, 0x89, 0x91, 0x0e, 0x2b, 0x7e, 0x9c, 0x3c, 0xea, 0x07, 0xdb, 0x2d, 0x8f,
0xe7, 0x73, 0x27, 0x63, 0x71, 0xd9, 0x78, 0xdd, 0x0a, 0x18, 0xb2, 0x18, 0xb1, 0xe5, 0x09, 0xe0,
0x60, 0x0a, 0xf3, 0x3c, 0xef, 0xf0, 0x5c, 0xb0, 0x5c, 0x60, 0x53, 0x98, 0x6f, 0x4c, 0x81, 0xa1,
0x29, 0x8c, 0x52, 0x00, 0x71, 0x2b, 0xcf, 0x83, 0x98, 0x38, 0x8a, 0x17, 0x68, 0xc6, 0xd6, 0x9c,
0xf5, 0x34, 0xf2, 0x50, 0xdc, 0x02, 0xce, 0x79, 0x9c, 0xeb, 0x7a, 0x99, 0xc6, 0xe5, 0xdc, 0x9c,
0x6e, 0x24, 0x83, 0xc7, 0xb4, 0x1d, 0x9f, 0x24, 0x1e, 0xe7, 0x86, 0x35, 0xc0, 0xb4, 0x33, 0x5e,
0xc4, 0x73, 0x53, 0x53, 0xa4, 0x06, 0x52, 0xde, 0xaa, 0xea, 0xfd, 0x6e, 0x10, 0xf8, 0x79, 0x91,
0x26, 0x8c, 0x07, 0xfc, 0x48, 0x79, 0x1f, 0x3f, 0x10, 0x04, 0xd9, 0x5b, 0x5d, 0xef, 0x66, 0x47,
0x37, 0xca, 0x13, 0xb5, 0x8f, 0x1d, 0x12, 0xcd, 0x03, 0xb8, 0x50, 0xf6, 0x46, 0xf0, 0x60, 0x8c,
0xea, 0x03, 0xda, 0xd0, 0x18, 0x35, 0xe7, 0xaf, 0x7d, 0xc6, 0x28, 0x06, 0x2b, 0x9f, 0x3f, 0x56,
0x63, 0x74, 0x37, 0x16, 0x71, 0x9d, 0xb7, 0xbf, 0x48, 0xd9, 0x2b, 0xb5, 0x11, 0x46, 0xea, 0xab,
0xa9, 0xa1, 0x7c, 0xeb, 0x19, 0xec, 0x8a, 0xb7, 0x7b, 0xf3, 0x01, 0xdf, 0x6a, 0x87, 0xd0, 0xe9,
0x1b, 0x6c, 0x15, 0xb6, 0x7b, 0xf3, 0x01, 0xdf, 0xea, 0xe3, 0x0d, 0x9d, 0xbe, 0xc1, 0x17, 0x1c,
0xb6, 0x7b, 0xf3, 0xca, 0xf7, 0x5f, 0xea, 0x81, 0xeb, 0x3a, 0xaf, 0xf3, 0xb0, 0x99, 0x48, 0xaf,
0x18, 0x96, 0x4e, 0xfa, 0xf6, 0x0c, 0x1a, 0x4a, 0x27, 0x69, 0x15, 0xe7, 0xf3, 0x69, 0x58, 0x29,
0x8e, 0x79, 0x95, 0xca, 0xeb, 0x18, 0x4f, 0x7b, 0x18, 0xd5, 0x70, 0x68, 0xd3, 0x14, 0x52, 0xb2,
0x0f, 0x96, 0x3d, 0xd4, 0x5e, 0x84, 0x7f, 0x14, 0xb0, 0xd7, 0xbe, 0x0f, 0xbf, 0xd5, 0x93, 0xb6,
0x8f, 0x78, 0x3d, 0x46, 0x3f, 0x9c, 0x9b, 0x30, 0x74, 0x95, 0x30, 0xa6, 0xcc, 0x43, 0x5b, 0xf7,
0x29, 0xe5, 0xe3, 0xfe, 0x0a, 0x1d, 0xee, 0x47, 0x49, 0xd2, 0xcf, 0xbd, 0xfb, 0x74, 0xfb, 0x71,
0x7f, 0x05, 0xe5, 0xfe, 0xaf, 0xf5, 0xb6, 0x06, 0xfa, 0x57, 0x63, 0xf0, 0x49, 0x1f, 0x8b, 0x60,
0x1c, 0x3e, 0xbd, 0x96, 0x8e, 0x2a, 0xc8, 0xdf, 0xeb, 0xfd, 0xbb, 0x46, 0xe5, 0xdb, 0x48, 0xf2,
0x2d, 0x66, 0x35, 0x24, 0x43, 0x51, 0x65, 0x61, 0x38, 0x30, 0x3f, 0xba, 0xa6, 0x96, 0xf3, 0x2d,
0x3f, 0x0f, 0x56, 0x6f, 0xcd, 0x3a, 0xe5, 0x09, 0x59, 0x76, 0x68, 0x58, 0xa0, 0x8f, 0xaf, 0xab,
0x46, 0x0d, 0x55, 0x07, 0x96, 0x5f, 0xb3, 0x79, 0xda, 0xd3, 0xb0, 0xf7, 0x7d, 0x9b, 0x0f, 0xaf,
0xa7, 0xa4, 0xca, 0xf2, 0x5f, 0x6b, 0xd1, 0x3d, 0x8f, 0xb5, 0x8f, 0x33, 0xc0, 0xa1, 0xcb, 0x0f,
0x02, 0xf6, 0x29, 0x25, 0x53, 0xb8, 0xdf, 0xf9, 0x66, 0xca, 0xf6, 0xc3, 0x77, 0x9e, 0xca, 0x5e,
0x9a, 0x09, 0x56, 0xb6, 0x3f, 0x7c, 0xe7, 0xdb, 0x6d, 0xa8, 0x21, 0xfd, 0xe1, 0xbb, 0x00, 0xee,
0x7c, 0xf8, 0x0e, 0xf1, 0x8c, 0x7e, 0xf8, 0x0e, 0xb5, 0x16, 0xfc, 0xf0, 0x5d, 0x58, 0x83, 0x5a,
0x5d, 0x74, 0x11, 0x9a, 0x63, 0xf3, 0x5e, 0x16, 0xfd, 0x53, 0xf4, 0x27, 0xd7, 0x51, 0x21, 0xd6,
0xd7, 0x86, 0x93, 0x17, 0x2a, 0x7b, 0xb4, 0xa9, 0x77, 0xa9, 0x72, 0xbb, 0x37, 0xaf, 0x7c, 0xff,
0x48, 0x6d, 0xae, 0xcc, 0x6a, 0xc2, 0x4b, 0xf9, 0xd1, 0xc3, 0xcd, 0xd0, 0xea, 0x50, 0x5b, 0x70,
0x7b, 0xfe, 0x51, 0x3f, 0x98, 0xa8, 0x6e, 0x4d, 0xa8, 0x4e, 0x1f, 0x76, 0x19, 0x02, 0x5d, 0xbe,
0xdd, 0x9b, 0x27, 0x96, 0x91, 0xc6, 0x77, 0xd3, 0xdb, 0x3d, 0x8c, 0xf9, 0x7d, 0xfd, 0xb8, 0xbf,
0x82, 0x72, 0x7f, 0xa5, 0xb2, 0x56, 0xd7, 0xbd, 0xec, 0xe7, 0xad, 0x2e, 0x53, 0x13, 0xaf, 0x9b,
0x87, 0x7d, 0xf1, 0x50, 0xfe, 0xe2, 0x2e, 0xa1, 0x5d, 0xf9, 0x0b, 0xba, 0x8c, 0x7e, 0x78, 0x3d,
0x25, 0x55, 0x96, 0x7f, 0x5a, 0x8b, 0x6e, 0x92, 0x65, 0x51, 0x71, 0xf0, 0x71, 0x5f, 0xcb, 0x20,
0x1e, 0x3e, 0xb9, 0xb6, 0x9e, 0x2a, 0xd4, 0xbf, 0xae, 0x45, 0xb7, 0x02, 0x85, 0x6a, 0x02, 0xe4,
0x1a, 0xd6, 0xfd, 0x40, 0xf9, 0xf4, 0xfa, 0x8a, 0xd4, 0x72, 0xef, 0xe2, 0x93, 0xf6, 0x47, 0xcc,
0x02, 0xb6, 0x27, 0xf4, 0x47, 0xcc, 0xba, 0xb5, 0xe0, 0x19, 0x53, 0x7c, 0xa6, 0xf7, 0x7c, 0xe8,
0x19, 0x93, 0xbc, 0x0a, 0x19, 0xfc, 0x3c, 0x0a, 0xc6, 0x61, 0x4e, 0x9e, 0xbf, 0x2e, 0xe2, 0x3c,
0xa1, 0x9d, 0x34, 0xf2, 0x6e, 0x27, 0x86, 0x83, 0x67, 0x73, 0xb5, 0xf4, 0x84, 0xeb, 0x7d, 0xdc,
0x03, 0x4a, 0xdf, 0x20, 0xc1, 0xb3, 0xb9, 0x16, 0x4a, 0x78, 0x53, 0x59, 0x63, 0xc8, 0x1b, 0x48,
0x16, 0x1f, 0xf6, 0x41, 0xc1, 0x0e, 0xc1, 0x78, 0x33, 0x47, 0xfe, 0x8f, 0x42, 0x56, 0x5a, 0xc7,
0xfe, 0x5b, 0x3d, 0x69, 0xc2, 0xed, 0x84, 0x89, 0xcf, 0x58, 0x9c, 0xb0, 0x32, 0xe8, 0xd6, 0x50,
0xbd, 0xdc, 0xba, 0x34, 0xe6, 0x76, 0x87, 0x67, 0xcb, 0x45, 0xae, 0x3a, 0x93, 0x74, 0xeb, 0x52,
0xdd, 0x6e, 0x01, 0x0d, 0x4f, 0x25, 0xad, 0x5b, 0x99, 0x5e, 0x3e, 0x0c, 0x9b, 0xf1, 0xb2, 0xca,
0xcd, 0x5e, 0x2c, 0x5d, 0x4f, 0x15, 0x46, 0x1d, 0xf5, 0x04, 0x91, 0xb4, 0xd5, 0x93, 0x86, 0xc7,
0x83, 0x8e, 0x5b, 0x13, 0x4f, 0xdb, 0x1d, 0xb6, 0x5a, 0x21, 0xf5, 0xb8, 0xbf, 0x02, 0x3c, 0x8c,
0x55, 0x51, 0x75, 0x90, 0x56, 0x62, 0x2f, 0xcd, 0xb2, 0xc1, 0x66, 0x20, 0x4c, 0x34, 0x14, 0x3c,
0x8c, 0x45, 0x60, 0x22, 0x92, 0xf5, 0xe1, 0x65, 0x3e, 0xe8, 0xb2, 0x23, 0xa9, 0x5e, 0x91, 0xec,
0xd2, 0xe0, 0x40, 0xcd, 0x69, 0x6a, 0x53, 0xdb, 0x61, 0xb8, 0xe1, 0x5a, 0x15, 0xde, 0xee, 0xcd,
0x83, 0xa7, 0xfd, 0x92, 0x92, 0x2b, 0xcb, 0x5d, 0xca, 0x84, 0xb7, 0x92, 0xdc, 0xeb, 0xa0, 0xc0,
0xa1, 0x64, 0x33, 0x8c, 0x5e, 0xa6, 0xc9, 0x9c, 0x09, 0xf4, 0x41, 0x95, 0x0b, 0x04, 0x1f, 0x54,
0x01, 0x10, 0x74, 0x5d, 0xf3, 0x77, 0x73, 0x1a, 0x3b, 0x4e, 0xb0, 0xae, 0x53, 0xca, 0x0e, 0x15,
0xea, 0x3a, 0x94, 0x06, 0xb3, 0x81, 0x71, 0xab, 0x3e, 0x28, 0xf1, 0x30, 0x64, 0x06, 0x7c, 0x55,
0x62, 0xb3, 0x17, 0x0b, 0x56, 0x14, 0xeb, 0x30, 0x5d, 0xa4, 0x02, 0x5b, 0x51, 0x1c, 0x1b, 0x35,
0x12, 0x5a, 0x51, 0xda, 0x28, 0x55, 0xbd, 0x3a, 0x47, 0x18, 0x27, 0xe1, 0xea, 0x35, 0x4c, 0xbf,
0xea, 0x19, 0xb6, 0xf5, 0x5c, 0x35, 0x37, 0x21, 0x23, 0x2e, 0xd4, 0x66, 0x19, 0x89, 0x6d, 0xf9,
0xa2, 0x31, 0x04, 0x43, 0xb3, 0x0e, 0xa5, 0x00, 0x9f, 0x17, 0xd4, 0x9c, 0x7e, 0xf4, 0x5b, 0x14,
0x2c, 0x2e, 0xe3, 0x7c, 0x86, 0x6e, 0x4e, 0xa5, 0xc1, 0x16, 0x19, 0xda, 0x9c, 0x92, 0x1a, 0xe0,
0xa9, 0xbd, 0xff, 0x26, 0x2f, 0x32, 0x14, 0xcc, 0x2b, 0xb3, 0xfe, 0x8b, 0xbc, 0x0f, 0x7a, 0x90,
0xf0, 0xa9, 0xbd, 0x06, 0xcc, 0xb9, 0x7b, 0xe3, 0xf4, 0x83, 0x80, 0x29, 0x1f, 0x0d, 0x6d, 0x84,
0x69, 0x15, 0x10, 0xd4, 0xce, 0xd9, 0xe2, 0xe7, 0x6c, 0x85, 0x05, 0xb5, 0x7b, 0x48, 0xf8, 0x39,
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, 0x6b, 0xfd, 0xa8, 0x1e, 0x29, 0xae,
0x94, 0xb7, 0x9e, 0xd5, 0xdf, 0xef, 0x06, 0xed, 0xfd, 0xdb, 0xe3, 0x92, 0xcf, 0x58, 0x55, 0xa9,
0x6f, 0xad, 0xfa, 0x17, 0x9c, 0x94, 0x6c, 0x08, 0xbe, 0xb4, 0x7a, 0x37, 0x0c, 0x39, 0x1f, 0x48,
0x6c, 0x44, 0xf6, 0xbb, 0x4d, 0xeb, 0xa8, 0x66, 0xfb, 0x93, 0x4d, 0x1b, 0x9d, 0x9c, 0x1d, 0x5e,
0x4a, 0xea, 0x7e, 0xa8, 0xe9, 0x3e, 0xaa, 0x8e, 0x7d, 0xa3, 0xe9, 0x41, 0x0f, 0x52, 0xb9, 0xfa,
0x2c, 0x7a, 0xf3, 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, 0x0a, 0xb0, 0x59, 0x84, 0xb1, 0x57, 0x27, 0xea, 0xf0, 0xce,
0xa0, 0xd5, 0x91, 0x52, 0x22, 0x8b, 0x68, 0x53, 0x36, 0xb8, 0x9b, 0xea, 0xcb, 0xef, 0xe6, 0x2c,
0x17, 0x8b, 0xb8, 0x5c, 0x81, 0xe0, 0x56, 0xb5, 0x74, 0x00, 0x22, 0xb8, 0x51, 0xd0, 0x8e, 0x5a,
0xdd, 0xcc, 0xb3, 0xcb, 0x7d, 0x5e, 0xf2, 0xa5, 0x48, 0x73, 0x06, 0xbf, 0x9d, 0x62, 0x1a, 0xd4,
0x65, 0x88, 0x51, 0x4b, 0xb1, 0x36, 0xcb, 0x95, 0x44, 0x73, 0x9d, 0x51, 0x7e, 0x6c, 0xbe, 0x12,
0xbc, 0x84, 0x8f, 0x33, 0x1b, 0x2b, 0x10, 0x22, 0xb2, 0x5c, 0x12, 0x06, 0x7d, 0x7f, 0x9c, 0xe6,
0x73, 0xb4, 0xef, 0x8f, 0xdd, 0xef, 0x17, 0xdf, 0xa2, 0x01, 0x3b, 0xa0, 0x9a, 0x46, 0x6b, 0x06,
0x80, 0x7a, 0x69, 0x18, 0x6d, 0x74, 0x97, 0x20, 0x06, 0x14, 0x4e, 0x02, 0x57, 0x5f, 0x14, 0x2c,
0x67, 0x89, 0xbe, 0xb4, 0x87, 0xb9, 0xf2, 0x88, 0xa0, 0x2b, 0x48, 0xda, 0xb9, 0x48, 0xca, 0x4f,
0x96, 0xf9, 0x71, 0xc9, 0xcf, 0xd3, 0x8c, 0x95, 0x60, 0x2e, 0x6a, 0xd4, 0x1d, 0x39, 0x31, 0x17,
0x61, 0x9c, 0xbd, 0xfd, 0x21, 0xa5, 0xde, 0x2f, 0x26, 0x4c, 0xcb, 0x78, 0x06, 0x6f, 0x7f, 0x34,
0x36, 0xda, 0x18, 0x71, 0x32, 0x18, 0xc0, 0x9d, 0x44, 0xa7, 0x71, 0x9d, 0xaf, 0x64, 0x7c, 0xa8,
0x97, 0x56, 0xe5, 0x57, 0x7d, 0x2b, 0x90, 0xe8, 0x28, 0x73, 0x18, 0x49, 0x24, 0x3a, 0x61, 0x0d,
0xbb, 0x94, 0x48, 0xee, 0x48, 0xdd, 0x6a, 0x02, 0x4b, 0x49, 0x63, 0x43, 0x0b, 0x89, 0xa5, 0xa4,
0x05, 0x81, 0x09, 0x49, 0x0f, 0x83, 0x39, 0x3a, 0x21, 0x19, 0x69, 0x70, 0x42, 0x72, 0x29, 0x3b,
0x51, 0x8c, 0xf3, 0x54, 0xa4, 0x71, 0x36, 0x61, 0xe2, 0x38, 0x2e, 0xe3, 0x05, 0x13, 0xac, 0x84,
0x13, 0x85, 0x42, 0x86, 0x1e, 0x43, 0x4c, 0x14, 0x14, 0xab, 0x1c, 0xfe, 0x6e, 0xf4, 0x76, 0xbd,
0xee, 0xb3, 0x5c, 0xfd, 0xd6, 0xd3, 0x73, 0xf9, 0x23, 0x71, 0x83, 0x77, 0x8c, 0x8d, 0x89, 0x28,
0x59, 0xbc, 0xd0, 0xb6, 0xdf, 0x32, 0x7f, 0x97, 0xe0, 0xe3, 0xb5, 0x3a, 0x9e, 0x8f, 0xb8, 0x48,
0xcf, 0xeb, 0x6d, 0xb6, 0x7a, 0x81, 0x09, 0xc4, 0xb3, 0x2b, 0x1e, 0x06, 0x3e, 0x7a, 0x82, 0x71,
0x76, 0x9e, 0x76, 0xa5, 0x27, 0xac, 0xc8, 0xe0, 0x3c, 0xed, 0x69, 0x4b, 0x80, 0x98, 0xa7, 0x51,
0xd0, 0x0e, 0x4e, 0x57, 0x3c, 0x65, 0xe1, 0xca, 0x4c, 0x59, 0xbf, 0xca, 0x4c, 0xbd, 0x77, 0x42,
0xb2, 0xe8, 0xed, 0x43, 0xb6, 0x38, 0x63, 0x65, 0x75, 0x91, 0x16, 0xd4, 0x97, 0x87, 0x2d, 0xd1,
0xf9, 0xe5, 0x61, 0x02, 0xb5, 0x2b, 0x81, 0x05, 0xc6, 0xd5, 0x51, 0xbc, 0x60, 0xf2, 0x13, 0x2e,
0x60, 0x25, 0x70, 0x8c, 0x38, 0x10, 0xb1, 0x12, 0x90, 0xb0, 0xf3, 0x7a, 0x99, 0x65, 0x4e, 0xd8,
0xbc, 0x8e, 0xb0, 0xf2, 0x38, 0x5e, 0x2d, 0x58, 0x2e, 0x94, 0x49, 0x70, 0x26, 0xef, 0x98, 0xc4,
0x79, 0xe2, 0x4c, 0xbe, 0x8f, 0x9e, 0x33, 0x35, 0x79, 0x0d, 0x7f, 0xcc, 0x4b, 0xd1, 0xfc, 0x92,
0xdb, 0x69, 0x99, 0x81, 0xa9, 0xc9, 0x6f, 0x54, 0x8f, 0x24, 0xa6, 0xa6, 0xb0, 0x86, 0xf3, 0xab,
0x1d, 0x5e, 0x19, 0x5e, 0xb0, 0xd2, 0xc4, 0xc9, 0xf3, 0x45, 0x9c, 0x66, 0x2a, 0x1a, 0xbe, 0x1f,
0xb0, 0x4d, 0xe8, 0x10, 0xbf, 0xda, 0xd1, 0x57, 0xd7, 0xf9, 0x9d, 0x93, 0x70, 0x09, 0xc1, 0x23,
0x82, 0x0e, 0xfb, 0xc4, 0x23, 0x82, 0x6e, 0x2d, 0xbb, 0x73, 0xb7, 0xac, 0xe4, 0x56, 0x92, 0xd8,
0xe1, 0x09, 0x3c, 0x2f, 0x74, 0x6c, 0x02, 0x90, 0xd8, 0xb9, 0x07, 0x15, 0x6c, 0x6a, 0x60, 0xb1,
0xbd, 0x34, 0x8f, 0xb3, 0xf4, 0xc7, 0x30, 0xad, 0x77, 0xec, 0x68, 0x82, 0x48, 0x0d, 0x70, 0x12,
0x73, 0xb5, 0xcf, 0xc4, 0x34, 0xad, 0xa7, 0xfe, 0xfb, 0x81, 0x76, 0x93, 0x44, 0xb7, 0x2b, 0x87,
0x74, 0xbe, 0x32, 0x0c, 0x9b, 0x75, 0x54, 0x14, 0x93, 0x7a, 0x55, 0x3d, 0x61, 0x33, 0x96, 0x16,
0x62, 0xf0, 0x51, 0xb8, 0xad, 0x00, 0x4e, 0x5c, 0xb4, 0xe8, 0xa1, 0xe6, 0x3c, 0xbe, 0xaf, 0xe7,
0x92, 0x49, 0xf3, 0x13, 0xa7, 0xa7, 0x15, 0x2b, 0x55, 0xa2, 0xb1, 0xcf, 0x04, 0x18, 0x9d, 0x0e,
0x37, 0x74, 0xc0, 0xba, 0xa2, 0xc4, 0xe8, 0x0c, 0x6b, 0xd8, 0xc3, 0x3e, 0x87, 0x53, 0x5f, 0x8d,
0x97, 0xd7, 0x1d, 0x1f, 0x91, 0xc6, 0x1c, 0x8a, 0x38, 0xec, 0xa3, 0x69, 0x9b, 0xad, 0xb5, 0xdd,
0x8e, 0xf2, 0xd5, 0x18, 0x5e, 0x99, 0x40, 0x2c, 0x49, 0x8c, 0xc8, 0xd6, 0x02, 0xb8, 0x73, 0x18,
0x5e, 0xf2, 0x38, 0x99, 0xc5, 0x95, 0x38, 0x8e, 0x57, 0x19, 0x8f, 0x13, 0xb9, 0xae, 0xc3, 0xc3,
0x70, 0xcd, 0x0c, 0x5d, 0x88, 0x3a, 0x0c, 0xa7, 0x60, 0x37, 0x3b, 0x93, 0xbf, 0xdc, 0xaa, 0xae,
0x92, 0xc2, 0xec, 0x4c, 0x96, 0x17, 0x5e, 0x23, 0xbd, 0x1b, 0x86, 0xec, 0x2b, 0x70, 0x8d, 0x48,
0xa6, 0x21, 0xb7, 0x30, 0x1d, 0x2f, 0x01, 0x79, 0x3f, 0x40, 0xd8, 0x0f, 0xa0, 0x34, 0x7f, 0xd7,
0x3f, 0xd6, 0x25, 0xd4, 0xb7, 0xd8, 0x1f, 0x61, 0xba, 0x2e, 0xe4, 0xdd, 0x50, 0xdb, 0xea, 0x49,
0xdb, 0x34, 0x73, 0xe7, 0x22, 0x16, 0xa3, 0x24, 0x39, 0x64, 0x15, 0xf2, 0x3e, 0x7b, 0x2d, 0x1c,
0x5a, 0x29, 0x91, 0x66, 0xb6, 0x29, 0x1b, 0xe8, 0xb5, 0xec, 0x79, 0x92, 0x0a, 0x25, 0xd3, 0x17,
0xb4, 0x1f, 0xb5, 0x0d, 0xb4, 0x29, 0xa2, 0x56, 0x34, 0x6d, 0xe7, 0xf2, 0x9a, 0x99, 0xf2, 0xf9,
0x3c, 0x63, 0x0a, 0x3a, 0x61, 0x71, 0xf3, 0x29, 0xca, 0xed, 0xb6, 0x2d, 0x14, 0x24, 0xe6, 0xf2,
0xa0, 0x82, 0x4d, 0x23, 0x6b, 0xac, 0x79, 0x24, 0xa5, 0x1b, 0x76, 0xa3, 0x6d, 0xc6, 0x03, 0x88,
0x34, 0x12, 0x05, 0xed, 0x6b, 0x77, 0xb5, 0x78, 0x9f, 0xe9, 0x96, 0x80, 0xdf, 0xba, 0x92, 0xca,
0x8e, 0x98, 0x78, 0xed, 0x0e, 0xc1, 0xec, 0x3e, 0x01, 0x78, 0x78, 0xb6, 0x1a, 0x27, 0x70, 0x9f,
0x00, 0xf5, 0x25, 0x43, 0xec, 0x13, 0x28, 0xd6, 0xef, 0x3a, 0x73, 0xee, 0x75, 0x10, 0x57, 0xb6,
0x72, 0x48, 0xd7, 0xa1, 0x60, 0xa8, 0xeb, 0x28, 0x05, 0xbf, 0x49, 0xdd, 0xa3, 0x35, 0xa4, 0x49,
0xb1, 0x73, 0xb5, 0xf5, 0x2e, 0xcc, 0xe6, 0xfe, 0xb5, 0xf0, 0x84, 0xc5, 0x89, 0xa9, 0x18, 0xa2,
0xeb, 0xca, 0x89, 0xdc, 0x1f, 0xe3, 0x94, 0x93, 0x3f, 0x88, 0x06, 0x4d, 0x35, 0x4a, 0xd7, 0xcd,
0x2d, 0xac, 0x88, 0x35, 0x41, 0x4c, 0x54, 0x3e, 0xe1, 0x24, 0x6e, 0x5e, 0x17, 0x4d, 0xb9, 0x72,
0xa0, 0x5e, 0x0b, 0xad, 0x40, 0xe2, 0xe6, 0x37, 0x7b, 0x8b, 0x26, 0x12, 0xb7, 0x6e, 0x2d, 0xe7,
0xb3, 0x3f, 0xa0, 0xcb, 0xf6, 0x4a, 0xbe, 0x80, 0x65, 0xfa, 0x34, 0xd8, 0x3d, 0x88, 0x06, 0xf1,
0xd9, 0x9f, 0x7e, 0x9a, 0x76, 0x0d, 0x32, 0x67, 0x07, 0xf2, 0x7a, 0x1a, 0xfe, 0x7b, 0x23, 0x8d,
0x90, 0x58, 0x83, 0x5a, 0x90, 0xf3, 0x23, 0xa5, 0xe3, 0x97, 0x65, 0x2a, 0xd2, 0x7c, 0x3e, 0xe5,
0x3c, 0x83, 0x47, 0x96, 0xa3, 0xf1, 0xd0, 0x95, 0x52, 0x3f, 0x52, 0xda, 0xa2, 0xec, 0x12, 0x37,
0x1a, 0x8f, 0x96, 0x82, 0x9f, 0xa7, 0x59, 0x06, 0x22, 0x67, 0x34, 0x1e, 0x6a, 0x09, 0x11, 0x39,
0x3e, 0xe1, 0xfc, 0xb4, 0xe6, 0x58, 0x9e, 0xfe, 0xab, 0x13, 0xd0, 0x3b, 0x50, 0xc7, 0x11, 0x52,
0x3f, 0xad, 0x09, 0x21, 0xe7, 0xa7, 0x42, 0xc7, 0xd8, 0x8f, 0xa6, 0x6c, 0x42, 0x75, 0x04, 0xa2,
0x7e, 0x2a, 0x94, 0x82, 0x1b, 0x9f, 0xcf, 0xde, 0xff, 0x9f, 0xaf, 0x6e, 0xac, 0xfd, 0xf4, 0xab,
0x1b, 0x6b, 0xff, 0xfb, 0xd5, 0x8d, 0xb5, 0x9f, 0x7c, 0x7d, 0xe3, 0x8d, 0x9f, 0x7e, 0x7d, 0xe3,
0x8d, 0x9f, 0x7d, 0x7d, 0xe3, 0x8d, 0x2f, 0xdf, 0x54, 0xbf, 0x9f, 0x7f, 0xf6, 0x0b, 0xf2, 0x57,
0xf0, 0x9f, 0xfe, 0x7f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x64, 0x71, 0xe6, 0xb7, 0x63, 0x7f, 0x00,
0x00,
}
// Reference imports to suppress errors if they are not otherwise used.
@ -511,6 +510,7 @@ type ClientCommandsClient interface {
ObjectUndo(ctx context.Context, in *pb.RpcObjectUndoRequest, opts ...grpc.CallOption) (*pb.RpcObjectUndoResponse, error)
ObjectRedo(ctx context.Context, in *pb.RpcObjectRedoRequest, opts ...grpc.CallOption) (*pb.RpcObjectRedoResponse, error)
ObjectListExport(ctx context.Context, in *pb.RpcObjectListExportRequest, opts ...grpc.CallOption) (*pb.RpcObjectListExportResponse, error)
ObjectExport(ctx context.Context, in *pb.RpcObjectExportRequest, opts ...grpc.CallOption) (*pb.RpcObjectExportResponse, error)
ObjectBookmarkFetch(ctx context.Context, in *pb.RpcObjectBookmarkFetchRequest, opts ...grpc.CallOption) (*pb.RpcObjectBookmarkFetchResponse, error)
ObjectToBookmark(ctx context.Context, in *pb.RpcObjectToBookmarkRequest, opts ...grpc.CallOption) (*pb.RpcObjectToBookmarkResponse, error)
ObjectImport(ctx context.Context, in *pb.RpcObjectImportRequest, opts ...grpc.CallOption) (*pb.RpcObjectImportResponse, error)
@ -755,8 +755,6 @@ type ClientCommandsClient interface {
AIAutofill(ctx context.Context, in *pb.RpcAIAutofillRequest, opts ...grpc.CallOption) (*pb.RpcAIAutofillResponse, error)
AIListSummary(ctx context.Context, in *pb.RpcAIListSummaryRequest, opts ...grpc.CallOption) (*pb.RpcAIListSummaryResponse, error)
AIObjectCreateFromUrl(ctx context.Context, in *pb.RpcAIObjectCreateFromUrlRequest, opts ...grpc.CallOption) (*pb.RpcAIObjectCreateFromUrlResponse, error)
// Push
PushNotificationRegisterToken(ctx context.Context, in *pb.RpcPushNotificationRegisterTokenRequest, opts ...grpc.CallOption) (*pb.RpcPushNotificationRegisterTokenResponse, error)
}
type clientCommandsClient struct {
@ -1622,6 +1620,15 @@ func (c *clientCommandsClient) ObjectListExport(ctx context.Context, in *pb.RpcO
return out, nil
}
func (c *clientCommandsClient) ObjectExport(ctx context.Context, in *pb.RpcObjectExportRequest, opts ...grpc.CallOption) (*pb.RpcObjectExportResponse, error) {
out := new(pb.RpcObjectExportResponse)
err := c.cc.Invoke(ctx, "/anytype.ClientCommands/ObjectExport", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *clientCommandsClient) ObjectBookmarkFetch(ctx context.Context, in *pb.RpcObjectBookmarkFetchRequest, opts ...grpc.CallOption) (*pb.RpcObjectBookmarkFetchResponse, error) {
out := new(pb.RpcObjectBookmarkFetchResponse)
err := c.cc.Invoke(ctx, "/anytype.ClientCommands/ObjectBookmarkFetch", in, out, opts...)
@ -3409,15 +3416,6 @@ func (c *clientCommandsClient) AIObjectCreateFromUrl(ctx context.Context, in *pb
return out, nil
}
func (c *clientCommandsClient) PushNotificationRegisterToken(ctx context.Context, in *pb.RpcPushNotificationRegisterTokenRequest, opts ...grpc.CallOption) (*pb.RpcPushNotificationRegisterTokenResponse, error) {
out := new(pb.RpcPushNotificationRegisterTokenResponse)
err := c.cc.Invoke(ctx, "/anytype.ClientCommands/PushNotificationRegisterToken", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// ClientCommandsServer is the server API for ClientCommands service.
type ClientCommandsServer interface {
AppGetVersion(context.Context, *pb.RpcAppGetVersionRequest) *pb.RpcAppGetVersionResponse
@ -3531,6 +3529,7 @@ type ClientCommandsServer interface {
ObjectUndo(context.Context, *pb.RpcObjectUndoRequest) *pb.RpcObjectUndoResponse
ObjectRedo(context.Context, *pb.RpcObjectRedoRequest) *pb.RpcObjectRedoResponse
ObjectListExport(context.Context, *pb.RpcObjectListExportRequest) *pb.RpcObjectListExportResponse
ObjectExport(context.Context, *pb.RpcObjectExportRequest) *pb.RpcObjectExportResponse
ObjectBookmarkFetch(context.Context, *pb.RpcObjectBookmarkFetchRequest) *pb.RpcObjectBookmarkFetchResponse
ObjectToBookmark(context.Context, *pb.RpcObjectToBookmarkRequest) *pb.RpcObjectToBookmarkResponse
ObjectImport(context.Context, *pb.RpcObjectImportRequest) *pb.RpcObjectImportResponse
@ -3775,8 +3774,6 @@ type ClientCommandsServer interface {
AIAutofill(context.Context, *pb.RpcAIAutofillRequest) *pb.RpcAIAutofillResponse
AIListSummary(context.Context, *pb.RpcAIListSummaryRequest) *pb.RpcAIListSummaryResponse
AIObjectCreateFromUrl(context.Context, *pb.RpcAIObjectCreateFromUrlRequest) *pb.RpcAIObjectCreateFromUrlResponse
// Push
PushNotificationRegisterToken(context.Context, *pb.RpcPushNotificationRegisterTokenRequest) *pb.RpcPushNotificationRegisterTokenResponse
}
// UnimplementedClientCommandsServer can be embedded to have forward compatible implementations.
@ -4068,6 +4065,9 @@ func (*UnimplementedClientCommandsServer) ObjectRedo(ctx context.Context, req *p
func (*UnimplementedClientCommandsServer) ObjectListExport(ctx context.Context, req *pb.RpcObjectListExportRequest) *pb.RpcObjectListExportResponse {
return nil
}
func (*UnimplementedClientCommandsServer) ObjectExport(ctx context.Context, req *pb.RpcObjectExportRequest) *pb.RpcObjectExportResponse {
return nil
}
func (*UnimplementedClientCommandsServer) ObjectBookmarkFetch(ctx context.Context, req *pb.RpcObjectBookmarkFetchRequest) *pb.RpcObjectBookmarkFetchResponse {
return nil
}
@ -4656,9 +4656,6 @@ func (*UnimplementedClientCommandsServer) AIListSummary(ctx context.Context, req
func (*UnimplementedClientCommandsServer) AIObjectCreateFromUrl(ctx context.Context, req *pb.RpcAIObjectCreateFromUrlRequest) *pb.RpcAIObjectCreateFromUrlResponse {
return nil
}
func (*UnimplementedClientCommandsServer) PushNotificationRegisterToken(ctx context.Context, req *pb.RpcPushNotificationRegisterTokenRequest) *pb.RpcPushNotificationRegisterTokenResponse {
return nil
}
func RegisterClientCommandsServer(s *grpc.Server, srv ClientCommandsServer) {
s.RegisterService(&_ClientCommands_serviceDesc, srv)
@ -6374,6 +6371,24 @@ func _ClientCommands_ObjectListExport_Handler(srv interface{}, ctx context.Conte
return interceptor(ctx, in, info, handler)
}
func _ClientCommands_ObjectExport_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(pb.RpcObjectExportRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ClientCommandsServer).ObjectExport(ctx, in), nil
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/anytype.ClientCommands/ObjectExport",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ClientCommandsServer).ObjectExport(ctx, req.(*pb.RpcObjectExportRequest)), nil
}
return interceptor(ctx, in, info, handler)
}
func _ClientCommands_ObjectBookmarkFetch_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(pb.RpcObjectBookmarkFetchRequest)
if err := dec(in); err != nil {
@ -9906,24 +9921,6 @@ func _ClientCommands_AIObjectCreateFromUrl_Handler(srv interface{}, ctx context.
return interceptor(ctx, in, info, handler)
}
func _ClientCommands_PushNotificationRegisterToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(pb.RpcPushNotificationRegisterTokenRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ClientCommandsServer).PushNotificationRegisterToken(ctx, in), nil
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/anytype.ClientCommands/PushNotificationRegisterToken",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ClientCommandsServer).PushNotificationRegisterToken(ctx, req.(*pb.RpcPushNotificationRegisterTokenRequest)), nil
}
return interceptor(ctx, in, info, handler)
}
var _ClientCommands_serviceDesc = grpc.ServiceDesc{
ServiceName: "anytype.ClientCommands",
HandlerType: (*ClientCommandsServer)(nil),
@ -10308,6 +10305,10 @@ var _ClientCommands_serviceDesc = grpc.ServiceDesc{
MethodName: "ObjectListExport",
Handler: _ClientCommands_ObjectListExport_Handler,
},
{
MethodName: "ObjectExport",
Handler: _ClientCommands_ObjectExport_Handler,
},
{
MethodName: "ObjectBookmarkFetch",
Handler: _ClientCommands_ObjectBookmarkFetch_Handler,
@ -11088,10 +11089,6 @@ var _ClientCommands_serviceDesc = grpc.ServiceDesc{
MethodName: "AIObjectCreateFromUrl",
Handler: _ClientCommands_AIObjectCreateFromUrl_Handler,
},
{
MethodName: "PushNotificationRegisterToken",
Handler: _ClientCommands_PushNotificationRegisterToken_Handler,
},
},
Streams: []grpc.StreamDesc{
{

View file

@ -9,7 +9,7 @@ import (
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
const RelationChecksum = "e8392d6bf8efab6b707d68bc3922e4284a18a68370828e3ec6b18e0267b84bea"
const RelationChecksum = "a23583f8faef1cbfcd05ca1d2761538e67bbdd1336d1a467a0d657b5f986b313"
const (
RelationKeyTag domain.RelationKey = "tag"
RelationKeyCamera domain.RelationKey = "camera"
@ -158,6 +158,7 @@ const (
RelationKeyDefaultViewType domain.RelationKey = "defaultViewType"
RelationKeyDefaultTypeId domain.RelationKey = "defaultTypeId"
RelationKeyAutoWidgetTargets domain.RelationKey = "autoWidgetTargets"
RelationKeyAutoWidgetDisabled domain.RelationKey = "autoWidgetDisabled"
RelationKeyPluralName domain.RelationKey = "pluralName"
)
@ -282,6 +283,19 @@ var (
Revision: 1,
Scope: model.Relation_type,
},
RelationKeyAutoWidgetDisabled: {
DataSource: model.Relation_details,
Description: "",
Format: model.RelationFormat_checkbox,
Hidden: true,
Id: "_brautoWidgetDisabled",
Key: "autoWidgetDisabled",
Name: "Auto Widget disabled",
ReadOnly: false,
ReadOnlyRelation: true,
Scope: model.Relation_type,
},
RelationKeyAutoWidgetTargets: {
DataSource: model.Relation_details,

View file

@ -1517,6 +1517,15 @@
"readonly": false,
"source": "details"
},
{
"format": "checkbox",
"hidden": true,
"key": "autoWidgetDisabled",
"maxCount": 0,
"name": "Auto Widget disabled",
"readonly": false,
"source": "details"
},
{
"description": "Name of Object type in plural form",
"format": "longtext",

View file

@ -9,7 +9,7 @@ import (
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
const TypeChecksum = "91c804ca7dd4df74fafbdaff99e50fa2ffcc6cb0fb073887a90bdf1b4a3d98d5"
const TypeChecksum = "f2672b9f52915f228e335b78ed94ce1b6c7b195ceb1cba9a44b25a870a3b2ac0"
const (
TypePrefix = "_ot"
)
@ -426,7 +426,7 @@ var (
Name: "Task",
PluralName: "Tasks",
Readonly: true,
RelationLinks: []*model.RelationLink{MustGetRelationLink(RelationKeyTag), MustGetRelationLink(RelationKeyAssignee), MustGetRelationLink(RelationKeyDone), MustGetRelationLink(RelationKeyDueDate), MustGetRelationLink(RelationKeyLinkedProjects), MustGetRelationLink(RelationKeyPriority), MustGetRelationLink(RelationKeyStatus), MustGetRelationLink(RelationKeyTasks)},
RelationLinks: []*model.RelationLink{MustGetRelationLink(RelationKeyTag), MustGetRelationLink(RelationKeyAssignee), MustGetRelationLink(RelationKeyDone), MustGetRelationLink(RelationKeyDueDate), MustGetRelationLink(RelationKeyLinkedProjects), MustGetRelationLink(RelationKeyStatus)},
Revision: 2,
Types: []model.SmartBlockType{model.SmartBlockType_Page},
Url: TypePrefix + "task",

View file

@ -107,9 +107,7 @@
"done",
"dueDate",
"linkedProjects",
"priority",
"status",
"tasks"
"status"
],
"description": "A piece of work to be done or undertaken",
"revision": 2

View file

@ -4,6 +4,7 @@ import (
"os"
"path/filepath"
"sync"
"time"
"github.com/anyproto/any-sync/app"
@ -54,8 +55,36 @@ func (s *TempDirService) TempDir() string {
s.tempDir = os.TempDir()
} else {
s.tempDir = path
go s.cleanUp()
}
})
return s.tempDir
}
func (s *TempDirService) cleanUp() {
cutoff := time.Now().Add(-72 * time.Hour)
recursiveCleanup(s.tempDir, cutoff)
}
func recursiveCleanup(path string, cutoff time.Time) {
entries, err := os.ReadDir(path)
if err != nil {
log.Warnf("tmp cleanup readdir: %v", err)
}
for _, entry := range entries {
fullEntryPath := filepath.Join(path, entry.Name())
info, err := entry.Info()
if err != nil {
log.Warnf("tmp cleanup entry: %v", err)
}
if entry.IsDir() {
recursiveCleanup(fullEntryPath, cutoff)
} else if info.ModTime().IsZero() || info.ModTime().Before(cutoff) {
err = os.RemoveAll(fullEntryPath)
if err != nil {
log.Warnf("tmp cleanup delete: %v", err)
}
}
}
}

View file

@ -0,0 +1,50 @@
package core
import (
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestTempDirService(t *testing.T) {
t.Run("cleanup", func(t *testing.T) {
// given
s := NewTempDirService()
s.tempDir = t.TempDir()
oldFile := filepath.Join(s.tempDir, "old.txt")
require.NoError(t, os.WriteFile(oldFile, []byte("old"), 0600))
oldTime := time.Now().Add(-100 * time.Hour)
require.NoError(t, os.Chtimes(oldFile, oldTime, oldTime))
newFile := filepath.Join(s.tempDir, "new.txt")
require.NoError(t, os.WriteFile(newFile, []byte("new"), 0600))
nestedDir := filepath.Join(s.tempDir, "nested")
require.NoError(t, os.MkdirAll(nestedDir, 0755))
nestedOldFile := filepath.Join(nestedDir, "nested_old.txt")
require.NoError(t, os.WriteFile(nestedOldFile, []byte("nested old"), 0600))
require.NoError(t, os.Chtimes(nestedOldFile, oldTime, oldTime))
nestedNewFile := filepath.Join(nestedDir, "nested_new.txt")
require.NoError(t, os.WriteFile(nestedNewFile, []byte("nested new"), 0600))
// when
s.cleanUp()
// then
_, err := os.Stat(oldFile)
require.True(t, os.IsNotExist(err), "old file should be deleted")
_, err = os.Stat(newFile)
require.NoError(t, err, "new file should remain")
_, err = os.Stat(nestedOldFile)
require.True(t, os.IsNotExist(err), "nested old file should be deleted")
_, err = os.Stat(nestedNewFile)
require.NoError(t, err, "nested new file should remain")
})
}

View file

@ -25,6 +25,7 @@ import (
"unicode"
"github.com/anyproto/any-sync/app"
"github.com/anyproto/any-sync/app/debugstat"
tantivy "github.com/anyproto/tantivy-go"
"github.com/valyala/fastjson"
@ -102,6 +103,19 @@ type ftSearch struct {
lang tantivy.Language
}
func (f *ftSearch) ProvideStat() any {
count, _ := f.DocCount()
return count
}
func (f *ftSearch) StatId() string {
return "doc_count"
}
func (f *ftSearch) StatType() string {
return CName
}
func TantivyNew() FTSearch {
return new(ftSearch)
}
@ -138,6 +152,10 @@ func (f *ftSearch) DeleteObject(objectId string) error {
func (f *ftSearch) Init(a *app.App) error {
repoPath := app.MustComponent[wallet.Wallet](a).RepoPath()
statService, _ := app.GetComponent[debugstat.StatService](a)
if statService != nil {
statService.AddProvider(f)
}
f.lang = validateLanguage(app.MustComponent[wallet.Wallet](a).FtsPrimaryLang())
f.rootPath = filepath.Join(repoPath, ftsDir2)
f.blevePath = filepath.Join(repoPath, ftsDir)

View file

@ -11,6 +11,7 @@ import (
anystore "github.com/anyproto/any-store"
"github.com/anyproto/any-store/anyenc"
"github.com/anyproto/any-sync/app"
"github.com/anyproto/any-sync/app/debugstat"
"github.com/anyproto/any-sync/coordinator/coordinatorproto"
"golang.org/x/exp/maps"
@ -124,6 +125,19 @@ type dsObjectStore struct {
componentCtxCancel context.CancelFunc
}
func (s *dsObjectStore) ProvideStat() any {
count, _ := s.ListIdsCrossSpace()
return len(count)
}
func (s *dsObjectStore) StatId() string {
return "ds_count"
}
func (s *dsObjectStore) StatType() string {
return CName
}
func (s *dsObjectStore) IterateSpaceIndex(f func(store spaceindex.Store) error) error {
s.Lock()
spaceIndexes := make([]spaceindex.Store, 0, len(s.spaceIndexes))
@ -169,6 +183,10 @@ func (s *dsObjectStore) Init(a *app.App) (err error) {
s.setDefaultConfig()
s.oldStore = app.MustComponent[oldstore.Service](a)
s.techSpaceIdProvider = app.MustComponent[TechSpaceIdProvider](a)
statService, _ := app.GetComponent[debugstat.StatService](a)
if statService != nil {
statService.AddProvider(s)
}
return nil
}

View file

@ -42,6 +42,14 @@ const (
TIFF Format = "tiff"
)
func IsImageExt(ext string) bool {
switch strings.ToLower(strings.TrimPrefix(ext, ".")) {
case "jpg", "jpeg", "png", "gif", "ico", "webp", "heic", "heif", "bmp", "tiff", "psd":
return true
}
return false
}
func IsImage(mime string) bool {
parts := strings.SplitN(mime, "/", 2)
if len(parts) == 1 {

File diff suppressed because it is too large Load diff

View file

@ -593,6 +593,7 @@ message Block {
Layout layout = 1;
int32 limit = 2;
string viewId = 3;
bool autoAdded = 4;
enum Layout {
Link = 0;

View file

@ -405,7 +405,7 @@ func (b *builtinObjects) createWidgets(ctx session.Context, spaceId string, useC
}
if err = cache.DoStateCtx(b.objectGetter, ctx, widgetObjectID, func(s *state.State, w widget.Widget) error {
for _, targetId := range widgetTargetsToCreate {
if err := w.AddAutoWidget(s, targetId, "", addr.ObjectTypeAllViewId, model.BlockContentWidget_View); err != nil {
if err := w.AddAutoWidget(s, targetId, "", addr.ObjectTypeAllViewId, model.BlockContentWidget_View, ""); err != nil {
log.Errorf("failed to create widget block for type '%s': %v", targetId, err)
}
}