From d832a0c283b2b3211a24b8c38d704bdb17466b93 Mon Sep 17 00:00:00 2001 From: Felicio Mununga Date: Wed, 15 Mar 2023 14:14:26 +0100 Subject: [PATCH] Add request client (#342) * add `preview-client.ts` * add exports * fix imports * set emoji hash * use `message.timestamp` * rename `RequestClient` * drop preview keyword * rm `appUrl` * rm todo?: * remove social urls * remove if clause --- packages/status-js/src/index.ts | 7 + .../src/protos/protocol-message.proto | 11 +- .../src/protos/protocol-message_pb.ts | 86 ++- .../src/protos/push-notifications.proto | 103 +++ .../src/protos/push-notifications_pb.ts | 717 ++++++++++++++++++ .../src/request-client/map-channel.ts | 42 + .../src/request-client/map-community.ts | 51 ++ .../status-js/src/request-client/map-user.ts | 32 + .../src/request-client/request-client.ts | 320 ++++++++ packages/status-js/src/request-client/tags.ts | 54 ++ 10 files changed, 1419 insertions(+), 4 deletions(-) create mode 100644 packages/status-js/src/protos/push-notifications.proto create mode 100644 packages/status-js/src/protos/push-notifications_pb.ts create mode 100644 packages/status-js/src/request-client/map-channel.ts create mode 100644 packages/status-js/src/request-client/map-community.ts create mode 100644 packages/status-js/src/request-client/map-user.ts create mode 100644 packages/status-js/src/request-client/request-client.ts create mode 100644 packages/status-js/src/request-client/tags.ts diff --git a/packages/status-js/src/index.ts b/packages/status-js/src/index.ts index 0d9d15e8a..1d88f5c74 100644 --- a/packages/status-js/src/index.ts +++ b/packages/status-js/src/index.ts @@ -11,3 +11,10 @@ export { createClient } from './client/client' export type { Community } from './client/community/community' export type { Reaction, Reactions } from './client/community/get-reactions' export type { Member } from './client/member' +export type { ChannelInfo } from './request-client/map-channel' +export type { CommunityInfo } from './request-client/map-community' +export type { UserInfo } from './request-client/map-user' +export type { RequestClient } from './request-client/request-client' +export { createRequestClient } from './request-client/request-client' +export { deserializePublicKey } from './utils/deserialize-public-key' +export { publicKeyToEmojiHash } from './utils/public-key-to-emoji-hash' diff --git a/packages/status-js/src/protos/protocol-message.proto b/packages/status-js/src/protos/protocol-message.proto index 3df17360a..50edd33e8 100644 --- a/packages/status-js/src/protos/protocol-message.proto +++ b/packages/status-js/src/protos/protocol-message.proto @@ -58,7 +58,16 @@ message HRHeader { // Community message number for this key_id uint32 seq_no = 2; // Community ID - string group_id = 3; + bytes group_id = 3; +} + +message HRKeys { + repeated HRKey keys = 1; +} + +message HRKey { + uint32 key_id = 1; + bytes key = 2; } // Direct message value diff --git a/packages/status-js/src/protos/protocol-message_pb.ts b/packages/status-js/src/protos/protocol-message_pb.ts index e43e71d43..0bf40fbd6 100644 --- a/packages/status-js/src/protos/protocol-message_pb.ts +++ b/packages/status-js/src/protos/protocol-message_pb.ts @@ -339,9 +339,9 @@ export class HRHeader extends Message { /** * Community ID * - * @generated from field: string group_id = 3; + * @generated from field: bytes group_id = 3; */ - groupId = ""; + groupId = new Uint8Array(0); constructor(data?: PartialMessage) { super(); @@ -353,7 +353,7 @@ export class HRHeader extends Message { static readonly fields: FieldList = proto3.util.newFieldList(() => [ { no: 1, name: "key_id", kind: "scalar", T: 13 /* ScalarType.UINT32 */ }, { no: 2, name: "seq_no", kind: "scalar", T: 13 /* ScalarType.UINT32 */ }, - { no: 3, name: "group_id", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 3, name: "group_id", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): HRHeader { @@ -373,6 +373,86 @@ export class HRHeader extends Message { } } +/** + * @generated from message HRKeys + */ +export class HRKeys extends Message { + /** + * @generated from field: repeated HRKey keys = 1; + */ + keys: HRKey[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime = proto3; + static readonly typeName = "HRKeys"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "keys", kind: "message", T: HRKey, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): HRKeys { + return new HRKeys().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): HRKeys { + return new HRKeys().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): HRKeys { + return new HRKeys().fromJsonString(jsonString, options); + } + + static equals(a: HRKeys | PlainMessage | undefined, b: HRKeys | PlainMessage | undefined): boolean { + return proto3.util.equals(HRKeys, a, b); + } +} + +/** + * @generated from message HRKey + */ +export class HRKey extends Message { + /** + * @generated from field: uint32 key_id = 1; + */ + keyId = 0; + + /** + * @generated from field: bytes key = 2; + */ + key = new Uint8Array(0); + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime = proto3; + static readonly typeName = "HRKey"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "key_id", kind: "scalar", T: 13 /* ScalarType.UINT32 */ }, + { no: 2, name: "key", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): HRKey { + return new HRKey().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): HRKey { + return new HRKey().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): HRKey { + return new HRKey().fromJsonString(jsonString, options); + } + + static equals(a: HRKey | PlainMessage | undefined, b: HRKey | PlainMessage | undefined): boolean { + return proto3.util.equals(HRKey, a, b); + } +} + /** * Direct message value * diff --git a/packages/status-js/src/protos/push-notifications.proto b/packages/status-js/src/protos/push-notifications.proto new file mode 100644 index 000000000..078b0ac31 --- /dev/null +++ b/packages/status-js/src/protos/push-notifications.proto @@ -0,0 +1,103 @@ +syntax = "proto3"; + +import "chat-identity.proto"; + +message PushNotificationRegistration { + enum TokenType { + UNKNOWN_TOKEN_TYPE = 0; + APN_TOKEN = 1; + FIREBASE_TOKEN = 2; + } + TokenType token_type = 1; + string device_token = 2; + string installation_id = 3; + string access_token = 4; + bool enabled = 5; + uint64 version = 6; + repeated bytes allowed_key_list = 7; + repeated bytes blocked_chat_list = 8; + bool unregister = 9; + bytes grant = 10; + bool allow_from_contacts_only = 11; + string apn_topic = 12; + bool block_mentions = 13; + repeated bytes allowed_mentions_chat_list = 14; +} + +message PushNotificationRegistrationResponse { + bool success = 1; + ErrorType error = 2; + bytes request_id = 3; + + enum ErrorType { + UNKNOWN_ERROR_TYPE = 0; + MALFORMED_MESSAGE = 1; + VERSION_MISMATCH = 2; + UNSUPPORTED_TOKEN_TYPE = 3; + INTERNAL_ERROR = 4; + } +} + +message ContactCodeAdvertisement { + repeated PushNotificationQueryInfo push_notification_info = 1; + ChatIdentity chat_identity = 2; +} + +message PushNotificationQuery { + repeated bytes public_keys = 1; +} + +message PushNotificationQueryInfo { + string access_token = 1; + string installation_id = 2; + bytes public_key = 3; + repeated bytes allowed_key_list = 4; + bytes grant = 5; + uint64 version = 6; + bytes server_public_key = 7; +} + +message PushNotificationQueryResponse { + repeated PushNotificationQueryInfo info = 1; + bytes message_id = 2; + bool success = 3; +} + +message PushNotification { + string access_token = 1; + bytes chat_id = 2; + bytes public_key = 3; + string installation_id = 4; + bytes message = 5; + PushNotificationType type = 6; + enum PushNotificationType { + UNKNOWN_PUSH_NOTIFICATION_TYPE = 0; + MESSAGE = 1; + MENTION = 2; + REQUEST_TO_JOIN_COMMUNITY = 3; + } + bytes author = 7; +} + +message PushNotificationRequest { + repeated PushNotification requests = 1; + bytes message_id = 2; +} + +message PushNotificationReport { + bool success = 1; + ErrorType error = 2; + enum ErrorType { + UNKNOWN_ERROR_TYPE = 0; + WRONG_TOKEN = 1; + INTERNAL_ERROR = 2; + NOT_REGISTERED = 3; + } + bytes public_key = 3; + string installation_id = 4; +} + +message PushNotificationResponse { + bytes message_id = 1; + repeated PushNotificationReport reports = 2; +} diff --git a/packages/status-js/src/protos/push-notifications_pb.ts b/packages/status-js/src/protos/push-notifications_pb.ts new file mode 100644 index 000000000..47ac8f92b --- /dev/null +++ b/packages/status-js/src/protos/push-notifications_pb.ts @@ -0,0 +1,717 @@ +// @generated by protoc-gen-es v1.0.0 with parameter "target=ts" +// @generated from file push-notifications.proto (syntax proto3) +/* eslint-disable */ +// @ts-nocheck + +import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf"; +import { Message, proto3, protoInt64 } from "@bufbuild/protobuf"; +import { ChatIdentity } from "./chat-identity_pb.js"; + +/** + * @generated from message PushNotificationRegistration + */ +export class PushNotificationRegistration extends Message { + /** + * @generated from field: PushNotificationRegistration.TokenType token_type = 1; + */ + tokenType = PushNotificationRegistration_TokenType.UNKNOWN_TOKEN_TYPE; + + /** + * @generated from field: string device_token = 2; + */ + deviceToken = ""; + + /** + * @generated from field: string installation_id = 3; + */ + installationId = ""; + + /** + * @generated from field: string access_token = 4; + */ + accessToken = ""; + + /** + * @generated from field: bool enabled = 5; + */ + enabled = false; + + /** + * @generated from field: uint64 version = 6; + */ + version = protoInt64.zero; + + /** + * @generated from field: repeated bytes allowed_key_list = 7; + */ + allowedKeyList: Uint8Array[] = []; + + /** + * @generated from field: repeated bytes blocked_chat_list = 8; + */ + blockedChatList: Uint8Array[] = []; + + /** + * @generated from field: bool unregister = 9; + */ + unregister = false; + + /** + * @generated from field: bytes grant = 10; + */ + grant = new Uint8Array(0); + + /** + * @generated from field: bool allow_from_contacts_only = 11; + */ + allowFromContactsOnly = false; + + /** + * @generated from field: string apn_topic = 12; + */ + apnTopic = ""; + + /** + * @generated from field: bool block_mentions = 13; + */ + blockMentions = false; + + /** + * @generated from field: repeated bytes allowed_mentions_chat_list = 14; + */ + allowedMentionsChatList: Uint8Array[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime = proto3; + static readonly typeName = "PushNotificationRegistration"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "token_type", kind: "enum", T: proto3.getEnumType(PushNotificationRegistration_TokenType) }, + { no: 2, name: "device_token", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 3, name: "installation_id", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 4, name: "access_token", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 5, name: "enabled", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, + { no: 6, name: "version", kind: "scalar", T: 4 /* ScalarType.UINT64 */ }, + { no: 7, name: "allowed_key_list", kind: "scalar", T: 12 /* ScalarType.BYTES */, repeated: true }, + { no: 8, name: "blocked_chat_list", kind: "scalar", T: 12 /* ScalarType.BYTES */, repeated: true }, + { no: 9, name: "unregister", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, + { no: 10, name: "grant", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 11, name: "allow_from_contacts_only", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, + { no: 12, name: "apn_topic", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 13, name: "block_mentions", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, + { no: 14, name: "allowed_mentions_chat_list", kind: "scalar", T: 12 /* ScalarType.BYTES */, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): PushNotificationRegistration { + return new PushNotificationRegistration().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): PushNotificationRegistration { + return new PushNotificationRegistration().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): PushNotificationRegistration { + return new PushNotificationRegistration().fromJsonString(jsonString, options); + } + + static equals(a: PushNotificationRegistration | PlainMessage | undefined, b: PushNotificationRegistration | PlainMessage | undefined): boolean { + return proto3.util.equals(PushNotificationRegistration, a, b); + } +} + +/** + * @generated from enum PushNotificationRegistration.TokenType + */ +export enum PushNotificationRegistration_TokenType { + /** + * @generated from enum value: UNKNOWN_TOKEN_TYPE = 0; + */ + UNKNOWN_TOKEN_TYPE = 0, + + /** + * @generated from enum value: APN_TOKEN = 1; + */ + APN_TOKEN = 1, + + /** + * @generated from enum value: FIREBASE_TOKEN = 2; + */ + FIREBASE_TOKEN = 2, +} +// Retrieve enum metadata with: proto3.getEnumType(PushNotificationRegistration_TokenType) +proto3.util.setEnumType(PushNotificationRegistration_TokenType, "PushNotificationRegistration.TokenType", [ + { no: 0, name: "UNKNOWN_TOKEN_TYPE" }, + { no: 1, name: "APN_TOKEN" }, + { no: 2, name: "FIREBASE_TOKEN" }, +]); + +/** + * @generated from message PushNotificationRegistrationResponse + */ +export class PushNotificationRegistrationResponse extends Message { + /** + * @generated from field: bool success = 1; + */ + success = false; + + /** + * @generated from field: PushNotificationRegistrationResponse.ErrorType error = 2; + */ + error = PushNotificationRegistrationResponse_ErrorType.UNKNOWN_ERROR_TYPE; + + /** + * @generated from field: bytes request_id = 3; + */ + requestId = new Uint8Array(0); + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime = proto3; + static readonly typeName = "PushNotificationRegistrationResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "success", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, + { no: 2, name: "error", kind: "enum", T: proto3.getEnumType(PushNotificationRegistrationResponse_ErrorType) }, + { no: 3, name: "request_id", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): PushNotificationRegistrationResponse { + return new PushNotificationRegistrationResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): PushNotificationRegistrationResponse { + return new PushNotificationRegistrationResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): PushNotificationRegistrationResponse { + return new PushNotificationRegistrationResponse().fromJsonString(jsonString, options); + } + + static equals(a: PushNotificationRegistrationResponse | PlainMessage | undefined, b: PushNotificationRegistrationResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(PushNotificationRegistrationResponse, a, b); + } +} + +/** + * @generated from enum PushNotificationRegistrationResponse.ErrorType + */ +export enum PushNotificationRegistrationResponse_ErrorType { + /** + * @generated from enum value: UNKNOWN_ERROR_TYPE = 0; + */ + UNKNOWN_ERROR_TYPE = 0, + + /** + * @generated from enum value: MALFORMED_MESSAGE = 1; + */ + MALFORMED_MESSAGE = 1, + + /** + * @generated from enum value: VERSION_MISMATCH = 2; + */ + VERSION_MISMATCH = 2, + + /** + * @generated from enum value: UNSUPPORTED_TOKEN_TYPE = 3; + */ + UNSUPPORTED_TOKEN_TYPE = 3, + + /** + * @generated from enum value: INTERNAL_ERROR = 4; + */ + INTERNAL_ERROR = 4, +} +// Retrieve enum metadata with: proto3.getEnumType(PushNotificationRegistrationResponse_ErrorType) +proto3.util.setEnumType(PushNotificationRegistrationResponse_ErrorType, "PushNotificationRegistrationResponse.ErrorType", [ + { no: 0, name: "UNKNOWN_ERROR_TYPE" }, + { no: 1, name: "MALFORMED_MESSAGE" }, + { no: 2, name: "VERSION_MISMATCH" }, + { no: 3, name: "UNSUPPORTED_TOKEN_TYPE" }, + { no: 4, name: "INTERNAL_ERROR" }, +]); + +/** + * @generated from message ContactCodeAdvertisement + */ +export class ContactCodeAdvertisement extends Message { + /** + * @generated from field: repeated PushNotificationQueryInfo push_notification_info = 1; + */ + pushNotificationInfo: PushNotificationQueryInfo[] = []; + + /** + * @generated from field: ChatIdentity chat_identity = 2; + */ + chatIdentity?: ChatIdentity; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime = proto3; + static readonly typeName = "ContactCodeAdvertisement"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "push_notification_info", kind: "message", T: PushNotificationQueryInfo, repeated: true }, + { no: 2, name: "chat_identity", kind: "message", T: ChatIdentity }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): ContactCodeAdvertisement { + return new ContactCodeAdvertisement().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): ContactCodeAdvertisement { + return new ContactCodeAdvertisement().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): ContactCodeAdvertisement { + return new ContactCodeAdvertisement().fromJsonString(jsonString, options); + } + + static equals(a: ContactCodeAdvertisement | PlainMessage | undefined, b: ContactCodeAdvertisement | PlainMessage | undefined): boolean { + return proto3.util.equals(ContactCodeAdvertisement, a, b); + } +} + +/** + * @generated from message PushNotificationQuery + */ +export class PushNotificationQuery extends Message { + /** + * @generated from field: repeated bytes public_keys = 1; + */ + publicKeys: Uint8Array[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime = proto3; + static readonly typeName = "PushNotificationQuery"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "public_keys", kind: "scalar", T: 12 /* ScalarType.BYTES */, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): PushNotificationQuery { + return new PushNotificationQuery().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): PushNotificationQuery { + return new PushNotificationQuery().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): PushNotificationQuery { + return new PushNotificationQuery().fromJsonString(jsonString, options); + } + + static equals(a: PushNotificationQuery | PlainMessage | undefined, b: PushNotificationQuery | PlainMessage | undefined): boolean { + return proto3.util.equals(PushNotificationQuery, a, b); + } +} + +/** + * @generated from message PushNotificationQueryInfo + */ +export class PushNotificationQueryInfo extends Message { + /** + * @generated from field: string access_token = 1; + */ + accessToken = ""; + + /** + * @generated from field: string installation_id = 2; + */ + installationId = ""; + + /** + * @generated from field: bytes public_key = 3; + */ + publicKey = new Uint8Array(0); + + /** + * @generated from field: repeated bytes allowed_key_list = 4; + */ + allowedKeyList: Uint8Array[] = []; + + /** + * @generated from field: bytes grant = 5; + */ + grant = new Uint8Array(0); + + /** + * @generated from field: uint64 version = 6; + */ + version = protoInt64.zero; + + /** + * @generated from field: bytes server_public_key = 7; + */ + serverPublicKey = new Uint8Array(0); + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime = proto3; + static readonly typeName = "PushNotificationQueryInfo"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "access_token", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "installation_id", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 3, name: "public_key", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 4, name: "allowed_key_list", kind: "scalar", T: 12 /* ScalarType.BYTES */, repeated: true }, + { no: 5, name: "grant", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 6, name: "version", kind: "scalar", T: 4 /* ScalarType.UINT64 */ }, + { no: 7, name: "server_public_key", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): PushNotificationQueryInfo { + return new PushNotificationQueryInfo().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): PushNotificationQueryInfo { + return new PushNotificationQueryInfo().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): PushNotificationQueryInfo { + return new PushNotificationQueryInfo().fromJsonString(jsonString, options); + } + + static equals(a: PushNotificationQueryInfo | PlainMessage | undefined, b: PushNotificationQueryInfo | PlainMessage | undefined): boolean { + return proto3.util.equals(PushNotificationQueryInfo, a, b); + } +} + +/** + * @generated from message PushNotificationQueryResponse + */ +export class PushNotificationQueryResponse extends Message { + /** + * @generated from field: repeated PushNotificationQueryInfo info = 1; + */ + info: PushNotificationQueryInfo[] = []; + + /** + * @generated from field: bytes message_id = 2; + */ + messageId = new Uint8Array(0); + + /** + * @generated from field: bool success = 3; + */ + success = false; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime = proto3; + static readonly typeName = "PushNotificationQueryResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "info", kind: "message", T: PushNotificationQueryInfo, repeated: true }, + { no: 2, name: "message_id", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 3, name: "success", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): PushNotificationQueryResponse { + return new PushNotificationQueryResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): PushNotificationQueryResponse { + return new PushNotificationQueryResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): PushNotificationQueryResponse { + return new PushNotificationQueryResponse().fromJsonString(jsonString, options); + } + + static equals(a: PushNotificationQueryResponse | PlainMessage | undefined, b: PushNotificationQueryResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(PushNotificationQueryResponse, a, b); + } +} + +/** + * @generated from message PushNotification + */ +export class PushNotification extends Message { + /** + * @generated from field: string access_token = 1; + */ + accessToken = ""; + + /** + * @generated from field: bytes chat_id = 2; + */ + chatId = new Uint8Array(0); + + /** + * @generated from field: bytes public_key = 3; + */ + publicKey = new Uint8Array(0); + + /** + * @generated from field: string installation_id = 4; + */ + installationId = ""; + + /** + * @generated from field: bytes message = 5; + */ + message = new Uint8Array(0); + + /** + * @generated from field: PushNotification.PushNotificationType type = 6; + */ + type = PushNotification_PushNotificationType.UNKNOWN_PUSH_NOTIFICATION_TYPE; + + /** + * @generated from field: bytes author = 7; + */ + author = new Uint8Array(0); + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime = proto3; + static readonly typeName = "PushNotification"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "access_token", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "chat_id", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 3, name: "public_key", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 4, name: "installation_id", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 5, name: "message", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 6, name: "type", kind: "enum", T: proto3.getEnumType(PushNotification_PushNotificationType) }, + { no: 7, name: "author", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): PushNotification { + return new PushNotification().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): PushNotification { + return new PushNotification().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): PushNotification { + return new PushNotification().fromJsonString(jsonString, options); + } + + static equals(a: PushNotification | PlainMessage | undefined, b: PushNotification | PlainMessage | undefined): boolean { + return proto3.util.equals(PushNotification, a, b); + } +} + +/** + * @generated from enum PushNotification.PushNotificationType + */ +export enum PushNotification_PushNotificationType { + /** + * @generated from enum value: UNKNOWN_PUSH_NOTIFICATION_TYPE = 0; + */ + UNKNOWN_PUSH_NOTIFICATION_TYPE = 0, + + /** + * @generated from enum value: MESSAGE = 1; + */ + MESSAGE = 1, + + /** + * @generated from enum value: MENTION = 2; + */ + MENTION = 2, + + /** + * @generated from enum value: REQUEST_TO_JOIN_COMMUNITY = 3; + */ + REQUEST_TO_JOIN_COMMUNITY = 3, +} +// Retrieve enum metadata with: proto3.getEnumType(PushNotification_PushNotificationType) +proto3.util.setEnumType(PushNotification_PushNotificationType, "PushNotification.PushNotificationType", [ + { no: 0, name: "UNKNOWN_PUSH_NOTIFICATION_TYPE" }, + { no: 1, name: "MESSAGE" }, + { no: 2, name: "MENTION" }, + { no: 3, name: "REQUEST_TO_JOIN_COMMUNITY" }, +]); + +/** + * @generated from message PushNotificationRequest + */ +export class PushNotificationRequest extends Message { + /** + * @generated from field: repeated PushNotification requests = 1; + */ + requests: PushNotification[] = []; + + /** + * @generated from field: bytes message_id = 2; + */ + messageId = new Uint8Array(0); + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime = proto3; + static readonly typeName = "PushNotificationRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "requests", kind: "message", T: PushNotification, repeated: true }, + { no: 2, name: "message_id", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): PushNotificationRequest { + return new PushNotificationRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): PushNotificationRequest { + return new PushNotificationRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): PushNotificationRequest { + return new PushNotificationRequest().fromJsonString(jsonString, options); + } + + static equals(a: PushNotificationRequest | PlainMessage | undefined, b: PushNotificationRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(PushNotificationRequest, a, b); + } +} + +/** + * @generated from message PushNotificationReport + */ +export class PushNotificationReport extends Message { + /** + * @generated from field: bool success = 1; + */ + success = false; + + /** + * @generated from field: PushNotificationReport.ErrorType error = 2; + */ + error = PushNotificationReport_ErrorType.UNKNOWN_ERROR_TYPE; + + /** + * @generated from field: bytes public_key = 3; + */ + publicKey = new Uint8Array(0); + + /** + * @generated from field: string installation_id = 4; + */ + installationId = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime = proto3; + static readonly typeName = "PushNotificationReport"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "success", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, + { no: 2, name: "error", kind: "enum", T: proto3.getEnumType(PushNotificationReport_ErrorType) }, + { no: 3, name: "public_key", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 4, name: "installation_id", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): PushNotificationReport { + return new PushNotificationReport().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): PushNotificationReport { + return new PushNotificationReport().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): PushNotificationReport { + return new PushNotificationReport().fromJsonString(jsonString, options); + } + + static equals(a: PushNotificationReport | PlainMessage | undefined, b: PushNotificationReport | PlainMessage | undefined): boolean { + return proto3.util.equals(PushNotificationReport, a, b); + } +} + +/** + * @generated from enum PushNotificationReport.ErrorType + */ +export enum PushNotificationReport_ErrorType { + /** + * @generated from enum value: UNKNOWN_ERROR_TYPE = 0; + */ + UNKNOWN_ERROR_TYPE = 0, + + /** + * @generated from enum value: WRONG_TOKEN = 1; + */ + WRONG_TOKEN = 1, + + /** + * @generated from enum value: INTERNAL_ERROR = 2; + */ + INTERNAL_ERROR = 2, + + /** + * @generated from enum value: NOT_REGISTERED = 3; + */ + NOT_REGISTERED = 3, +} +// Retrieve enum metadata with: proto3.getEnumType(PushNotificationReport_ErrorType) +proto3.util.setEnumType(PushNotificationReport_ErrorType, "PushNotificationReport.ErrorType", [ + { no: 0, name: "UNKNOWN_ERROR_TYPE" }, + { no: 1, name: "WRONG_TOKEN" }, + { no: 2, name: "INTERNAL_ERROR" }, + { no: 3, name: "NOT_REGISTERED" }, +]); + +/** + * @generated from message PushNotificationResponse + */ +export class PushNotificationResponse extends Message { + /** + * @generated from field: bytes message_id = 1; + */ + messageId = new Uint8Array(0); + + /** + * @generated from field: repeated PushNotificationReport reports = 2; + */ + reports: PushNotificationReport[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime = proto3; + static readonly typeName = "PushNotificationResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "message_id", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 2, name: "reports", kind: "message", T: PushNotificationReport, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): PushNotificationResponse { + return new PushNotificationResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): PushNotificationResponse { + return new PushNotificationResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): PushNotificationResponse { + return new PushNotificationResponse().fromJsonString(jsonString, options); + } + + static equals(a: PushNotificationResponse | PlainMessage | undefined, b: PushNotificationResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(PushNotificationResponse, a, b); + } +} + diff --git a/packages/status-js/src/request-client/map-channel.ts b/packages/status-js/src/request-client/map-channel.ts new file mode 100644 index 000000000..f794af1d1 --- /dev/null +++ b/packages/status-js/src/request-client/map-channel.ts @@ -0,0 +1,42 @@ +import { mapCommunity } from './map-community' + +import type { + CommunityChat, + CommunityDescription, +} from '../protos/communities_pb' +import type { CommunityInfo } from './map-community' + +export type ChannelInfo = { + emoji?: string + displayName: string + description: string + color: string + community: CommunityInfo +} + +export function mapChannel( + communityChat: CommunityChat, + communityDescription: CommunityDescription +): ChannelInfo | undefined { + const community = mapCommunity(communityDescription) + + if (!community) { + return + } + + const { identity } = communityChat + + if (!identity) { + return + } + + const channelInfo: ChannelInfo = { + emoji: identity.emoji, + displayName: identity.displayName, + description: identity.description, + color: identity.color, + community, + } + + return channelInfo +} diff --git a/packages/status-js/src/request-client/map-community.ts b/packages/status-js/src/request-client/map-community.ts new file mode 100644 index 000000000..21f66b368 --- /dev/null +++ b/packages/status-js/src/request-client/map-community.ts @@ -0,0 +1,51 @@ +import { tags as tagsMap } from './tags' + +import type { CommunityDescription } from '../protos/communities_pb' + +export type CommunityInfo = { + banner?: Uint8Array + photo?: Uint8Array + displayName: string + description: string + membersCount: number + tags: Array<{ + emoji: string + text: string + }> + color: string +} + +export function mapCommunity( + communityDescription: CommunityDescription +): CommunityInfo | undefined { + const { identity, tags, members } = communityDescription + + if (!identity) { + return + } + + const communityInfo: CommunityInfo = { + banner: identity.images.banner?.payload, + photo: identity.images.thumbnail?.payload, + displayName: identity.displayName, + description: identity.description, + membersCount: Object.keys(members).length, + tags: tags.reduce((tags, nextTag) => { + const emoji = tagsMap[nextTag as keyof typeof tagsMap] + + if (!emoji) { + return tags + } + + tags.push({ + text: nextTag, + emoji, + }) + + return tags + }, []), + color: identity.color, + } + + return communityInfo +} diff --git a/packages/status-js/src/request-client/map-user.ts b/packages/status-js/src/request-client/map-user.ts new file mode 100644 index 000000000..5519aa9d5 --- /dev/null +++ b/packages/status-js/src/request-client/map-user.ts @@ -0,0 +1,32 @@ +import { publicKeyToEmojiHash } from '../utils/public-key-to-emoji-hash' + +import type { ContactCodeAdvertisement } from '../protos/push-notifications_pb' + +export type UserInfo = { + photo?: Uint8Array + displayName: string + description?: string + emojiHash: string + // todo: currently not in protobuf nor in product + // color: string +} + +export function mapUser( + contactCodeAdvertisement: ContactCodeAdvertisement, + userPublicKey: string +): UserInfo | undefined { + const { chatIdentity: identity } = contactCodeAdvertisement + + if (!identity) { + return + } + + const userInfo: UserInfo = { + photo: identity.images.thumbnail?.payload, + displayName: identity.displayName, + description: identity.description, + emojiHash: publicKeyToEmojiHash(userPublicKey), + } + + return userInfo +} diff --git a/packages/status-js/src/request-client/request-client.ts b/packages/status-js/src/request-client/request-client.ts new file mode 100644 index 000000000..e179632af --- /dev/null +++ b/packages/status-js/src/request-client/request-client.ts @@ -0,0 +1,320 @@ +import { bytesToHex } from 'ethereum-cryptography/utils' +import { Protocols } from 'js-waku' +import { createLightNode } from 'js-waku/lib/create_waku' +import { PeerDiscoveryStaticPeers } from 'js-waku/lib/peer_discovery_static_list' +import { waitForRemotePeer } from 'js-waku/lib/wait_for_remote_peer' +import { SymDecoder } from 'js-waku/lib/waku_message/version_1' + +import { peers } from '../consts/peers' +import { + ApplicationMetadataMessage, + ApplicationMetadataMessage_Type, +} from '../protos/application-metadata-message_pb' +import { CommunityDescription } from '../protos/communities_pb' +import { ProtocolMessage } from '../protos/protocol-message_pb' +import { ContactCodeAdvertisement } from '../protos/push-notifications_pb' +import { compressPublicKey } from '../utils/compress-public-key' +import { generateKeyFromPassword } from '../utils/generate-key-from-password' +import { idToContentTopic } from '../utils/id-to-content-topic' +import { isClockValid } from '../utils/is-clock-valid' +import { payloadToId } from '../utils/payload-to-id' +import { recoverPublicKey } from '../utils/recover-public-key' +import { mapChannel } from './map-channel' +import { mapCommunity } from './map-community' +import { mapUser } from './map-user' + +import type { ChannelInfo } from './map-channel' +import type { CommunityInfo } from './map-community' +import type { UserInfo } from './map-user' +import type { WakuLight } from 'js-waku/lib/interfaces' +import type { MessageV1 as WakuMessage } from 'js-waku/lib/waku_message/version_1' + +export interface RequestClientOptions { + environment?: 'production' | 'test' +} + +class RequestClient { + public waku: WakuLight + /** Cache. */ + public readonly wakuMessages: Set + + constructor(waku: WakuLight) { + this.waku = waku + this.wakuMessages = new Set() + } + + static async start(options: RequestClientOptions): Promise { + const { environment = 'production' } = options + + let waku: WakuLight | undefined + let client: RequestClient | undefined + + try { + // Waku + waku = await createLightNode({ + defaultBootstrap: false, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + emitSelf: true, + pingKeepAlive: 0, + relayKeepAlive: 0, + libp2p: { + peerDiscovery: [ + new PeerDiscoveryStaticPeers(peers[environment], { maxPeers: 1 }), + ], + }, + }) + await waku.start() + await waitForRemotePeer(waku, [Protocols.Store], 10 * 1000) + + client = new RequestClient(waku) + } catch (error) { + if (waku) { + await waku.stop() + } + + throw error + } + + return client + } + + public async stop() { + await this.waku.stop() + } + + public fetchCommunity = async ( + /** Uncompressed */ + publicKey: string + ): Promise => { + const communityDescription = await this.fetchCommunityDescription(publicKey) + + if (!communityDescription) { + return + } + + return mapCommunity(communityDescription) + } + + public fetchChannel = async ( + /** Compressed */ + publicKey: string, + uuid: string + ): Promise => { + const communityDescription = await this.fetchCommunityDescription(publicKey) + + if (!communityDescription) { + return + } + + const communityChat = communityDescription.chats[uuid] + + return mapChannel(communityChat, communityDescription) + } + + public fetchUser = async ( + /** Uncompressed */ + publicKey: string + ): Promise => { + const contactCodeAdvertisement = await this.fetchContactCodeAdvertisement( + publicKey + ) + + if (!contactCodeAdvertisement) { + return + } + + return mapUser(contactCodeAdvertisement, publicKey) + } + + private fetchCommunityDescription = async ( + /** Uncompressed */ + publicKey: string + ): Promise => { + const contentTopic = idToContentTopic(publicKey) + const symmetricKey = await generateKeyFromPassword(publicKey) + + let communityDescription: CommunityDescription | undefined = undefined + await this.waku.store.queryOrderedCallback( + [new SymDecoder(contentTopic, symmetricKey)], + wakuMessage => { + // handle + const message = this.handleWakuMessage(wakuMessage) + + if (!message) { + return + } + + if ( + message.type !== ApplicationMetadataMessage_Type.COMMUNITY_DESCRIPTION + ) { + return + } + + // decode + const decodedCommunityDescription = CommunityDescription.fromBinary( + message.payload + ) + + // validate + if ( + !isClockValid( + BigInt(decodedCommunityDescription.clock), + message.timestamp + ) + ) { + return + } + + if (publicKey !== `0x${compressPublicKey(message.signerPublicKey)}`) { + return + } + + if (!communityDescription) { + communityDescription = decodedCommunityDescription + } + + // stop + return true + } + ) + + return communityDescription + } + + private fetchContactCodeAdvertisement = async ( + publicKey: string + ): Promise => { + const contentTopic = idToContentTopic(`${publicKey}-contact-code`) + const symmetricKey = await generateKeyFromPassword( + `${publicKey}-contact-code` + ) + + let contactCodeAdvertisement: ContactCodeAdvertisement | undefined = + undefined + await this.waku.store.queryOrderedCallback( + [new SymDecoder(contentTopic, symmetricKey)], + wakuMessage => { + // handle + const message = this.handleWakuMessage(wakuMessage) + + if (!message) { + return + } + + if ( + message.type !== + ApplicationMetadataMessage_Type.CONTACT_CODE_ADVERTISEMENT + ) { + return + } + + // decode + const decodedContactCode = ContactCodeAdvertisement.fromBinary( + message.payload + ) + + // validate + if (!decodedContactCode.chatIdentity) { + return + } + + if ( + !isClockValid( + BigInt(decodedContactCode.chatIdentity.clock), + message.timestamp + ) + ) { + return + } + + if (publicKey !== message.signerPublicKey) { + return + } + + if (!contactCodeAdvertisement) { + contactCodeAdvertisement = decodedContactCode + } + + // stop + return true + } + ) + + return contactCodeAdvertisement + } + + private handleWakuMessage = ( + wakuMessage: WakuMessage + ): + | { + timestamp: Date + signerPublicKey: string + type: ApplicationMetadataMessage_Type + payload: Uint8Array + } + | undefined => { + // validate + if (!wakuMessage.payload) { + return + } + + if (!wakuMessage.signaturePublicKey) { + return + } + + if (!wakuMessage.timestamp) { + return + } + + // decode (layers) + let messageToDecode = wakuMessage.payload + let decodedProtocol + try { + decodedProtocol = ProtocolMessage.fromBinary(messageToDecode) + if (decodedProtocol) { + messageToDecode = decodedProtocol.publicMessage + } + } catch { + // eslint-disable-next-line no-empty + } + + const decodedMetadata = + ApplicationMetadataMessage.fromBinary(messageToDecode) + if (!decodedMetadata.payload) { + return + } + + const signerPublicKeyBytes = recoverPublicKey( + decodedMetadata.signature, + decodedMetadata.payload + ) + + const messageId = payloadToId( + decodedProtocol?.publicMessage ?? wakuMessage.payload, + signerPublicKeyBytes + ) + + // already handled + if (this.wakuMessages.has(messageId)) { + return + } + + this.wakuMessages.add(messageId) + + return { + timestamp: wakuMessage.timestamp, + signerPublicKey: `0x${bytesToHex(signerPublicKeyBytes)}`, + type: decodedMetadata.type, + payload: decodedMetadata.payload, + } + } +} + +export async function createRequestClient( + options: RequestClientOptions +): Promise { + return await RequestClient.start(options) +} + +export type { RequestClient } diff --git a/packages/status-js/src/request-client/tags.ts b/packages/status-js/src/request-client/tags.ts new file mode 100644 index 000000000..42d622c30 --- /dev/null +++ b/packages/status-js/src/request-client/tags.ts @@ -0,0 +1,54 @@ +export const tags = { + Activism: 'โœŠ', + Art: '๐ŸŽจ', + Blockchain: '๐Ÿ”—', + 'Books & blogs': '๐Ÿ“š', + Career: '๐Ÿ’ผ', + Collaboration: '๐Ÿค', + Commerce: '๐Ÿ›’', + Culture: '๐ŸŽŽ', + DAO: '๐Ÿš€', + DeFi: '๐Ÿ“ˆ', + Design: '๐Ÿงฉ', + DIY: '๐Ÿ”จ', + Environment: '๐ŸŒฟ', + Education: '๐ŸŽ’', + Entertainment: '๐Ÿฟ', + Ethereum: 'ฮž', + Event: '๐Ÿ—“', + Fantasy: '๐Ÿง™โ€โ™‚๏ธ', + Fashion: '๐Ÿงฆ', + Food: '๐ŸŒถ', + Gaming: '๐ŸŽฎ', + Global: '๐ŸŒ', + Health: '๐Ÿง ', + Hobby: '๐Ÿ“', + Innovation: '๐Ÿงช', + Language: '๐Ÿ“œ', + Lifestyle: 'โœจ', + Local: '๐Ÿ“', + Love: 'โค๏ธ', + Markets: '๐Ÿ’Ž', + 'Movies & TV': '๐ŸŽž', + Music: '๐ŸŽถ', + News: '๐Ÿ—ž', + NFT: '๐Ÿ–ผ', + 'Non-profit': '๐Ÿ™', + NSFW: '๐Ÿ†', + Org: '๐Ÿข', + Pets: '๐Ÿถ', + Play: '๐ŸŽฒ', + Podcast: '๐ŸŽ™๏ธ', + Politics: '๐Ÿ—ณ๏ธ', + Product: '๐Ÿฑ', + Psyche: '๐Ÿ', + Privacy: '๐Ÿ‘ป', + Security: '๐Ÿ”’', + Social: 'โ˜•', + 'Software dev': '๐Ÿ‘ฉโ€๐Ÿ’ป', + Sports: 'โšฝ๏ธ', + Tech: '๐Ÿ“ฑ', + Travel: '๐Ÿ—บ', + Vehicles: '๐Ÿš•', + Web3: '๐ŸŒ', +}