'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var platformAdaptersNode = require('@leancloud/platform-adapters-node'); var protobufLight = _interopDefault(require('protobufjs/dist/protobuf-light')); var EventEmitter = _interopDefault(require('eventemitter3')); var d = _interopDefault(require('debug')); var shuffle = _interopDefault(require('lodash/shuffle')); var values = _interopDefault(require('lodash/values')); var StateMachine = _interopDefault(require('javascript-state-machine')); var isPlainObject = _interopDefault(require('lodash/isPlainObject')); var promiseTimeout = require('promise-timeout'); var uuid = _interopDefault(require('uuid/v4')); var base64Arraybuffer = require('base64-arraybuffer'); var remove = _interopDefault(require('lodash/remove')); var isEmpty = _interopDefault(require('lodash/isEmpty')); var cloneDeep = _interopDefault(require('lodash/cloneDeep')); var find = _interopDefault(require('lodash/find')); var get = _interopDefault(require('lodash/get')); var messageCompiled = protobufLight.newBuilder({})['import']({ package: 'push_server.messages2', syntax: 'proto2', options: { objc_class_prefix: 'AVIM' }, messages: [{ name: 'JsonObjectMessage', syntax: 'proto2', fields: [{ rule: 'required', type: 'string', name: 'data', id: 1 }] }, { name: 'UnreadTuple', syntax: 'proto2', fields: [{ rule: 'required', type: 'string', name: 'cid', id: 1 }, { rule: 'required', type: 'int32', name: 'unread', id: 2 }, { rule: 'optional', type: 'string', name: 'mid', id: 3 }, { rule: 'optional', type: 'int64', name: 'timestamp', id: 4 }, { rule: 'optional', type: 'string', name: 'from', id: 5 }, { rule: 'optional', type: 'string', name: 'data', id: 6 }, { rule: 'optional', type: 'int64', name: 'patchTimestamp', id: 7 }, { rule: 'optional', type: 'bool', name: 'mentioned', id: 8 }, { rule: 'optional', type: 'bytes', name: 'binaryMsg', id: 9 }, { rule: 'optional', type: 'int32', name: 'convType', id: 10 }] }, { name: 'LogItem', syntax: 'proto2', fields: [{ rule: 'optional', type: 'string', name: 'from', id: 1 }, { rule: 'optional', type: 'string', name: 'data', id: 2 }, { rule: 'optional', type: 'int64', name: 'timestamp', id: 3 }, { rule: 'optional', type: 'string', name: 'msgId', id: 4 }, { rule: 'optional', type: 'int64', name: 'ackAt', id: 5 }, { rule: 'optional', type: 'int64', name: 'readAt', id: 6 }, { rule: 'optional', type: 'int64', name: 'patchTimestamp', id: 7 }, { rule: 'optional', type: 'bool', name: 'mentionAll', id: 8 }, { rule: 'repeated', type: 'string', name: 'mentionPids', id: 9 }, { rule: 'optional', type: 'bool', name: 'bin', id: 10 }, { rule: 'optional', type: 'int32', name: 'convType', id: 11 }] }, { name: 'ConvMemberInfo', syntax: 'proto2', fields: [{ rule: 'optional', type: 'string', name: 'pid', id: 1 }, { rule: 'optional', type: 'string', name: 'role', id: 2 }, { rule: 'optional', type: 'string', name: 'infoId', id: 3 }] }, { name: 'DataCommand', syntax: 'proto2', fields: [{ rule: 'repeated', type: 'string', name: 'ids', id: 1 }, { rule: 'repeated', type: 'JsonObjectMessage', name: 'msg', id: 2 }, { rule: 'optional', type: 'bool', name: 'offline', id: 3 }] }, { name: 'SessionCommand', syntax: 'proto2', fields: [{ rule: 'optional', type: 'int64', name: 't', id: 1 }, { rule: 'optional', type: 'string', name: 'n', id: 2 }, { rule: 'optional', type: 'string', name: 's', id: 3 }, { rule: 'optional', type: 'string', name: 'ua', id: 4 }, { rule: 'optional', type: 'bool', name: 'r', id: 5 }, { rule: 'optional', type: 'string', name: 'tag', id: 6 }, { rule: 'optional', type: 'string', name: 'deviceId', id: 7 }, { rule: 'repeated', type: 'string', name: 'sessionPeerIds', id: 8 }, { rule: 'repeated', type: 'string', name: 'onlineSessionPeerIds', id: 9 }, { rule: 'optional', type: 'string', name: 'st', id: 10 }, { rule: 'optional', type: 'int32', name: 'stTtl', id: 11 }, { rule: 'optional', type: 'int32', name: 'code', id: 12 }, { rule: 'optional', type: 'string', name: 'reason', id: 13 }, { rule: 'optional', type: 'string', name: 'deviceToken', id: 14 }, { rule: 'optional', type: 'bool', name: 'sp', id: 15 }, { rule: 'optional', type: 'string', name: 'detail', id: 16 }, { rule: 'optional', type: 'int64', name: 'lastUnreadNotifTime', id: 17 }, { rule: 'optional', type: 'int64', name: 'lastPatchTime', id: 18 }, { rule: 'optional', type: 'int64', name: 'configBitmap', id: 19 }] }, { name: 'ErrorCommand', syntax: 'proto2', fields: [{ rule: 'required', type: 'int32', name: 'code', id: 1 }, { rule: 'required', type: 'string', name: 'reason', id: 2 }, { rule: 'optional', type: 'int32', name: 'appCode', id: 3 }, { rule: 'optional', type: 'string', name: 'detail', id: 4 }, { rule: 'repeated', type: 'string', name: 'pids', id: 5 }, { rule: 'optional', type: 'string', name: 'appMsg', id: 6 }] }, { name: 'DirectCommand', syntax: 'proto2', fields: [{ rule: 'optional', type: 'string', name: 'msg', id: 1 }, { rule: 'optional', type: 'string', name: 'uid', id: 2 }, { rule: 'optional', type: 'string', name: 'fromPeerId', id: 3 }, { rule: 'optional', type: 'int64', name: 'timestamp', id: 4 }, { rule: 'optional', type: 'bool', name: 'offline', id: 5 }, { rule: 'optional', type: 'bool', name: 'hasMore', id: 6 }, { rule: 'repeated', type: 'string', name: 'toPeerIds', id: 7 }, { rule: 'optional', type: 'bool', name: 'r', id: 10 }, { rule: 'optional', type: 'string', name: 'cid', id: 11 }, { rule: 'optional', type: 'string', name: 'id', id: 12 }, { rule: 'optional', type: 'bool', name: 'transient', id: 13 }, { rule: 'optional', type: 'string', name: 'dt', id: 14 }, { rule: 'optional', type: 'string', name: 'roomId', id: 15 }, { rule: 'optional', type: 'string', name: 'pushData', id: 16 }, { rule: 'optional', type: 'bool', name: 'will', id: 17 }, { rule: 'optional', type: 'int64', name: 'patchTimestamp', id: 18 }, { rule: 'optional', type: 'bytes', name: 'binaryMsg', id: 19 }, { rule: 'repeated', type: 'string', name: 'mentionPids', id: 20 }, { rule: 'optional', type: 'bool', name: 'mentionAll', id: 21 }, { rule: 'optional', type: 'int32', name: 'convType', id: 22 }] }, { name: 'AckCommand', syntax: 'proto2', fields: [{ rule: 'optional', type: 'int32', name: 'code', id: 1 }, { rule: 'optional', type: 'string', name: 'reason', id: 2 }, { rule: 'optional', type: 'string', name: 'mid', id: 3 }, { rule: 'optional', type: 'string', name: 'cid', id: 4 }, { rule: 'optional', type: 'int64', name: 't', id: 5 }, { rule: 'optional', type: 'string', name: 'uid', id: 6 }, { rule: 'optional', type: 'int64', name: 'fromts', id: 7 }, { rule: 'optional', type: 'int64', name: 'tots', id: 8 }, { rule: 'optional', type: 'string', name: 'type', id: 9 }, { rule: 'repeated', type: 'string', name: 'ids', id: 10 }, { rule: 'optional', type: 'int32', name: 'appCode', id: 11 }, { rule: 'optional', type: 'string', name: 'appMsg', id: 12 }] }, { name: 'UnreadCommand', syntax: 'proto2', fields: [{ rule: 'repeated', type: 'UnreadTuple', name: 'convs', id: 1 }, { rule: 'optional', type: 'int64', name: 'notifTime', id: 2 }] }, { name: 'ConvCommand', syntax: 'proto2', fields: [{ rule: 'repeated', type: 'string', name: 'm', id: 1 }, { rule: 'optional', type: 'bool', name: 'transient', id: 2 }, { rule: 'optional', type: 'bool', name: 'unique', id: 3 }, { rule: 'optional', type: 'string', name: 'cid', id: 4 }, { rule: 'optional', type: 'string', name: 'cdate', id: 5 }, { rule: 'optional', type: 'string', name: 'initBy', id: 6 }, { rule: 'optional', type: 'string', name: 'sort', id: 7 }, { rule: 'optional', type: 'int32', name: 'limit', id: 8 }, { rule: 'optional', type: 'int32', name: 'skip', id: 9 }, { rule: 'optional', type: 'int32', name: 'flag', id: 10 }, { rule: 'optional', type: 'int32', name: 'count', id: 11 }, { rule: 'optional', type: 'string', name: 'udate', id: 12 }, { rule: 'optional', type: 'int64', name: 't', id: 13 }, { rule: 'optional', type: 'string', name: 'n', id: 14 }, { rule: 'optional', type: 'string', name: 's', id: 15 }, { rule: 'optional', type: 'bool', name: 'statusSub', id: 16 }, { rule: 'optional', type: 'bool', name: 'statusPub', id: 17 }, { rule: 'optional', type: 'int32', name: 'statusTTL', id: 18 }, { rule: 'optional', type: 'string', name: 'uniqueId', id: 19 }, { rule: 'optional', type: 'string', name: 'targetClientId', id: 20 }, { rule: 'optional', type: 'int64', name: 'maxReadTimestamp', id: 21 }, { rule: 'optional', type: 'int64', name: 'maxAckTimestamp', id: 22 }, { rule: 'optional', type: 'bool', name: 'queryAllMembers', id: 23 }, { rule: 'repeated', type: 'MaxReadTuple', name: 'maxReadTuples', id: 24 }, { rule: 'repeated', type: 'string', name: 'cids', id: 25 }, { rule: 'optional', type: 'ConvMemberInfo', name: 'info', id: 26 }, { rule: 'optional', type: 'bool', name: 'tempConv', id: 27 }, { rule: 'optional', type: 'int32', name: 'tempConvTTL', id: 28 }, { rule: 'repeated', type: 'string', name: 'tempConvIds', id: 29 }, { rule: 'repeated', type: 'string', name: 'allowedPids', id: 30 }, { rule: 'repeated', type: 'ErrorCommand', name: 'failedPids', id: 31 }, { rule: 'optional', type: 'string', name: 'next', id: 40 }, { rule: 'optional', type: 'JsonObjectMessage', name: 'results', id: 100 }, { rule: 'optional', type: 'JsonObjectMessage', name: 'where', id: 101 }, { rule: 'optional', type: 'JsonObjectMessage', name: 'attr', id: 103 }, { rule: 'optional', type: 'JsonObjectMessage', name: 'attrModified', id: 104 }] }, { name: 'RoomCommand', syntax: 'proto2', fields: [{ rule: 'optional', type: 'string', name: 'roomId', id: 1 }, { rule: 'optional', type: 'string', name: 's', id: 2 }, { rule: 'optional', type: 'int64', name: 't', id: 3 }, { rule: 'optional', type: 'string', name: 'n', id: 4 }, { rule: 'optional', type: 'bool', name: 'transient', id: 5 }, { rule: 'repeated', type: 'string', name: 'roomPeerIds', id: 6 }, { rule: 'optional', type: 'string', name: 'byPeerId', id: 7 }] }, { name: 'LogsCommand', syntax: 'proto2', fields: [{ rule: 'optional', type: 'string', name: 'cid', id: 1 }, { rule: 'optional', type: 'int32', name: 'l', id: 2 }, { rule: 'optional', type: 'int32', name: 'limit', id: 3 }, { rule: 'optional', type: 'int64', name: 't', id: 4 }, { rule: 'optional', type: 'int64', name: 'tt', id: 5 }, { rule: 'optional', type: 'string', name: 'tmid', id: 6 }, { rule: 'optional', type: 'string', name: 'mid', id: 7 }, { rule: 'optional', type: 'string', name: 'checksum', id: 8 }, { rule: 'optional', type: 'bool', name: 'stored', id: 9 }, { rule: 'optional', type: 'QueryDirection', name: 'direction', id: 10, options: { default: 'OLD' } }, { rule: 'optional', type: 'bool', name: 'tIncluded', id: 11 }, { rule: 'optional', type: 'bool', name: 'ttIncluded', id: 12 }, { rule: 'optional', type: 'int32', name: 'lctype', id: 13 }, { rule: 'repeated', type: 'LogItem', name: 'logs', id: 105 }], enums: [{ name: 'QueryDirection', syntax: 'proto2', values: [{ name: 'OLD', id: 1 }, { name: 'NEW', id: 2 }] }] }, { name: 'RcpCommand', syntax: 'proto2', fields: [{ rule: 'optional', type: 'string', name: 'id', id: 1 }, { rule: 'optional', type: 'string', name: 'cid', id: 2 }, { rule: 'optional', type: 'int64', name: 't', id: 3 }, { rule: 'optional', type: 'bool', name: 'read', id: 4 }, { rule: 'optional', type: 'string', name: 'from', id: 5 }] }, { name: 'ReadTuple', syntax: 'proto2', fields: [{ rule: 'required', type: 'string', name: 'cid', id: 1 }, { rule: 'optional', type: 'int64', name: 'timestamp', id: 2 }, { rule: 'optional', type: 'string', name: 'mid', id: 3 }] }, { name: 'MaxReadTuple', syntax: 'proto2', fields: [{ rule: 'optional', type: 'string', name: 'pid', id: 1 }, { rule: 'optional', type: 'int64', name: 'maxAckTimestamp', id: 2 }, { rule: 'optional', type: 'int64', name: 'maxReadTimestamp', id: 3 }] }, { name: 'ReadCommand', syntax: 'proto2', fields: [{ rule: 'optional', type: 'string', name: 'cid', id: 1 }, { rule: 'repeated', type: 'string', name: 'cids', id: 2 }, { rule: 'repeated', type: 'ReadTuple', name: 'convs', id: 3 }] }, { name: 'PresenceCommand', syntax: 'proto2', fields: [{ rule: 'optional', type: 'StatusType', name: 'status', id: 1 }, { rule: 'repeated', type: 'string', name: 'sessionPeerIds', id: 2 }, { rule: 'optional', type: 'string', name: 'cid', id: 3 }] }, { name: 'ReportCommand', syntax: 'proto2', fields: [{ rule: 'optional', type: 'bool', name: 'initiative', id: 1 }, { rule: 'optional', type: 'string', name: 'type', id: 2 }, { rule: 'optional', type: 'string', name: 'data', id: 3 }] }, { name: 'PatchItem', syntax: 'proto2', fields: [{ rule: 'optional', type: 'string', name: 'cid', id: 1 }, { rule: 'optional', type: 'string', name: 'mid', id: 2 }, { rule: 'optional', type: 'int64', name: 'timestamp', id: 3 }, { rule: 'optional', type: 'bool', name: 'recall', id: 4 }, { rule: 'optional', type: 'string', name: 'data', id: 5 }, { rule: 'optional', type: 'int64', name: 'patchTimestamp', id: 6 }, { rule: 'optional', type: 'string', name: 'from', id: 7 }, { rule: 'optional', type: 'bytes', name: 'binaryMsg', id: 8 }, { rule: 'optional', type: 'bool', name: 'mentionAll', id: 9 }, { rule: 'repeated', type: 'string', name: 'mentionPids', id: 10 }, { rule: 'optional', type: 'int64', name: 'patchCode', id: 11 }, { rule: 'optional', type: 'string', name: 'patchReason', id: 12 }] }, { name: 'PatchCommand', syntax: 'proto2', fields: [{ rule: 'repeated', type: 'PatchItem', name: 'patches', id: 1 }, { rule: 'optional', type: 'int64', name: 'lastPatchTime', id: 2 }] }, { name: 'PubsubCommand', syntax: 'proto2', fields: [{ rule: 'optional', type: 'string', name: 'cid', id: 1 }, { rule: 'repeated', type: 'string', name: 'cids', id: 2 }, { rule: 'optional', type: 'string', name: 'topic', id: 3 }, { rule: 'optional', type: 'string', name: 'subtopic', id: 4 }, { rule: 'repeated', type: 'string', name: 'topics', id: 5 }, { rule: 'repeated', type: 'string', name: 'subtopics', id: 6 }, { rule: 'optional', type: 'JsonObjectMessage', name: 'results', id: 7 }] }, { name: 'BlacklistCommand', syntax: 'proto2', fields: [{ rule: 'optional', type: 'string', name: 'srcCid', id: 1 }, { rule: 'repeated', type: 'string', name: 'toPids', id: 2 }, { rule: 'optional', type: 'string', name: 'srcPid', id: 3 }, { rule: 'repeated', type: 'string', name: 'toCids', id: 4 }, { rule: 'optional', type: 'int32', name: 'limit', id: 5 }, { rule: 'optional', type: 'string', name: 'next', id: 6 }, { rule: 'repeated', type: 'string', name: 'blockedPids', id: 8 }, { rule: 'repeated', type: 'string', name: 'blockedCids', id: 9 }, { rule: 'repeated', type: 'string', name: 'allowedPids', id: 10 }, { rule: 'repeated', type: 'ErrorCommand', name: 'failedPids', id: 11 }, { rule: 'optional', type: 'int64', name: 't', id: 12 }, { rule: 'optional', type: 'string', name: 'n', id: 13 }, { rule: 'optional', type: 'string', name: 's', id: 14 }] }, { name: 'GenericCommand', syntax: 'proto2', fields: [{ rule: 'optional', type: 'CommandType', name: 'cmd', id: 1 }, { rule: 'optional', type: 'OpType', name: 'op', id: 2 }, { rule: 'optional', type: 'string', name: 'appId', id: 3 }, { rule: 'optional', type: 'string', name: 'peerId', id: 4 }, { rule: 'optional', type: 'int32', name: 'i', id: 5 }, { rule: 'optional', type: 'string', name: 'installationId', id: 6 }, { rule: 'optional', type: 'int32', name: 'priority', id: 7 }, { rule: 'optional', type: 'int32', name: 'service', id: 8 }, { rule: 'optional', type: 'int64', name: 'serverTs', id: 9 }, { rule: 'optional', type: 'int64', name: 'clientTs', id: 10 }, { rule: 'optional', type: 'int32', name: 'notificationType', id: 11 }, { rule: 'optional', type: 'DataCommand', name: 'dataMessage', id: 101 }, { rule: 'optional', type: 'SessionCommand', name: 'sessionMessage', id: 102 }, { rule: 'optional', type: 'ErrorCommand', name: 'errorMessage', id: 103 }, { rule: 'optional', type: 'DirectCommand', name: 'directMessage', id: 104 }, { rule: 'optional', type: 'AckCommand', name: 'ackMessage', id: 105 }, { rule: 'optional', type: 'UnreadCommand', name: 'unreadMessage', id: 106 }, { rule: 'optional', type: 'ReadCommand', name: 'readMessage', id: 107 }, { rule: 'optional', type: 'RcpCommand', name: 'rcpMessage', id: 108 }, { rule: 'optional', type: 'LogsCommand', name: 'logsMessage', id: 109 }, { rule: 'optional', type: 'ConvCommand', name: 'convMessage', id: 110 }, { rule: 'optional', type: 'RoomCommand', name: 'roomMessage', id: 111 }, { rule: 'optional', type: 'PresenceCommand', name: 'presenceMessage', id: 112 }, { rule: 'optional', type: 'ReportCommand', name: 'reportMessage', id: 113 }, { rule: 'optional', type: 'PatchCommand', name: 'patchMessage', id: 114 }, { rule: 'optional', type: 'PubsubCommand', name: 'pubsubMessage', id: 115 }, { rule: 'optional', type: 'BlacklistCommand', name: 'blacklistMessage', id: 116 }] }], enums: [{ name: 'CommandType', syntax: 'proto2', values: [{ name: 'session', id: 0 }, { name: 'conv', id: 1 }, { name: 'direct', id: 2 }, { name: 'ack', id: 3 }, { name: 'rcp', id: 4 }, { name: 'unread', id: 5 }, { name: 'logs', id: 6 }, { name: 'error', id: 7 }, { name: 'login', id: 8 }, { name: 'data', id: 9 }, { name: 'room', id: 10 }, { name: 'read', id: 11 }, { name: 'presence', id: 12 }, { name: 'report', id: 13 }, { name: 'echo', id: 14 }, { name: 'loggedin', id: 15 }, { name: 'logout', id: 16 }, { name: 'loggedout', id: 17 }, { name: 'patch', id: 18 }, { name: 'pubsub', id: 19 }, { name: 'blacklist', id: 20 }, { name: 'goaway', id: 21 }] }, { name: 'OpType', syntax: 'proto2', values: [{ name: 'open', id: 1 }, { name: 'add', id: 2 }, { name: 'remove', id: 3 }, { name: 'close', id: 4 }, { name: 'opened', id: 5 }, { name: 'closed', id: 6 }, { name: 'query', id: 7 }, { name: 'query_result', id: 8 }, { name: 'conflict', id: 9 }, { name: 'added', id: 10 }, { name: 'removed', id: 11 }, { name: 'refresh', id: 12 }, { name: 'refreshed', id: 13 }, { name: 'start', id: 30 }, { name: 'started', id: 31 }, { name: 'joined', id: 32 }, { name: 'members_joined', id: 33 }, { name: 'left', id: 39 }, { name: 'members_left', id: 40 }, { name: 'results', id: 42 }, { name: 'count', id: 43 }, { name: 'result', id: 44 }, { name: 'update', id: 45 }, { name: 'updated', id: 46 }, { name: 'mute', id: 47 }, { name: 'unmute', id: 48 }, { name: 'status', id: 49 }, { name: 'members', id: 50 }, { name: 'max_read', id: 51 }, { name: 'is_member', id: 52 }, { name: 'member_info_update', id: 53 }, { name: 'member_info_updated', id: 54 }, { name: 'member_info_changed', id: 55 }, { name: 'join', id: 80 }, { name: 'invite', id: 81 }, { name: 'leave', id: 82 }, { name: 'kick', id: 83 }, { name: 'reject', id: 84 }, { name: 'invited', id: 85 }, { name: 'kicked', id: 86 }, { name: 'upload', id: 100 }, { name: 'uploaded', id: 101 }, { name: 'subscribe', id: 120 }, { name: 'subscribed', id: 121 }, { name: 'unsubscribe', id: 122 }, { name: 'unsubscribed', id: 123 }, { name: 'is_subscribed', id: 124 }, { name: 'modify', id: 150 }, { name: 'modified', id: 151 }, { name: 'block', id: 170 }, { name: 'unblock', id: 171 }, { name: 'blocked', id: 172 }, { name: 'unblocked', id: 173 }, { name: 'members_blocked', id: 174 }, { name: 'members_unblocked', id: 175 }, { name: 'check_block', id: 176 }, { name: 'check_result', id: 177 }, { name: 'add_shutup', id: 180 }, { name: 'remove_shutup', id: 181 }, { name: 'query_shutup', id: 182 }, { name: 'shutup_added', id: 183 }, { name: 'shutup_removed', id: 184 }, { name: 'shutup_result', id: 185 }, { name: 'shutuped', id: 186 }, { name: 'unshutuped', id: 187 }, { name: 'members_shutuped', id: 188 }, { name: 'members_unshutuped', id: 189 }, { name: 'check_shutup', id: 190 }] }, { name: 'StatusType', syntax: 'proto2', values: [{ name: 'on', id: 1 }, { name: 'off', id: 2 }] }], isNamespace: true }).build(); const { JsonObjectMessage, UnreadTuple, LogItem, DataCommand, SessionCommand, ErrorCommand, DirectCommand, AckCommand, UnreadCommand, ConvCommand, RoomCommand, LogsCommand, RcpCommand, ReadTuple, MaxReadTuple, ReadCommand, PresenceCommand, ReportCommand, GenericCommand, BlacklistCommand, PatchCommand, PatchItem, ConvMemberInfo, CommandType, OpType, StatusType } = messageCompiled.push_server.messages2; var message = /*#__PURE__*/Object.freeze({ __proto__: null, JsonObjectMessage: JsonObjectMessage, UnreadTuple: UnreadTuple, LogItem: LogItem, DataCommand: DataCommand, SessionCommand: SessionCommand, ErrorCommand: ErrorCommand, DirectCommand: DirectCommand, AckCommand: AckCommand, UnreadCommand: UnreadCommand, ConvCommand: ConvCommand, RoomCommand: RoomCommand, LogsCommand: LogsCommand, RcpCommand: RcpCommand, ReadTuple: ReadTuple, MaxReadTuple: MaxReadTuple, ReadCommand: ReadCommand, PresenceCommand: PresenceCommand, ReportCommand: ReportCommand, GenericCommand: GenericCommand, BlacklistCommand: BlacklistCommand, PatchCommand: PatchCommand, PatchItem: PatchItem, ConvMemberInfo: ConvMemberInfo, CommandType: CommandType, OpType: OpType, StatusType: StatusType }); function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) { var desc = {}; Object.keys(descriptor).forEach(function (key) { desc[key] = descriptor[key]; }); desc.enumerable = !!desc.enumerable; desc.configurable = !!desc.configurable; if ('value' in desc || desc.initializer) { desc.writable = true; } desc = decorators.slice().reverse().reduce(function (desc, decorator) { return decorator(target, property, desc) || desc; }, desc); if (context && desc.initializer !== void 0) { desc.value = desc.initializer ? desc.initializer.call(context) : void 0; desc.initializer = undefined; } if (desc.initializer === void 0) { Object.defineProperty(target, property, desc); desc = null; } return desc; } const adapters = {}; const getAdapter = name => { const adapter = adapters[name]; if (adapter === undefined) { throw new Error(`${name} adapter is not configured`); } return adapter; }; /** * 指定 Adapters * @function * @memberof module:leancloud-realtime * @param {Adapters} newAdapters Adapters 的类型请参考 {@link https://url.leanapp.cn/adapter-type-definitions @leancloud/adapter-types} 中的定义 */ const setAdapters = newAdapters => { Object.assign(adapters, newAdapters); }; /* eslint-disable */ var global$1 = typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : {}; const EXPIRED = Symbol('expired'); const debug = d('LC:Expirable'); class Expirable { constructor(value, ttl) { this.originalValue = value; if (typeof ttl === 'number') { this.expiredAt = Date.now() + ttl; } } get value() { const expired = this.expiredAt && this.expiredAt <= Date.now(); if (expired) debug(`expired: ${this.originalValue}`); return expired ? EXPIRED : this.originalValue; } } Expirable.EXPIRED = EXPIRED; const debug$1 = d('LC:Cache'); class Cache { constructor(name = 'anonymous') { this.name = name; this._map = {}; } get(key) { const cache = this._map[key]; if (cache) { const { value } = cache; if (value !== Expirable.EXPIRED) { debug$1('[%s] hit: %s', this.name, key); return value; } delete this._map[key]; } debug$1(`[${this.name}] missed: ${key}`); return null; } set(key, value, ttl) { debug$1('[%s] set: %s %d', this.name, key, ttl); this._map[key] = new Expirable(value, ttl); } } /** * 调试日志控制器 * @const * @memberof module:leancloud-realtime * @example * debug.enable(); // 启用调试日志 * debug.disable(); // 关闭调试日志 */ const debug$2 = { enable: (namespaces = 'LC*') => d.enable(namespaces), disable: d.disable }; const tryAll = promiseConstructors => { const promise = new Promise(promiseConstructors[0]); if (promiseConstructors.length === 1) { return promise; } return promise.catch(() => tryAll(promiseConstructors.slice(1))); }; // eslint-disable-next-line no-sequences const tap = interceptor => value => (interceptor(value), value); const finalize = callback => [// eslint-disable-next-line no-sequences value => (callback(), value), error => { callback(); throw error; }]; /** * 将对象转换为 Date,支持 string、number、ProtoBuf Long 以及 LeanCloud 的 Date 类型, * 其他情况下(包括对象为 falsy)返回原值。 * @private */ const decodeDate = date => { if (!date) return date; if (typeof date === 'string' || typeof date === 'number') { return new Date(date); } if (date.__type === 'Date' && date.iso) { return new Date(date.iso); } // Long if (typeof date.toNumber === 'function') { return new Date(date.toNumber()); } return date; }; /** * 获取 Date 的毫秒数,如果不是一个 Date 返回 undefined。 * @private */ const getTime = date => date && date.getTime ? date.getTime() : undefined; /** * 解码对象中的 LeanCloud 数据结构。 * 目前仅会处理 Date 类型。 * @private */ const decode = value => { if (!value) return value; if (value.__type === 'Date' && value.iso) { return new Date(value.iso); } if (Array.isArray(value)) { return value.map(decode); } if (isPlainObject(value)) { return Object.keys(value).reduce((result, key) => ({ ...result, [key]: decode(value[key]) }), {}); } return value; }; /** * 将对象中的特殊类型编码为 LeanCloud 数据结构。 * 目前仅会处理 Date 类型。 * @private */ const encode = value => { if (value instanceof Date) return { __type: 'Date', iso: value.toJSON() }; if (Array.isArray(value)) { return value.map(encode); } if (isPlainObject(value)) { return Object.keys(value).reduce((result, key) => ({ ...result, [key]: encode(value[key]) }), {}); } return value; }; const keyRemap = (keymap, obj) => Object.keys(obj).reduce((newObj, key) => { const newKey = keymap[key] || key; return Object.assign(newObj, { [newKey]: obj[key] }); }, {}); const isIE10 = global$1.navigator && global$1.navigator.userAgent && global$1.navigator.userAgent.indexOf('MSIE 10.') !== -1; /* eslint-disable no-proto */ const getStaticProperty = (klass, property) => klass[property] || (klass.__proto__ ? getStaticProperty(klass.__proto__, property) : undefined); /* eslint-enable no-proto */ const union = (a, b) => Array.from(new Set([...a, ...b])); const difference = (a, b) => Array.from((bSet => new Set(a.filter(x => !bSet.has(x))))(new Set(b))); const map = new WeakMap(); // protected property helper const internal = object => { if (!map.has(object)) { map.set(object, {}); } return map.get(object); }; const compact = (obj, filter) => { if (!isPlainObject(obj)) return obj; const object = { ...obj }; Object.keys(object).forEach(prop => { const value = object[prop]; if (value === filter) { delete object[prop]; } else { object[prop] = compact(value, filter); } }); return object; }; // debug utility const removeNull = obj => compact(obj, null); const trim = message => removeNull(JSON.parse(JSON.stringify(message))); const ensureArray = target => { if (Array.isArray(target)) { return target; } if (target === undefined || target === null) { return []; } return [target]; }; const setValue = (target, key, value) => { // '.' is not allowed in Class keys, escaping is not in concern now. const segs = key.split('.'); const lastSeg = segs.pop(); let currentTarget = target; segs.forEach(seg => { if (currentTarget[seg] === undefined) currentTarget[seg] = {}; currentTarget = currentTarget[seg]; }); currentTarget[lastSeg] = value; return target; }; const isWeapp = // eslint-disable-next-line no-undef typeof wx === 'object' && typeof wx.connectSocket === 'function'; // throttle decorator const throttle = wait => (target, property, descriptor) => { const callback = descriptor.value; // very naive, internal use only if (callback.length) { throw new Error('throttled function should not accept any arguments'); } return { ...descriptor, value() { let { throttleMeta } = internal(this); if (!throttleMeta) { throttleMeta = {}; internal(this).throttleMeta = throttleMeta; } let { [property]: propertyMeta } = throttleMeta; if (!propertyMeta) { propertyMeta = {}; throttleMeta[property] = propertyMeta; } const { previouseTimestamp = 0, timeout } = propertyMeta; const now = Date.now(); const remainingTime = wait - (now - previouseTimestamp); if (remainingTime <= 0) { throttleMeta[property].previouseTimestamp = now; callback.apply(this); } else if (!timeout) { propertyMeta.timeout = setTimeout(() => { propertyMeta.previouseTimestamp = Date.now(); delete propertyMeta.timeout; callback.apply(this); }, remainingTime); } } }; }; const isCNApp = appId => appId.slice(-9) !== '-MdYXbMMI'; const equalBuffer = (buffer1, buffer2) => { if (!buffer1 || !buffer2) return false; if (buffer1.byteLength !== buffer2.byteLength) return false; const a = new Uint8Array(buffer1); const b = new Uint8Array(buffer2); return !a.some((value, index) => value !== b[index]); }; var _class; const debug$3 = d('LC:WebSocketPlus'); const OPEN = 'open'; const DISCONNECT = 'disconnect'; const RECONNECT = 'reconnect'; const RETRY = 'retry'; const SCHEDULE = 'schedule'; const OFFLINE = 'offline'; const ONLINE = 'online'; const ERROR = 'error'; const MESSAGE = 'message'; const HEARTBEAT_TIME = 180000; const TIMEOUT_TIME = 380000; const DEFAULT_RETRY_STRATEGY = attempt => Math.min(1000 * 2 ** attempt, 300000); const requireConnected = (target, name, descriptor) => ({ ...descriptor, value: function requireConnectedWrapper(...args) { this.checkConnectionAvailability(name); return descriptor.value.call(this, ...args); } }); let WebSocketPlus = (_class = class WebSocketPlus extends EventEmitter { get urls() { return this._urls; } set urls(urls) { this._urls = ensureArray(urls); } constructor(getUrls, protocol) { super(); this.init(); this._protocol = protocol; Promise.resolve(typeof getUrls === 'function' ? getUrls() : getUrls).then(ensureArray).then(urls => { this._urls = urls; return this._open(); }).then(() => { this.__postponeTimeoutTimer = this._postponeTimeoutTimer.bind(this); if (global$1.addEventListener) { this.__pause = () => { if (this.can('pause')) this.pause(); }; this.__resume = () => { if (this.can('resume')) this.resume(); }; global$1.addEventListener('offline', this.__pause); global$1.addEventListener('online', this.__resume); } this.open(); }).catch(this.throw.bind(this)); } _open() { return this._createWs(this._urls, this._protocol).then(ws => { const [first, ...reset] = this._urls; this._urls = [...reset, first]; return ws; }); } _createWs(urls, protocol) { return tryAll(urls.map(url => (resolve, reject) => { debug$3(`connect [${url}] ${protocol}`); const WebSocket = getAdapter('WebSocket'); const ws = protocol ? new WebSocket(url, protocol) : new WebSocket(url); ws.binaryType = this.binaryType || 'arraybuffer'; ws.onopen = () => resolve(ws); ws.onclose = error => { if (error instanceof Error) { return reject(error); } // in browser, error event is useless return reject(new Error(`Failed to connect [${url}]`)); }; ws.onerror = ws.onclose; })).then(ws => { this._ws = ws; this._ws.onclose = this._handleClose.bind(this); this._ws.onmessage = this._handleMessage.bind(this); return ws; }); } _destroyWs() { const ws = this._ws; if (!ws) return; ws.onopen = null; ws.onclose = null; ws.onerror = null; ws.onmessage = null; this._ws = null; ws.close(); } // eslint-disable-next-line class-methods-use-this onbeforeevent(event, from, to, ...payload) { debug$3(`${event}: ${from} -> ${to} %o`, payload); } onopen() { this.emit(OPEN); } onconnected() { this._startConnectionKeeper(); } onleaveconnected(event, from, to) { this._stopConnectionKeeper(); this._destroyWs(); if (to === 'offline' || to === 'disconnected') { this.emit(DISCONNECT); } } onpause() { this.emit(OFFLINE); } onbeforeresume() { this.emit(ONLINE); } onreconnect() { this.emit(RECONNECT); } ondisconnected(event, from, to, attempt = 0) { const delay = from === OFFLINE ? 0 : DEFAULT_RETRY_STRATEGY.call(null, attempt); debug$3(`schedule attempt=${attempt} delay=${delay}`); this.emit(SCHEDULE, attempt, delay); if (this.__scheduledRetry) { clearTimeout(this.__scheduledRetry); } this.__scheduledRetry = setTimeout(() => { if (this.is('disconnected')) { this.retry(attempt); } }, delay); } onretry(event, from, to, attempt = 0) { this.emit(RETRY, attempt); this._open().then(() => this.can('reconnect') && this.reconnect(), () => this.can('fail') && this.fail(attempt + 1)); } onerror(event, from, to, error) { this.emit(ERROR, error); } onclose() { if (global$1.removeEventListener) { if (this.__pause) global$1.removeEventListener('offline', this.__pause); if (this.__resume) global$1.removeEventListener('online', this.__resume); } } checkConnectionAvailability(name = 'API') { if (!this.is('connected')) { const currentState = this.current; console.warn(`${name} should not be called when the connection is ${currentState}`); if (this.is('disconnected') || this.is('reconnecting')) { console.warn('disconnect and reconnect event should be handled to avoid such calls.'); } throw new Error('Connection unavailable'); } } // jsdoc-ignore-start // jsdoc-ignore-end _ping() { debug$3('ping'); try { this.ping(); } catch (error) { console.warn(`websocket ping error: ${error.message}`); } } ping() { if (this._ws.ping) { this._ws.ping(); } else { console.warn(`The WebSocket implement does not support sending ping frame. Override ping method to use application defined ping/pong mechanism.`); } } _postponeTimeoutTimer() { debug$3('_postponeTimeoutTimer'); this._clearTimeoutTimers(); this._timeoutTimer = setTimeout(() => { debug$3('timeout'); this.disconnect(); }, TIMEOUT_TIME); } _clearTimeoutTimers() { if (this._timeoutTimer) { clearTimeout(this._timeoutTimer); } } _startConnectionKeeper() { debug$3('start connection keeper'); this._heartbeatTimer = setInterval(this._ping.bind(this), HEARTBEAT_TIME); const addListener = this._ws.addListener || this._ws.addEventListener; if (!addListener) { debug$3('connection keeper disabled due to the lack of #addEventListener.'); return; } addListener.call(this._ws, 'message', this.__postponeTimeoutTimer); addListener.call(this._ws, 'pong', this.__postponeTimeoutTimer); this._postponeTimeoutTimer(); } _stopConnectionKeeper() { debug$3('stop connection keeper'); // websockets/ws#489 const removeListener = this._ws.removeListener || this._ws.removeEventListener; if (removeListener) { removeListener.call(this._ws, 'message', this.__postponeTimeoutTimer); removeListener.call(this._ws, 'pong', this.__postponeTimeoutTimer); this._clearTimeoutTimers(); } if (this._heartbeatTimer) { clearInterval(this._heartbeatTimer); } } _handleClose(event) { debug$3(`ws closed [${event.code}] ${event.reason}`); // socket closed manually, ignore close event. if (this.isFinished()) return; this.handleClose(event); } handleClose() { // reconnect this.disconnect(); } // jsdoc-ignore-start // jsdoc-ignore-end send(data) { debug$3('send', data); this._ws.send(data); } _handleMessage(event) { debug$3('message', event.data); this.handleMessage(event.data); } handleMessage(message) { this.emit(MESSAGE, message); } }, (_applyDecoratedDescriptor(_class.prototype, "_ping", [requireConnected], Object.getOwnPropertyDescriptor(_class.prototype, "_ping"), _class.prototype), _applyDecoratedDescriptor(_class.prototype, "send", [requireConnected], Object.getOwnPropertyDescriptor(_class.prototype, "send"), _class.prototype)), _class); StateMachine.create({ target: WebSocketPlus.prototype, initial: { state: 'initialized', event: 'init', defer: true }, terminal: 'closed', events: [{ name: 'open', from: 'initialized', to: 'connected' }, { name: 'disconnect', from: 'connected', to: 'disconnected' }, { name: 'retry', from: 'disconnected', to: 'reconnecting' }, { name: 'fail', from: 'reconnecting', to: 'disconnected' }, { name: 'reconnect', from: 'reconnecting', to: 'connected' }, { name: 'pause', from: ['connected', 'disconnected', 'reconnecting'], to: 'offline' }, {}, { name: 'resume', from: 'offline', to: 'disconnected' }, { name: 'close', from: ['connected', 'disconnected', 'reconnecting', 'offline'], to: 'closed' }, { name: 'throw', from: '*', to: 'error' }] }); const error = Object.freeze({ 1000: { name: 'CLOSE_NORMAL' }, 1006: { name: 'CLOSE_ABNORMAL' }, 4100: { name: 'APP_NOT_AVAILABLE', message: 'App not exists or realtime message service is disabled.' }, 4102: { name: 'SIGNATURE_FAILED', message: 'Login signature mismatch.' }, 4103: { name: 'INVALID_LOGIN', message: 'Malformed clientId.' }, 4105: { name: 'SESSION_REQUIRED', message: 'Message sent before session opened.' }, 4107: { name: 'READ_TIMEOUT' }, 4108: { name: 'LOGIN_TIMEOUT' }, 4109: { name: 'FRAME_TOO_LONG' }, 4110: { name: 'INVALID_ORIGIN', message: 'Access denied by domain whitelist.' }, 4111: { name: 'SESSION_CONFLICT' }, 4112: { name: 'SESSION_TOKEN_EXPIRED' }, 4113: { name: 'APP_QUOTA_EXCEEDED', message: 'The daily active users limit exceeded.' }, 4116: { name: 'MESSAGE_SENT_QUOTA_EXCEEDED', message: 'Command sent too fast.' }, 4200: { name: 'INTERNAL_ERROR', message: 'Internal error, please contact LeanCloud for support.' }, 4301: { name: 'CONVERSATION_API_FAILED', message: 'Upstream Conversatoin API failed, see error.detail for details.' }, 4302: { name: 'CONVERSATION_SIGNATURE_FAILED', message: 'Conversation action signature mismatch.' }, 4303: { name: 'CONVERSATION_NOT_FOUND' }, 4304: { name: 'CONVERSATION_FULL' }, 4305: { name: 'CONVERSATION_REJECTED_BY_APP', message: 'Conversation action rejected by hook.' }, 4306: { name: 'CONVERSATION_UPDATE_FAILED' }, 4307: { name: 'CONVERSATION_READ_ONLY' }, 4308: { name: 'CONVERSATION_NOT_ALLOWED' }, 4309: { name: 'CONVERSATION_UPDATE_REJECTED', message: 'Conversation update rejected because the client is not a member.' }, 4310: { name: 'CONVERSATION_QUERY_FAILED', message: 'Conversation query failed because it is too expansive.' }, 4311: { name: 'CONVERSATION_LOG_FAILED' }, 4312: { name: 'CONVERSATION_LOG_REJECTED', message: 'Message query rejected because the client is not a member of the conversation.' }, 4313: { name: 'SYSTEM_CONVERSATION_REQUIRED' }, 4314: { name: 'NORMAL_CONVERSATION_REQUIRED' }, 4315: { name: 'CONVERSATION_BLACKLISTED', message: 'Blacklisted in the conversation.' }, 4316: { name: 'TRANSIENT_CONVERSATION_REQUIRED' }, 4317: { name: 'CONVERSATION_MEMBERSHIP_REQUIRED' }, 4318: { name: 'CONVERSATION_API_QUOTA_EXCEEDED', message: 'LeanCloud API quota exceeded. You may upgrade your plan.' }, 4323: { name: 'TEMPORARY_CONVERSATION_EXPIRED', message: 'Temporary conversation expired or does not exist.' }, 4401: { name: 'INVALID_MESSAGING_TARGET', message: 'Conversation does not exist or client is not a member.' }, 4402: { name: 'MESSAGE_REJECTED_BY_APP', message: 'Message rejected by hook.' }, 4403: { name: 'MESSAGE_OWNERSHIP_REQUIRED' }, 4404: { name: 'MESSAGE_NOT_FOUND' }, 4405: { name: 'MESSAGE_UPDATE_REJECTED_BY_APP', message: 'Message update rejected by hook.' }, 4406: { name: 'MESSAGE_EDIT_DISABLED' }, 4407: { name: 'MESSAGE_RECALL_DISABLED' }, 5130: { name: 'OWNER_PROMOTION_NOT_ALLOWED', message: "Updating a member's role to owner is not allowed." } }); const ErrorCode = Object.freeze(Object.keys(error).reduce((result, code) => Object.assign(result, { [error[code].name]: Number(code) }), {})); const createError = ({ code, reason, appCode, detail, error: errorMessage }) => { let message = reason || detail || errorMessage; let name = reason; if (!message && error[code]) { ({ name } = error[code]); message = error[code].message || name; } if (!message) { message = `Unknow Error: ${code}`; } const err = new Error(message); return Object.assign(err, { code, appCode, detail, name }); }; const debug$4 = d('LC:Connection'); const COMMAND_TIMEOUT = 20000; const EXPIRE = Symbol('expire'); const isIdempotentCommand = command => !(command.cmd === CommandType.direct || command.cmd === CommandType.session && command.op === OpType.open || command.cmd === CommandType.conv && (command.op === OpType.start || command.op === OpType.update || command.op === OpType.members)); class Connection extends WebSocketPlus { constructor(getUrl, { format, version }) { debug$4('initializing Connection'); const protocolString = `lc.${format}.${version}`; super(getUrl, protocolString); this._protocolFormat = format; this._commands = {}; this._serialId = 0; } async send(command, waitingForRespond = true) { let buffer; let serialId; if (waitingForRespond) { if (isIdempotentCommand(command)) { buffer = command.toArrayBuffer(); const duplicatedCommand = values(this._commands).find(({ buffer: targetBuffer, command: targetCommand }) => targetCommand.cmd === command.cmd && targetCommand.op === command.op && equalBuffer(targetBuffer, buffer)); if (duplicatedCommand) { console.warn(`Duplicated command [cmd:${command.cmd} op:${command.op}] is throttled.`); return duplicatedCommand.promise; } } this._serialId += 1; serialId = this._serialId; command.i = serialId; // eslint-disable-line no-param-reassign } if (debug$4.enabled) debug$4('↑ %O sent', trim(command)); let message; if (this._protocolFormat === 'proto2base64') { message = command.toBase64(); } else if (command.toArrayBuffer) { message = command.toArrayBuffer(); } if (!message) { throw new TypeError(`${command} is not a GenericCommand`); } super.send(message); if (!waitingForRespond) return undefined; const promise = new Promise((resolve, reject) => { this._commands[serialId] = { command, buffer, resolve, reject, timeout: setTimeout(() => { if (this._commands[serialId]) { if (debug$4.enabled) debug$4('✗ %O timeout', trim(command)); reject(createError({ error: `Command Timeout [cmd:${command.cmd} op:${command.op}]`, name: 'COMMAND_TIMEOUT' })); delete this._commands[serialId]; } }, COMMAND_TIMEOUT) }; }); this._commands[serialId].promise = promise; return promise; } handleMessage(msg) { let message; try { message = GenericCommand.decode(msg); if (debug$4.enabled) debug$4('↓ %O received', trim(message)); } catch (e) { console.warn('Decode message failed:', e.message, msg); return; } const serialId = message.i; if (serialId) { if (this._commands[serialId]) { clearTimeout(this._commands[serialId].timeout); if (message.cmd === CommandType.error) { this._commands[serialId].reject(createError(message.errorMessage)); } else { this._commands[serialId].resolve(message); } delete this._commands[serialId]; } else { console.warn(`Unexpected command received with serialId [${serialId}], which have timed out or never been requested.`); } } else { switch (message.cmd) { case CommandType.error: { this.emit(ERROR, createError(message.errorMessage)); return; } case CommandType.goaway: { this.emit(EXPIRE); return; } default: { this.emit(MESSAGE, message); } } } } ping() { return this.send(new GenericCommand({ cmd: CommandType.echo })).catch(error => debug$4('ping failed:', error)); } } const debug$5 = d('LC:request'); var request = (({ method = 'GET', url: _url, query, headers, data, timeout: time }) => { let url = _url; if (query) { const queryString = Object.keys(query).map(key => { const value = query[key]; if (value === undefined) return undefined; const v = isPlainObject(value) ? JSON.stringify(value) : value; return `${encodeURIComponent(key)}=${encodeURIComponent(v)}`; }).filter(qs => qs).join('&'); url = `${url}?${queryString}`; } debug$5('Req: %O %O %O', method, url, { headers, data }); const request = getAdapter('request'); const promise = request(url, { method, headers, data }).then(response => { if (response.ok === false) { const error = createError(response.data); error.response = response; throw error; } debug$5('Res: %O %O %O', url, response.status, response.data); return response.data; }).catch(error => { if (error.response) { debug$5('Error: %O %O %O', url, error.response.status, error.response.data); } throw error; }); return time ? promiseTimeout.timeout(promise, time) : promise; }); /* eslint-disable max-len */ const checkType = middleware => param => { const { constructor } = param; return Promise.resolve(param).then(middleware).then(tap(result => { if (result === undefined || result === null) { // eslint-disable-next-line max-len return console.warn(`Middleware[${middleware._pluginName || 'anonymous plugin'}:${middleware.name || 'anonymous middleware'}] param/return types not match. It returns ${result} while a ${param.constructor.name} expected.`); } if (!(result instanceof constructor)) { // eslint-disable-next-line max-len return console.warn(`Middleware[${middleware._pluginName || 'anonymous plugin'}:${middleware.name || 'anonymous middleware'}] param/return types not match. It returns a ${result.constructor.name} while a ${param.constructor.name} expected.`); } return 0; })); }; const applyDecorators = (decorators, target) => { if (decorators) { decorators.forEach(decorator => { try { decorator(target); } catch (error) { if (decorator._pluginName) { error.message += `[${decorator._pluginName}]`; } throw error; } }); } }; const applyMiddlewares = middlewares => target => ensureArray(middlewares).reduce((previousPromise, middleware) => previousPromise.then(checkType(middleware)).catch(error => { if (middleware._pluginName) { // eslint-disable-next-line no-param-reassign error.message += `[${middleware._pluginName}]`; } throw error; }), Promise.resolve(target)); const applyDispatcher = (dispatchers, payload) => ensureArray(dispatchers).reduce((resultPromise, dispatcher) => resultPromise.then(shouldDispatch => shouldDispatch === false ? false : dispatcher(...payload)).catch(error => { if (dispatcher._pluginName) { // eslint-disable-next-line no-param-reassign error.message += `[${dispatcher._pluginName}]`; } throw error; }), Promise.resolve(true)); var version = "5.0.0-rc.7"; // eslint-disable-next-line max-classes-per-file const debug$6 = d('LC:Realtime'); const routerCache = new Cache('push-router'); const initializedApp = {}; class Realtime extends EventEmitter { /** * @extends EventEmitter * @param {Object} options * @param {String} options.appId * @param {String} options.appKey (since 4.0.0) * @param {String|Object} [options.server] 指定服务器域名,中国节点应用此参数必填(since 4.0.0) * @param {Boolean} [options.noBinary=false] 设置 WebSocket 使用字符串格式收发消息(默认为二进制格式)。 * 适用于 WebSocket 实现不支持二进制数据格式的情况 * @param {Boolean} [options.ssl=true] 使用 wss 进行连接 * @param {String|String[]} [options.RTMServers] 指定私有部署的 RTM 服务器地址(since 4.0.0) * @param {Plugin[]} [options.plugins] 加载插件(since 3.1.0) */ constructor({ plugins, ...options }) { debug$6('initializing Realtime %s %O', version, options); super(); const { appId } = options; if (typeof appId !== 'string') { throw new TypeError(`appId [${appId}] is not a string`); } if (initializedApp[appId]) { throw new Error(`App [${appId}] is already initialized.`); } initializedApp[appId] = true; if (typeof options.appKey !== 'string') { throw new TypeError(`appKey [${options.appKey}] is not a string`); } if (isCNApp(appId)) { if (!options.server) { throw new TypeError(`server option is required for apps from CN region`); } } this._options = { appId: undefined, appKey: undefined, noBinary: false, ssl: true, RTMServerName: typeof process !== 'undefined' ? process.env.RTM_SERVER_NAME : undefined, // undocumented on purpose, internal use only ...options }; this._cache = new Cache('endpoints'); const _this = internal(this); _this.clients = new Set(); _this.pendingClients = new Set(); const mergedPlugins = [...ensureArray(Realtime.__preRegisteredPlugins), ...ensureArray(plugins)]; debug$6('Using plugins %o', mergedPlugins.map(plugin => plugin.name)); this._plugins = mergedPlugins.reduce((result, plugin) => { Object.keys(plugin).forEach(hook => { if ({}.hasOwnProperty.call(plugin, hook) && hook !== 'name') { if (plugin.name) { ensureArray(plugin[hook]).forEach(value => { // eslint-disable-next-line no-param-reassign value._pluginName = plugin.name; }); } // eslint-disable-next-line no-param-reassign result[hook] = ensureArray(result[hook]).concat(plugin[hook]); } }); return result; }, {}); // onRealtimeCreate hook applyDecorators(this._plugins.onRealtimeCreate, this); } async _request({ method, url: _url, version = '1.1', path, query, headers, data }) { let url = _url; if (!url) { const { appId, server } = this._options; const { api } = await this.constructor._getServerUrls({ appId, server }); url = `${api}/${version}${path}`; } return request({ url, method, query, headers: { 'X-LC-Id': this._options.appId, 'X-LC-Key': this._options.appKey, ...headers }, data }); } _open() { if (this._openPromise) return this._openPromise; let format = 'protobuf2'; if (this._options.noBinary) { // 不发送 binary data,fallback to base64 string format = 'proto2base64'; } const version = 3; const protocol = { format, version }; this._openPromise = new Promise((resolve, reject) => { debug$6('No connection established, create a new one.'); const connection = new Connection(() => this._getRTMServers(this._options), protocol); connection.on(OPEN, () => resolve(connection)).on(ERROR, error => { delete this._openPromise; reject(error); }).on(EXPIRE, async () => { debug$6('Connection expired. Refresh endpoints.'); this._cache.set('endpoints', null, 0); connection.urls = await this._getRTMServers(this._options); connection.disconnect(); }).on(MESSAGE, this._dispatchCommand.bind(this)); /** * 连接断开。 * 连接断开可能是因为 SDK 进入了离线状态(see {@link Realtime#event:OFFLINE}),或长时间没有收到服务器心跳。 * 连接断开后所有的网络操作都会失败,请在连接断开后禁用相关的 UI 元素。 * @event Realtime#DISCONNECT */ /** * 计划在一段时间后尝试重新连接 * @event Realtime#SCHEDULE * @param {Number} attempt 尝试重连的次数 * @param {Number} delay 延迟的毫秒数 */ /** * 正在尝试重新连接 * @event Realtime#RETRY * @param {Number} attempt 尝试重连的次数 */ /** * 连接恢复正常。 * 请重新启用在 {@link Realtime#event:DISCONNECT} 事件中禁用的相关 UI 元素 * @event Realtime#RECONNECT */ /** * 客户端连接断开 * @event IMClient#DISCONNECT * @see Realtime#event:DISCONNECT * @since 3.2.0 */ /** * 计划在一段时间后尝试重新连接 * @event IMClient#SCHEDULE * @param {Number} attempt 尝试重连的次数 * @param {Number} delay 延迟的毫秒数 * @since 3.2.0 */ /** * 正在尝试重新连接 * @event IMClient#RETRY * @param {Number} attempt 尝试重连的次数 * @since 3.2.0 */ /** * 客户端进入离线状态。 * 这通常意味着网络已断开,或者 {@link Realtime#pause} 被调用 * @event Realtime#OFFLINE * @since 3.4.0 */ /** * 客户端恢复在线状态 * 这通常意味着网络已恢复,或者 {@link Realtime#resume} 被调用 * @event Realtime#ONLINE * @since 3.4.0 */ /** * 进入离线状态。 * 这通常意味着网络已断开,或者 {@link Realtime#pause} 被调用 * @event IMClient#OFFLINE * @since 3.4.0 */ /** * 恢复在线状态 * 这通常意味着网络已恢复,或者 {@link Realtime#resume} 被调用 * @event IMClient#ONLINE * @since 3.4.0 */ // event proxy [DISCONNECT, RECONNECT, RETRY, SCHEDULE, OFFLINE, ONLINE].forEach(event => connection.on(event, (...payload) => { debug$6(`${event} event emitted. %o`, payload); this.emit(event, ...payload); if (event !== RECONNECT) { internal(this).clients.forEach(client => { client.emit(event, ...payload); }); } })); // override handleClose connection.handleClose = function handleClose(event) { const isFatal = [ErrorCode.APP_NOT_AVAILABLE, ErrorCode.INVALID_LOGIN, ErrorCode.INVALID_ORIGIN].some(errorCode => errorCode === event.code); if (isFatal) { // in these cases, SDK should throw. this.throw(createError(event)); } else { // reconnect this.disconnect(); } }; internal(this).connection = connection; }); return this._openPromise; } async _getRTMServers(options) { if (options.RTMServers) return shuffle(ensureArray(options.RTMServers)); let info; const cachedEndPoints = this._cache.get('endpoints'); if (cachedEndPoints) { info = cachedEndPoints; } else { info = await this.constructor._fetchRTMServers(options); const { server, secondary, ttl } = info; if (typeof server !== 'string' && typeof secondary !== 'string' && typeof ttl !== 'number') { throw new Error(`malformed RTM route response: ${JSON.stringify(info)}`); } this._cache.set('endpoints', info, info.ttl * 1000); } debug$6('endpoint info: %O', info); return [info.server, info.secondary]; } static async _getServerUrls({ appId, server }) { debug$6('fetch server urls'); if (server) { if (typeof server !== 'string') return server; return { RTMRouter: server, api: server }; } const cachedRouter = routerCache.get(appId); if (cachedRouter) return cachedRouter; const defaultProtocol = 'https://'; return request({ url: 'https://app-router.com/2/route', query: { appId }, timeout: 20000 }).then(tap(debug$6)).then(({ rtm_router_server: RTMRouterServer, api_server: APIServer, ttl = 3600 }) => { if (!RTMRouterServer) { throw new Error('rtm router not exists'); } const serverUrls = { RTMRouter: `${defaultProtocol}${RTMRouterServer}`, api: `${defaultProtocol}${APIServer}` }; routerCache.set(appId, serverUrls, ttl * 1000); return serverUrls; }).catch(() => { const id = appId.slice(0, 8).toLowerCase(); const domain = 'lncldglobal.com'; return { RTMRouter: `${defaultProtocol}${id}.rtm.${domain}`, api: `${defaultProtocol}${id}.api.${domain}` }; }); } static _fetchRTMServers({ appId, ssl, server, RTMServerName }) { debug$6('fetch endpoint info'); return this._getServerUrls({ appId, server }).then(tap(debug$6)).then(({ RTMRouter }) => request({ url: `${RTMRouter}/v1/route`, query: { appId, secure: ssl, features: isWeapp ? 'wechat' : undefined, server: RTMServerName, _t: Date.now() }, timeout: 20000 }).then(tap(debug$6))); } _close() { if (this._openPromise) { this._openPromise.then(connection => connection.close()); } delete this._openPromise; } /** * 手动进行重连。 * SDK 在网络出现异常时会自动按照一定的时间间隔尝试重连,调用该方法会立即尝试重连并重置重连尝试计数器。 * 只能在 `SCHEDULE` 事件之后,`RETRY` 事件之前调用,如果当前网络正常或者正在进行重连,调用该方法会抛异常。 */ retry() { const { connection } = internal(this); if (!connection) { throw new Error('no connection established'); } if (connection.cannot('retry')) { throw new Error(`retrying not allowed when not disconnected. the connection is now ${connection.current}`); } return connection.retry(); } /** * 暂停,使 SDK 进入离线状态。 * 你可以在网络断开、应用进入后台等时刻调用该方法让 SDK 进入离线状态,离线状态下不会尝试重连。 * 在浏览器中 SDK 会自动监听网络变化,因此无需手动调用该方法。 * * @since 3.4.0 * @see Realtime#event:OFFLINE */ pause() { // 这个方法常常在网络断开、进入后台时被调用,此时 connection 可能没有建立或者已经 close。 // 因此不像 retry,这个方法应该尽可能 loose const { connection } = internal(this); if (!connection) return; if (connection.can('pause')) connection.pause(); } /** * 恢复在线状态。 * 你可以在网络恢复、应用回到前台等时刻调用该方法让 SDK 恢复在线状态,恢复在线状态后 SDK 会开始尝试重连。 * * @since 3.4.0 * @see Realtime#event:ONLINE */ resume() { // 与 pause 一样,这个方法应该尽可能 loose const { connection } = internal(this); if (!connection) return; if (connection.can('resume')) connection.resume(); } _registerPending(value) { internal(this).pendingClients.add(value); } _deregisterPending(client) { internal(this).pendingClients.delete(client); } _register(client) { internal(this).clients.add(client); } _deregister(client) { const _this = internal(this); _this.clients.delete(client); if (_this.clients.size + _this.pendingClients.size === 0) { this._close(); } } _dispatchCommand(command) { return applyDispatcher(this._plugins.beforeCommandDispatch, [command, this]).then(shouldDispatch => { // no plugin handled this command if (shouldDispatch) return debug$6('[WARN] Unexpected message received: %O', trim(command)); return false; }); } } // For test purpose only const polyfilledPromise = Promise; // IMClient const UNREAD_MESSAGES_COUNT_UPDATE = 'unreadmessagescountupdate'; const CLOSE = 'close'; const CONFLICT = 'conflict'; const CONVERSATION_INFO_UPDATED = 'conversationinfoupdated'; const UNHANDLED_MESSAGE = 'unhandledmessage'; // shared const INVITED = 'invited'; const KICKED = 'kicked'; const MEMBERS_JOINED = 'membersjoined'; const MEMBERS_LEFT = 'membersleft'; const MEMBER_INFO_UPDATED = 'memberinfoupdated'; const BLOCKED = 'blocked'; const UNBLOCKED = 'unblocked'; const MEMBERS_BLOCKED = 'membersblocked'; const MEMBERS_UNBLOCKED = 'membersunblocked'; const MUTED = 'muted'; const UNMUTED = 'unmuted'; const MEMBERS_MUTED = 'membersmuted'; const MEMBERS_UNMUTED = 'membersunmuted'; const MESSAGE$1 = 'message'; const MESSAGE_RECALL = 'messagerecall'; const MESSAGE_UPDATE = 'messageupdate'; // Conversation const LAST_DELIVERED_AT_UPDATE = 'lastdeliveredatupdate'; const LAST_READ_AT_UPDATE = 'lastreadatupdate'; const INFO_UPDATED = 'infoupdated'; var IMEvent = /*#__PURE__*/Object.freeze({ __proto__: null, UNREAD_MESSAGES_COUNT_UPDATE: UNREAD_MESSAGES_COUNT_UPDATE, CLOSE: CLOSE, CONFLICT: CONFLICT, CONVERSATION_INFO_UPDATED: CONVERSATION_INFO_UPDATED, UNHANDLED_MESSAGE: UNHANDLED_MESSAGE, INVITED: INVITED, KICKED: KICKED, MEMBERS_JOINED: MEMBERS_JOINED, MEMBERS_LEFT: MEMBERS_LEFT, MEMBER_INFO_UPDATED: MEMBER_INFO_UPDATED, BLOCKED: BLOCKED, UNBLOCKED: UNBLOCKED, MEMBERS_BLOCKED: MEMBERS_BLOCKED, MEMBERS_UNBLOCKED: MEMBERS_UNBLOCKED, MUTED: MUTED, UNMUTED: UNMUTED, MEMBERS_MUTED: MEMBERS_MUTED, MEMBERS_UNMUTED: MEMBERS_UNMUTED, MESSAGE: MESSAGE$1, MESSAGE_RECALL: MESSAGE_RECALL, MESSAGE_UPDATE: MESSAGE_UPDATE, LAST_DELIVERED_AT_UPDATE: LAST_DELIVERED_AT_UPDATE, LAST_READ_AT_UPDATE: LAST_READ_AT_UPDATE, INFO_UPDATED: INFO_UPDATED }); /** * 消息状态枚举 * @enum {Symbol} * @since 3.2.0 * @memberof module:leancloud-realtime */ const MessageStatus = { /** 初始状态、未知状态 */ NONE: Symbol('none'), /** 正在发送 */ SENDING: Symbol('sending'), /** 已发送 */ SENT: Symbol('sent'), /** 已送达 */ DELIVERED: Symbol('delivered'), /** 发送失败 */ FAILED: Symbol('failed') }; Object.freeze(MessageStatus); const rMessageStatus = { [MessageStatus.NONE]: true, [MessageStatus.SENDING]: true, [MessageStatus.SENT]: true, [MessageStatus.DELIVERED]: true, [MessageStatus.READ]: true, [MessageStatus.FAILED]: true }; class Message { /** * @implements AVMessage * @param {Object|String|ArrayBuffer} content 消息内容 */ constructor(content) { Object.assign(this, { content }, { /** * @type {String} * @memberof Message# */ id: uuid(), /** * 消息所在的 conversation id * @memberof Message# * @type {String?} */ cid: null, /** * 消息发送时间 * @memberof Message# * @type {Date} */ timestamp: new Date(), /** * 消息发送者 * @memberof Message# * @type {String} */ from: undefined, /** * 消息提及的用户 * @since 4.0.0 * @memberof Message# * @type {String[]} */ mentionList: [], /** * 消息是否提及了所有人 * @since 4.0.0 * @memberof Message# * @type {Boolean} */ mentionedAll: false, _mentioned: false }); this._setStatus(MessageStatus.NONE); } /** * 将当前消息的内容序列化为 JSON 对象 * @private * @return {Object} */ getPayload() { return this.content; } _toJSON() { const { id, cid, from, timestamp, deliveredAt, updatedAt, mentionList, mentionedAll, mentioned } = this; return { id, cid, from, timestamp, deliveredAt, updatedAt, mentionList, mentionedAll, mentioned }; } /** * 返回 JSON 格式的消息 * @return {Object} 返回值是一个 plain Object */ toJSON() { return { ...this._toJSON(), data: this.content }; } /** * 返回 JSON 格式的消息,与 toJSON 不同的是,该对象包含了完整的信息,可以通过 {@link IMClient#parseMessage} 反序列化。 * @return {Object} 返回值是一个 plain Object * @since 4.0.0 */ toFullJSON() { const { content, id, cid, from, timestamp, deliveredAt, _updatedAt, mentionList, mentionedAll } = this; return { data: content, id, cid, from, timestamp: getTime(timestamp), deliveredAt: getTime(deliveredAt), updatedAt: getTime(_updatedAt), mentionList, mentionedAll }; } /** * 消息状态,值为 {@link module:leancloud-realtime.MessageStatus} 之一 * @type {Symbol} * @readonly * @since 3.2.0 */ get status() { return this._status; } _setStatus(status) { if (!rMessageStatus[status]) { throw new Error('Invalid message status'); } this._status = status; } get timestamp() { return this._timestamp; } set timestamp(value) { this._timestamp = decodeDate(value); } /** * 消息送达时间 * @type {?Date} */ get deliveredAt() { return this._deliveredAt; } set deliveredAt(value) { this._deliveredAt = decodeDate(value); } /** * 消息修改或撤回时间,可以通过比较其与消息的 timestamp 是否相等判断消息是否被修改过或撤回过。 * @type {Date} * @since 3.5.0 */ get updatedAt() { return this._updatedAt || this.timestamp; } set updatedAt(value) { this._updatedAt = decodeDate(value); } /** * 当前用户是否在该消息中被提及 * @type {Boolean} * @readonly * @since 4.0.0 */ get mentioned() { return this._mentioned; } _updateMentioned(client) { this._mentioned = this.from !== client && (this.mentionedAll || this.mentionList.indexOf(client) > -1); } /** * 获取提及用户列表 * @since 4.0.0 * @return {String[]} 提及用户的 id 列表 */ getMentionList() { return this.mentionList; } /** * 设置提及用户列表 * @since 4.0.0 * @param {String[]} clients 提及用户的 id 列表 * @return {this} self */ setMentionList(clients) { this.mentionList = ensureArray(clients); return this; } /** * 设置是否提及所有人 * @since 4.0.0 * @param {Boolean} [value=true] * @return {this} self */ mentionAll(value = true) { this.mentionedAll = Boolean(value); return this; } /** * 判断给定的内容是否是有效的 Message, * 该方法始终返回 true * @private * @returns {Boolean} * @implements AVMessage.validate */ static validate() { return true; } /** * 解析处理消息内容 *
* 如果子类提供了 message,返回该 message * 如果没有提供,将 json 作为 content 实例化一个 Message * @private * @param {Object} json json 格式的消息内容 * @param {Message} message 子类提供的 message * @return {Message} * @implements AVMessage.parse */ static parse(json, message) { return message || new this(json); } } /* eslint-disable no-param-reassign */ const messageType = type => { if (typeof type !== 'number') { throw new TypeError(`${type} is not a Number`); } return target => { target.TYPE = type; target.validate = json => json._lctype === type; target.prototype._getType = () => ({ _lctype: type }); }; }; // documented in ../plugin-im.js const messageField = fields => { if (typeof fields !== 'string') { if (!Array.isArray(fields)) { throw new TypeError(`${fields} is not an Array`); } else if (fields.some(value => typeof value !== 'string')) { throw new TypeError('fields contains non-string typed member'); } } return target => { // IE10 Hack: // static properties in IE10 will not be inherited from super // search for parse method and assign it manually let originalCustomFields = isIE10 ? getStaticProperty(target, '_customFields') : target._customFields; originalCustomFields = Array.isArray(originalCustomFields) ? originalCustomFields : []; target._customFields = originalCustomFields.concat(fields); }; }; // IE10 Hack: // static properties in IE10 will not be inherited from super // search for parse method and assign it manually const IE10Compatible = target => { if (isIE10) { target.parse = getStaticProperty(target, 'parse'); } }; var _dec, _class$1; let // jsdoc-ignore-end /** * 所有内置的富媒体消息均继承自本类 * @extends Message */ TypedMessage = (_dec = messageField(['_lctext', '_lcattrs']), _dec(_class$1 = class TypedMessage extends Message { /** * @type {Number} * @readonly */ get type() { return this.constructor.TYPE; } /** @type {String} */ set text(text) { return this.setText(text); } get text() { return this.getText(); } /** @type {Object} */ set attributes(attributes) { return this.setAttributes(attributes); } get attributes() { return this.getAttributes(); } /** * 在客户端需要以文本形式展示该消息时显示的文案, * 如[红包] 新春快乐
。 * 默认值为消息的 text。 * @type {String} * @readonly */ get summary() { return this.text; } /** * @param {String} text * @return {this} self */ setText(text) { this._lctext = text; return this; } /** * @return {String} */ getText() { return this._lctext; } /** * @param {Object} attributes * @return {this} self */ setAttributes(attributes) { this._lcattrs = attributes; return this; } /** * @return {Object} */ getAttributes() { return this._lcattrs; } _getCustomFields() { const fields = Array.isArray(this.constructor._customFields) ? this.constructor._customFields : []; return fields.reduce((result, field) => { if (typeof field !== 'string') return result; result[field] = this[field]; // eslint-disable-line no-param-reassign return result; }, {}); } /* eslint-disable class-methods-use-this */ _getType() { throw new Error('not implemented'); } /* eslint-enable class-methods-use-this */ getPayload() { return compact({ _lctext: this.getText(), _lcattrs: this.getAttributes(), ...this._getCustomFields(), ...this._getType() }); } toJSON() { const { type, text, attributes, summary } = this; return { ...super._toJSON(), type, text, attributes, summary }; } toFullJSON() { return { ...super.toFullJSON(), data: this.getPayload() }; } /** * 解析处理消息内容 ** 为给定的 message 设置 text 与 attributes 属性,返回该 message * 如果子类没有提供 message,new this() * @protected * @param {Object} json json 格式的消息内容 * @param {TypedMessage} message 子类提供的 message * @return {TypedMessage} * @implements AVMessage.parse */ static parse(json, message = new this()) { message.content = json; // eslint-disable-line no-param-reassign const customFields = isIE10 ? getStaticProperty(message.constructor, '_customFields') : message.constructor._customFields; let fields = Array.isArray(customFields) ? customFields : []; fields = fields.reduce((result, field) => { if (typeof field !== 'string') return result; result[field] = json[field]; // eslint-disable-line no-param-reassign return result; }, {}); Object.assign(message, fields); return super.parse(json, message); } }) || _class$1); var _dec$1, _class$2; let // jsdoc-ignore-end /** * 已撤回类型消息,当消息被撤回时,SDK 会使用该类型的消息替代原始消息 * @extends TypedMessage */ RecalledMessage = (_dec$1 = messageType(-127), _dec$1(_class$2 = IE10Compatible(_class$2 = class RecalledMessage extends TypedMessage { /** * 在客户端需要以文本形式展示该消息时显示的文案,值为[该消息已撤回]
* @type {String} * @readonly */ // eslint-disable-next-line class-methods-use-this get summary() { return '[该消息已撤回]'; } }) || _class$2) || _class$2); /* eslint class-methods-use-this: ["error", { "exceptMethods": ["_addMembers", "_removeMembers"] }] */ const debug$7 = d('LC:Conversation'); const serializeMessage = message => { const content = message.getPayload(); let msg; let binaryMsg; if (content instanceof ArrayBuffer) { binaryMsg = content; } else if (typeof content !== 'string') { msg = JSON.stringify(content); } else { msg = content; } return { msg, binaryMsg }; }; const { NEW, OLD } = LogsCommand.QueryDirection; /** * 历史消息查询方向枚举 * @enum {Number} * @since 4.0.0 * @memberof module:leancloud-realtime */ const MessageQueryDirection = { /** 从后向前 */ NEW_TO_OLD: OLD, /** 从前向后 */ OLD_TO_NEW: NEW }; Object.freeze(MessageQueryDirection); class ConversationBase extends EventEmitter { /** * @extends EventEmitter * @private * @abstract */ constructor({ id, lastMessageAt, lastMessage, lastDeliveredAt, lastReadAt, unreadMessagesCount = 0, members = [], mentioned = false, ...properties }, client) { super(); Object.assign(this, { /** * 对话 id,对应 _Conversation 表中的 objectId * @memberof ConversationBase# * @type {String} */ id, /** * 最后一条消息时间 * @memberof ConversationBase# * @type {?Date} */ lastMessageAt, /** * 最后一条消息 * @memberof ConversationBase# * @type {?Message} */ lastMessage, /** * 参与该对话的用户列表 * @memberof ConversationBase# * @type {String[]} */ members, // other properties provided by subclasses ...properties }); this.members = Array.from(new Set(this.members)); Object.assign(internal(this), { messagesWaitingForReceipt: {}, lastDeliveredAt, lastReadAt, unreadMessagesCount, mentioned }); this._client = client; if (debug$7.enabled) { values(IMEvent).forEach(event => this.on(event, (...payload) => this._debug(`${event} event emitted. %o`, payload))); } // onConversationCreate hook applyDecorators(this._client._plugins.onConversationCreate, this); } /** * 当前用户是否在该对话的未读消息中被提及 * @type {Boolean} * @since 4.0.0 */ get unreadMessagesMentioned() { return internal(this).unreadMessagesMentioned; } _setUnreadMessagesMentioned(value) { internal(this).unreadMessagesMentioned = Boolean(value); } set unreadMessagesCount(value) { if (value !== this.unreadMessagesCount) { internal(this).unreadMessagesCount = value; this._client.emit(UNREAD_MESSAGES_COUNT_UPDATE, [this]); } } /** * 当前用户在该对话的未读消息数 * @type {Number} */ get unreadMessagesCount() { return internal(this).unreadMessagesCount; } set lastMessageAt(value) { const time = decodeDate(value); if (time <= this._lastMessageAt) return; this._lastMessageAt = time; } get lastMessageAt() { return this._lastMessageAt; } /** * 最后消息送达时间,常用来实现消息的「已送达」标记,可通过 {@link Conversation#fetchReceiptTimestamps} 获取或更新该属性 * @type {?Date} * @since 3.4.0 */ get lastDeliveredAt() { if (this.members.length !== 2) return null; return internal(this).lastDeliveredAt; } _setLastDeliveredAt(value) { const date = decodeDate(value); if (!(date < internal(this).lastDeliveredAt)) { internal(this).lastDeliveredAt = date; /** * 最后消息送达时间更新 * @event ConversationBase#LAST_DELIVERED_AT_UPDATE * @since 3.4.0 */ this.emit(LAST_DELIVERED_AT_UPDATE); } } /** * 最后消息被阅读时间,常用来实现发送消息的「已读」标记,可通过 {@link Conversation#fetchReceiptTimestamps} 获取或更新该属性 * @type {?Date} * @since 3.4.0 */ get lastReadAt() { if (this.members.length !== 2) return null; return internal(this).lastReadAt; } _setLastReadAt(value) { const date = decodeDate(value); if (!(date < internal(this).lastReadAt)) { internal(this).lastReadAt = date; /** * 最后消息被阅读时间更新 * @event ConversationBase#LAST_READ_AT_UPDATE * @since 3.4.0 */ this.emit(LAST_READ_AT_UPDATE); } } /** * 返回 JSON 格式的对话,与 toJSON 不同的是,该对象包含了完整的信息,可以通过 {@link IMClient#parseConversation} 反序列化。 * @return {Object} 返回值是一个 plain Object * @since 4.0.0 */ toFullJSON() { const { id, members, lastMessageAt, lastDeliveredAt, lastReadAt, lastMessage, unreadMessagesCount } = this; return { id, members, lastMessageAt: getTime(lastMessageAt), lastDeliveredAt: getTime(lastDeliveredAt), lastReadAt: getTime(lastReadAt), lastMessage: lastMessage ? lastMessage.toFullJSON() : undefined, unreadMessagesCount }; } /** * 返回 JSON 格式的对话 * @return {Object} 返回值是一个 plain Object * @since 4.0.0 */ toJSON() { const { id, members, lastMessageAt, lastDeliveredAt, lastReadAt, lastMessage, unreadMessagesCount, unreadMessagesMentioned } = this; return { id, members, lastMessageAt, lastDeliveredAt, lastReadAt, lastMessage: lastMessage ? lastMessage.toJSON() : undefined, unreadMessagesCount, unreadMessagesMentioned }; } _debug(...params) { debug$7(...params, `[${this.id}]`); } _send(command, ...args) { /* eslint-disable no-param-reassign */ if (command.cmd === null) { command.cmd = 'conv'; } if (command.cmd === 'conv' && command.convMessage === null) { command.convMessage = new ConvCommand(); } if (command.convMessage && command.convMessage.cid === null) { command.convMessage.cid = this.id; } /* eslint-enable no-param-reassign */ return this._client._send(command, ...args); } /** * 发送消息 * @param {Message} message 消息,Message 及其子类的实例 * @param {Object} [options] since v3.3.0,发送选项 * @param {Boolean} [options.transient] since v3.3.1,是否作为暂态消息发送 * @param {Boolean} [options.receipt] 是否需要回执,仅在普通对话中有效 * @param {Boolean} [options.will] since v3.4.0,是否指定该消息作为「掉线消息」发送, * 「掉线消息」会延迟到当前用户掉线后发送,常用来实现「下线通知」功能 * @param {MessagePriority} [options.priority] 消息优先级,仅在暂态对话中有效, * see: {@link module:leancloud-realtime.MessagePriority MessagePriority} * @param {Object} [options.pushData] 消息对应的离线推送内容,如果消息接收方不在线,会推送指定的内容。其结构说明参见: {@link https://url.leanapp.cn/pushData 推送消息内容} * @return {Promise.} 发送的消息 */ async send(message, options) { this._debug(message, 'send'); if (!(message instanceof Message)) { throw new TypeError(`${message} is not a Message`); } const { transient, receipt, priority, pushData, will } = { // support Message static property: sendOptions ...message.constructor.sendOptions, // support Message static property: getSendOptions ...(typeof message.constructor.getSendOptions === 'function' ? message.constructor.getSendOptions(message) : {}), ...options }; if (receipt) { if (this.transient) { console.warn('receipt option is ignored as the conversation is transient.'); } else if (transient) { console.warn('receipt option is ignored as the message is sent transiently.'); } else if (this.members.length > 2) { console.warn('receipt option is recommended to be used in one-on-one conversation.'); // eslint-disable-line max-len } } if (priority && !this.transient) { console.warn('priority option is ignored as the conversation is not transient.'); } Object.assign(message, { cid: this.id, from: this._client.id }); message._setStatus(MessageStatus.SENDING); const { msg, binaryMsg } = serializeMessage(message); const command = new GenericCommand({ cmd: 'direct', directMessage: new DirectCommand({ msg, binaryMsg, cid: this.id, r: receipt, transient, dt: message.id, pushData: JSON.stringify(pushData), will, mentionPids: message.mentionList, mentionAll: message.mentionedAll }), priority }); try { const resCommand = await this._send(command); const { ackMessage: { uid, t, code, reason, appCode } } = resCommand; if (code !== null) { throw createError({ code, reason, appCode }); } Object.assign(message, { id: uid, timestamp: t }); if (!transient) { this.lastMessage = message; this.lastMessageAt = message.timestamp; } message._setStatus(MessageStatus.SENT); if (receipt) { internal(this).messagesWaitingForReceipt[message.id] = message; } return message; } catch (error) { message._setStatus(MessageStatus.FAILED); throw error; } } async _update(message, newMessage, recall) { this._debug('patch %O %O %O', message, newMessage, recall); if (message instanceof Message) { if (message.from !== this._client.id) { throw new Error('Updating message from others is not allowed'); } if (message.status !== MessageStatus.SENT && message.status !== MessageStatus.DELIVERED) { throw new Error('Message is not sent'); } } else if (!(message.id && message.timestamp)) { throw new TypeError(`${message} is not a Message`); } let msg; let binaryMsg; if (!recall) { const content = serializeMessage(newMessage); ({ msg, binaryMsg } = content); } await this._send(new GenericCommand({ cmd: CommandType.patch, op: OpType.modify, patchMessage: new PatchCommand({ patches: [new PatchItem({ cid: this.id, mid: message.id, timestamp: Number(message.timestamp), recall, data: msg, binaryMsg, mentionPids: newMessage.mentionList, mentionAll: newMessage.mentionedAll })], lastPatchTime: this._client._lastPatchTime }) })); const { id, cid, timestamp, from, _status } = message; Object.assign(newMessage, { id, cid, timestamp, from, _status }); if (this.lastMessage && this.lastMessage.id === newMessage.id) { this.lastMessage = newMessage; } return newMessage; } /** * 获取对话人数,或暂态对话的在线人数 * @return {Promise. } */ async count() { this._debug('count'); const resCommand = await this._send(new GenericCommand({ op: 'count' })); return resCommand.convMessage.count; } /** * 应用增加成员的操作,产生副作用 * @param {string[]} members * @abstract * @private */ _addMembers() {} /** * 应用减少成员的操作,产生副作用 * @param {string[]} members * @abstract * @private */ _removeMembers() {} /** * 修改已发送的消息 * @param {AVMessage} message 要修改的消息,该消息必须是由当前用户发送的。也可以提供一个包含消息 {id, timestamp} 的对象 * @param {AVMessage} newMessage 新的消息 * @return {Promise. } 更新后的消息 */ async update(message, newMessage) { if (!(newMessage instanceof Message)) { throw new TypeError(`${newMessage} is not a Message`); } return this._update(message, newMessage, false); } /** * 撤回已发送的消息 * @param {AVMessage} message 要撤回的消息,该消息必须是由当前用户发送的。也可以提供一个包含消息 {id, timestamp} 的对象 * @return {Promise. } 一条已撤回的消息 */ async recall(message) { return this._update(message, new RecalledMessage(), true); } /** * 查询消息记录 * 如果仅需实现消息向前记录翻页查询需求,建议使用 {@link Conversation#createMessagesIterator}。 * 不论何种方向,获得的消息都是按照时间升序排列的。 * startClosed 与 endClosed 用于指定查询区间的开闭。 * * @param {Object} [options] * @param {Number} [options.limit] 限制查询结果的数量,目前服务端默认为 20 * @param {Number} [options.type] 指定查询的富媒体消息类型,不指定则查询所有消息。 * @param {MessageQueryDirection} [options.direction] 查询的方向。 * 在不指定的情况下如果 startTime 大于 endTime,则为从新到旧查询,可以实现加载聊天记录等场景。 * 如果 startTime 小于 endTime,则为从旧到新查询,可以实现弹幕等场景。 * @param {Date} [options.startTime] 从该时间开始查询,不传则从当前时间开始查询 * @param {String} [options.startMessageId] 从该消息之前开始查询,需要与 startTime 同时使用,为防止某时刻有重复消息 * @param {Boolean}[options.startClosed] 指定查询范围是否包括开始的时间点,默认不包括 * @param {Date} [options.endTime] 查询到该时间为止,不传则查询最早消息为止 * @param {String} [options.endMessageId] 查询到该消息为止,需要与 endTime 同时使用,为防止某时刻有重复消息 * @param {Boolean}[options.endClosed] 指定查询范围是否包括结束的时间点,默认不包括 * * @param {Date} [options.beforeTime] DEPRECATED: 使用 startTime 代替。限制查询结果为小于该时间之前的消息,不传则为当前时间 * @param {String} [options.beforeMessageId] DEPRECATED: 使用 startMessageId 代替。 * 限制查询结果为该消息之前的消息,需要与 beforeTime 同时使用,为防止某时刻有重复消息 * @param {Date} [options.afterTime] DEPRECATED: 使用 endTime 代替。限制查询结果为大于该时间之前的消息 * @param {String} [options.afterMessageId] DEPRECATED: 使用 endMessageId 代替。 * 限制查询结果为该消息之后的消息,需要与 afterTime 同时使用,为防止某时刻有重复消息 * @return {Promise. } 消息列表 */ async queryMessages(options = {}) { this._debug('query messages %O', options); const { beforeTime, beforeMessageId, afterTime, afterMessageId, limit, direction, type, startTime, startMessageId, startClosed, endTime, endMessageId, endClosed } = options; if (beforeMessageId || beforeTime || afterMessageId || afterTime) { console.warn('DEPRECATION: queryMessages options beforeTime, beforeMessageId, afterTime and afterMessageId are deprecated in favor of startTime, startMessageId, endTime and endMessageId.'); return this.queryMessages({ startTime: beforeTime, startMessageId: beforeMessageId, endTime: afterTime, endMessageId: afterMessageId, limit }); } if (startMessageId && !startTime) { throw new Error('query option startMessageId must be used with option startTime'); } if (endMessageId && !endTime) { throw new Error('query option endMessageId must be used with option endTime'); } const conditions = { t: startTime, mid: startMessageId, tIncluded: startClosed, tt: endTime, tmid: endMessageId, ttIncluded: endClosed, l: limit, lctype: type }; if (conditions.t instanceof Date) { conditions.t = conditions.t.getTime(); } if (conditions.tt instanceof Date) { conditions.tt = conditions.tt.getTime(); } if (direction !== undefined) { conditions.direction = direction; } else if (conditions.tt > conditions.t) { conditions.direction = MessageQueryDirection.OLD_TO_NEW; } const resCommand = await this._send(new GenericCommand({ cmd: 'logs', logsMessage: new LogsCommand(Object.assign(conditions, { cid: this.id })) })); return Promise.all(resCommand.logsMessage.logs.map(async ({ msgId, timestamp, patchTimestamp, from, ackAt, readAt, data, mentionAll, mentionPids, bin }) => { const messageData = { data, bin, id: msgId, cid: this.id, timestamp, from, deliveredAt: ackAt, updatedAt: patchTimestamp, mentionList: mentionPids, mentionedAll: mentionAll }; const message = await this._client.parseMessage(messageData); let status = MessageStatus.SENT; if (this.members.length === 2) { if (ackAt) status = MessageStatus.DELIVERED; if (ackAt) this._setLastDeliveredAt(ackAt); if (readAt) this._setLastReadAt(readAt); } message._setStatus(status); return message; })); } /** * 获取消息翻页迭代器 * @param {Object} [options] * @param {Date} [options.beforeTime] 限制起始查询结果为小于该时间之前的消息,不传则为当前时间 * @param {String} [options.beforeMessageId] 限制起始查询结果为该消息之前的消息,需要与 beforeTime 同时使用,为防止某时刻有重复消息 * @param {Number} [options.limit] 限制每页查询结果的数量,目前服务端默认为 20 * @return {AsyncIterater. >>} [AsyncIterator]{@link https://github.com/tc39/proposal-async-iteration},调用其 next 方法返回获取下一页消息的 Promise * @example * var messageIterator = conversation.createMessagesIterator({ limit: 10 }); * messageIterator.next().then(function(result) { * // result: { * // value: [message1, ..., message10], * // done: false, * // } * }); * messageIterator.next().then(function(result) { * // result: { * // value: [message11, ..., message20], * // done: false, * // } * }); * messageIterator.next().then(function(result) { * // No more messages * // result: { value: [], done: true } * }); */ createMessagesIterator({ beforeTime, beforeMessageId, limit } = {}) { let promise; return { next: () => { if (promise === undefined) { // first call promise = this.queryMessages({ limit, startTime: beforeTime, startMessageId: beforeMessageId }); } else { promise = promise.then(prevMessages => { if (prevMessages.length === 0 || prevMessages.length < limit) { // no more messages return []; } return this.queryMessages({ startTime: prevMessages[0].timestamp, startMessageId: prevMessages[0].id, limit }); }); } return promise.then(value => ({ value: Array.from(value), done: value.length === 0 || value.length < limit })); } }; } /** * 将该会话标记为已读 * @return {Promise. } self */ async read() { this.unreadMessagesCount = 0; this._setUnreadMessagesMentioned(false); // 跳过暂态会话 if (this.transient) return this; const client = this._client; if (!internal(client).readConversationsBuffer) { internal(client).readConversationsBuffer = new Set(); } internal(client).readConversationsBuffer.add(this); client._doSendRead(); return this; } _handleReceipt({ messageId, timestamp, read }) { if (read) { this._setLastReadAt(timestamp); } else { this._setLastDeliveredAt(timestamp); } const { messagesWaitingForReceipt } = internal(this); const message = messagesWaitingForReceipt[messageId]; if (!message) return; message._setStatus(MessageStatus.DELIVERED); message.deliveredAt = timestamp; delete messagesWaitingForReceipt[messageId]; } /** * 更新对话的最新回执时间戳(lastDeliveredAt、lastReadAt) * @since 3.4.0 * @return {Promise. } this */ async fetchReceiptTimestamps() { // 暂态/系统会话不支持回执 if (this.transient || this.system) return this; const { convMessage: { maxReadTimestamp, maxAckTimestamp } } = await this._send(new GenericCommand({ op: 'max_read' })); this._setLastDeliveredAt(maxAckTimestamp); this._setLastReadAt(maxReadTimestamp); return this; } _fetchAllReceiptTimestamps() { // 暂态/系统会话不支持回执 if (this.transient || this.system) return this; const convMessage = new ConvCommand({ queryAllMembers: true }); return this._send(new GenericCommand({ op: 'max_read', convMessage })).then(({ convMessage: { maxReadTuples } }) => maxReadTuples.filter(maxReadTuple => maxReadTuple.maxAckTimestamp || maxReadTuple.maxReadTimestamp).map(({ pid, maxAckTimestamp, maxReadTimestamp }) => ({ pid, lastDeliveredAt: decodeDate(maxAckTimestamp), lastReadAt: decodeDate(maxReadTimestamp) }))); } } const debug$8 = d('LC:SignatureFactoryRunner'); function _validateSignature(signatureResult = {}) { const { signature, timestamp, nonce } = signatureResult; if (typeof signature !== 'string' || typeof timestamp !== 'number' || typeof nonce !== 'string') { throw new Error('malformed signature'); } return { signature, timestamp, nonce }; } var runSignatureFactory = ((signatureFactory, params) => Promise.resolve().then(() => { debug$8('call signatureFactory with %O', params); return signatureFactory(...params); }).then(tap(signatureResult => debug$8('sign result %O', signatureResult)), error => { // eslint-disable-next-line no-param-reassign error.message = `sign error: ${error.message}`; debug$8(error); throw error; }).then(_validateSignature)); /** * 部分失败异常 * @typedef OperationFailureError * @type {Error} * @property {string} message 异常信息 * @property {string[]} clientIds 因为该原因失败的 client id 列表 * @property {number} [code] 错误码 * @property {string} [detail] 详细信息 */ /** * 部分成功的结果 * @typedef PartiallySuccess * @type {Object} * @property {string[]} successfulClientIds 成功的 client id 列表 * @property {OperationFailureError[]} failures 失败的异常列表 */ /** * 分页查询结果 * @typedef PagedResults * @type {Object} * @property {T[]} results 查询结果 * @property {string} [next] 存在表示还有更多结果,在下次查询中带上可实现翻页。 */ const createPartiallySuccess = ({ allowedPids, failedPids }) => ({ successfulClientIds: allowedPids, failures: failedPids.map(({ pids, ...error }) => Object.assign(createError(error), { clientIds: pids })) }); /** * @extends ConversationBase * @private * @abstract */ class PersistentConversation extends ConversationBase { constructor(data, { creator, createdAt, updatedAt, transient = false, system = false, muted = false, mutedMembers = [], ...attributes }, client) { super({ ...data, /** * 对话创建者 * @memberof PersistentConversation# * @type {String} */ creator, /** * 对话创建时间 * @memberof PersistentConversation# * @type {Date} */ createdAt, /** * 对话更新时间 * @memberof PersistentConversation# * @type {Date} */ updatedAt, /** * 对该对话设置了静音的用户列表 * @memberof PersistentConversation# * @type {?String[]} */ mutedMembers, /** * 暂态对话标记 * @memberof PersistentConversation# * @type {Boolean} */ transient, /** * 系统对话标记 * @memberof PersistentConversation# * @type {Boolean} * @since 3.3.0 */ system, /** * 当前用户静音该对话标记 * @memberof PersistentConversation# * @type {Boolean} */ muted, _attributes: attributes }, client); this._reset(); } set createdAt(value) { this._createdAt = decodeDate(value); } get createdAt() { return this._createdAt; } set updatedAt(value) { this._updatedAt = decodeDate(value); } get updatedAt() { return this._updatedAt; } /** * 对话名字,对应 _Conversation 表中的 name * @type {String} */ get name() { return this.get('name'); } set name(value) { this.set('name', value); } /** * 获取对话的自定义属性 * @since 3.2.0 * @param {String} key key 属性的键名,'x' 对应 Conversation 表中的 x 列 * @return {Any} 属性的值 */ get(key) { return get(internal(this).currentAttributes, key); } /** * 设置对话的自定义属性 * @since 3.2.0 * @param {String} key 属性的键名,'x' 对应 Conversation 表中的 x 列,支持使用 'x.y.z' 来修改对象的部分字段。 * @param {Any} value 属性的值 * @return {this} self * @example * * // 设置对话的 color 属性 * conversation.set('color', { * text: '#000', * background: '#DDD', * }); * // 设置对话的 color.text 属性 * conversation.set('color.text', '#333'); */ set(key, value) { this._debug(`set [${key}]: ${value}`); const { pendingAttributes } = internal(this); const pendingKeys = Object.keys(pendingAttributes); // suppose pendingAttributes = { 'a.b': {} } // set 'a' or 'a.b': delete 'a.b' const re = new RegExp(`^${key}`); const childKeys = pendingKeys.filter(re.test.bind(re)); childKeys.forEach(k => { delete pendingAttributes[k]; }); if (childKeys.length) { pendingAttributes[key] = value; } else { // set 'a.c': nothing to do // set 'a.b.c.d': assign c: { d: {} } to 'a.b' const parentKey = find(pendingKeys, k => key.indexOf(k) === 0); // 'a.b' if (parentKey) { setValue(pendingAttributes[parentKey], key.slice(parentKey.length + 1), value); } else { pendingAttributes[key] = value; } } this._buildCurrentAttributes(); return this; } _buildCurrentAttributes() { const { pendingAttributes } = internal(this); internal(this).currentAttributes = Object.keys(pendingAttributes).reduce((target, k) => setValue(target, k, pendingAttributes[k]), cloneDeep(this._attributes)); } _updateServerAttributes(attributes) { Object.keys(attributes).forEach(key => setValue(this._attributes, key, attributes[key])); this._buildCurrentAttributes(); } _reset() { Object.assign(internal(this), { pendingAttributes: {}, currentAttributes: this._attributes }); } /** * 保存当前对话的属性至服务器 * @return {Promise. } self */ async save() { this._debug('save'); const attr = internal(this).pendingAttributes; if (isEmpty(attr)) { this._debug('nothing touched, resolve with self'); return this; } this._debug('attr: %O', attr); const convMessage = new ConvCommand({ attr: new JsonObjectMessage({ data: JSON.stringify(encode(attr)) }) }); const resCommand = await this._send(new GenericCommand({ op: 'update', convMessage })); this.updatedAt = resCommand.convMessage.udate; this._attributes = internal(this).currentAttributes; internal(this).pendingAttributes = {}; return this; } /** * 从服务器更新对话的属性 * @return {Promise. } self */ async fetch() { const query = this._client.getQuery().equalTo('objectId', this.id); await query.find(); return this; } /** * 静音,客户端拒绝收到服务器端的离线推送通知 * @return {Promise. } self */ async mute() { this._debug('mute'); await this._send(new GenericCommand({ op: 'mute' })); if (!this.transient) { this.muted = true; this.mutedMembers = union(this.mutedMembers, [this._client.id]); } return this; } /** * 取消静音 * @return {Promise. } self */ async unmute() { this._debug('unmute'); await this._send(new GenericCommand({ op: 'unmute' })); if (!this.transient) { this.muted = false; this.mutedMembers = difference(this.mutedMembers, [this._client.id]); } return this; } async _appendConversationSignature(command, action, clientIds) { if (this._client.options.conversationSignatureFactory) { const params = [this.id, this._client.id, clientIds.sort(), action]; const signatureResult = await runSignatureFactory(this._client.options.conversationSignatureFactory, params); Object.assign(command.convMessage, keyRemap({ signature: 's', timestamp: 't', nonce: 'n' }, signatureResult)); } } async _appendBlacklistSignature(command, action, clientIds) { if (this._client.options.blacklistSignatureFactory) { const params = [this.id, this._client.id, clientIds.sort(), action]; const signatureResult = await runSignatureFactory(this._client.options.blacklistSignatureFactory, params); Object.assign(command.blacklistMessage, keyRemap({ signature: 's', timestamp: 't', nonce: 'n' }, signatureResult)); } } /** * 增加成员 * @param {String|String[]} clientIds 新增成员 client id * @return {Promise. } 部分成功结果,包含了成功的 id 列表、失败原因与对应的 id 列表 */ async add(clientIds) { this._debug('add', clientIds); if (typeof clientIds === 'string') { clientIds = [clientIds]; // eslint-disable-line no-param-reassign } const command = new GenericCommand({ op: 'add', convMessage: new ConvCommand({ m: clientIds }) }); await this._appendConversationSignature(command, 'invite', clientIds); const { convMessage, convMessage: { allowedPids } } = await this._send(command); this._addMembers(allowedPids); return createPartiallySuccess(convMessage); } /** * 剔除成员 * @param {String|String[]} clientIds 成员 client id * @return {Promise. } 部分成功结果,包含了成功的 id 列表、失败原因与对应的 id 列表 */ async remove(clientIds) { this._debug('remove', clientIds); if (typeof clientIds === 'string') { clientIds = [clientIds]; // eslint-disable-line no-param-reassign } const command = new GenericCommand({ op: 'remove', convMessage: new ConvCommand({ m: clientIds }) }); await this._appendConversationSignature(command, 'kick', clientIds); const { convMessage, convMessage: { allowedPids } } = await this._send(command); this._removeMembers(allowedPids); return createPartiallySuccess(convMessage); } /** * (当前用户)加入该对话 * @return {Promise. } self */ async join() { this._debug('join'); return this.add(this._client.id).then(({ failures }) => { if (failures[0]) throw failures[0]; return this; }); } /** * (当前用户)退出该对话 * @return {Promise. } self */ async quit() { this._debug('quit'); return this.remove(this._client.id).then(({ failures }) => { if (failures[0]) throw failures[0]; return this; }); } /** * 在该对话中禁言成员 * @param {String|String[]} clientIds 成员 client id * @return {Promise. } 部分成功结果,包含了成功的 id 列表、失败原因与对应的 id 列表 */ async muteMembers(clientIds) { this._debug('mute', clientIds); clientIds = ensureArray(clientIds); // eslint-disable-line no-param-reassign const command = new GenericCommand({ op: OpType.add_shutup, convMessage: new ConvCommand({ m: clientIds }) }); const { convMessage } = await this._send(command); return createPartiallySuccess(convMessage); } /** * 在该对话中解除成员禁言 * @param {String|String[]} clientIds 成员 client id * @return {Promise. } 部分成功结果,包含了成功的 id 列表、失败原因与对应的 id 列表 */ async unmuteMembers(clientIds) { this._debug('unmute', clientIds); clientIds = ensureArray(clientIds); // eslint-disable-line no-param-reassign const command = new GenericCommand({ op: OpType.remove_shutup, convMessage: new ConvCommand({ m: clientIds }) }); const { convMessage } = await this._send(command); return createPartiallySuccess(convMessage); } /** * 查询该对话禁言成员列表 * @param {Object} [options] * @param {Number} [options.limit] 返回的成员数量,服务器默认值 10 * @param {String} [options.next] 从指定 next 开始查询,与 limit 一起使用可以完成翻页。 * @return {PagedResults. } 查询结果。其中的 cureser 存在表示还有更多结果。 */ async queryMutedMembers({ limit, next } = {}) { this._debug('query muted: limit %O, next: %O', limit, next); const command = new GenericCommand({ op: OpType.query_shutup, convMessage: new ConvCommand({ limit, next }) }); const { convMessage: { m, next: newNext } } = await this._send(command); return { results: m, next: newNext }; } /** * 将用户加入该对话黑名单 * @param {String|String[]} clientIds 成员 client id * @return {Promise. } 部分成功结果,包含了成功的 id 列表、失败原因与对应的 id 列表 */ async blockMembers(clientIds) { this._debug('block', clientIds); clientIds = ensureArray(clientIds); // eslint-disable-line no-param-reassign const command = new GenericCommand({ cmd: 'blacklist', op: OpType.block, blacklistMessage: new BlacklistCommand({ srcCid: this.id, toPids: clientIds }) }); await this._appendBlacklistSignature(command, 'conversation-block-clients', clientIds); const { blacklistMessage } = await this._send(command); return createPartiallySuccess(blacklistMessage); } /** * 将用户移出该对话黑名单 * @param {String|String[]} clientIds 成员 client id * @return {Promise. } 部分成功结果,包含了成功的 id 列表、失败原因与对应的 id 列表 */ async unblockMembers(clientIds) { this._debug('unblock', clientIds); clientIds = ensureArray(clientIds); // eslint-disable-line no-param-reassign const command = new GenericCommand({ cmd: 'blacklist', op: OpType.unblock, blacklistMessage: new BlacklistCommand({ srcCid: this.id, toPids: clientIds }) }); await this._appendBlacklistSignature(command, 'conversation-unblock-clients', clientIds); const { blacklistMessage } = await this._send(command); return createPartiallySuccess(blacklistMessage); } /** * 查询该对话黑名单 * @param {Object} [options] * @param {Number} [options.limit] 返回的成员数量,服务器默认值 10 * @param {String} [options.next] 从指定 next 开始查询,与 limit 一起使用可以完成翻页 * @return {PagedResults. } 查询结果。其中的 cureser 存在表示还有更多结果。 */ async queryBlockedMembers({ limit, next } = {}) { this._debug('query blocked: limit %O, next: %O', limit, next); const command = new GenericCommand({ cmd: 'blacklist', op: OpType.query, blacklistMessage: new BlacklistCommand({ srcCid: this.id, limit, next }) }); const { blacklistMessage: { blockedPids, next: newNext } } = await this._send(command); return { results: blockedPids, next: newNext }; } toFullJSON() { const { creator, system, transient, createdAt, updatedAt, _attributes } = this; return { ...super.toFullJSON(), creator, system, transient, createdAt: getTime(createdAt), updatedAt: getTime(updatedAt), ..._attributes }; } toJSON() { const { creator, system, transient, muted, mutedMembers, createdAt, updatedAt, _attributes } = this; return { ...super.toJSON(), creator, system, transient, muted, mutedMembers, createdAt, updatedAt, ..._attributes }; } } /** * 对话成员角色枚举 * @enum {String} * @since 4.0.0 * @memberof module:leancloud-realtime */ const ConversationMemberRole = { /** 所有者 */ OWNER: 'Owner', /** 管理员 */ MANAGER: 'Manager', /** 成员 */ MEMBER: 'Member' }; Object.freeze(ConversationMemberRole); class ConversationMemberInfo { /** * 对话成员属性,保存了成员与某个对话相关的属性,对应 _ConversationMemberInfo 表 * @since 4.0.0 */ constructor({ conversation, memberId, role }) { if (!conversation) throw new Error('conversation requried'); if (!memberId) throw new Error('memberId requried'); Object.assign(internal(this), { conversation, memberId, role }); } /** * 对话 Id * @type {String} * @readonly */ get conversationId() { return internal(this).conversation.id; } /** * 成员 Id * @type {String} * @readonly */ get memberId() { return internal(this).memberId; } /** * 角色 * @type {module:leancloud-realtime.ConversationMemberRole | String} * @readonly */ get role() { if (this.isOwner) return ConversationMemberRole.OWNER; return internal(this).role; } /** * 是否是管理员 * @type {Boolean} * @readonly */ get isOwner() { return this.memberId === internal(this).conversation.creator; } toJSON() { const { conversationId, memberId, role, isOwner } = this; return { conversationId, memberId, role, isOwner }; } } /** * 普通对话 * * 无法直接实例化,请使用 {@link IMClient#createConversation} 创建新的普通对话。 * @extends PersistentConversation * @public */ class Conversation extends PersistentConversation { _addMembers(members) { super._addMembers(members); this.members = union(this.members, members); const { memberInfoMap } = internal(this); if (!memberInfoMap) return; members.forEach(memberId => { memberInfoMap[memberId] = memberInfoMap[memberId] || new ConversationMemberInfo({ conversation: this, memberId, role: ConversationMemberRole.MEMBER }); }); } _removeMembers(members) { super._removeMembers(members); this.members = difference(this.members, members); const { memberInfoMap } = internal(this); if (!memberInfoMap) return; members.forEach(memberId => { delete memberInfoMap[memberId]; }); } async _fetchAllMemberInfo() { const response = await this._client._requestWithSessionToken({ method: 'GET', path: '/classes/_ConversationMemberInfo', query: { where: { cid: this.id } } }); const memberInfos = response.results.map(info => new ConversationMemberInfo({ conversation: this, memberId: info.clientId, role: info.role })); const memberInfoMap = {}; memberInfos.forEach(memberInfo => { memberInfoMap[memberInfo.memberId] = memberInfo; }); this.members.forEach(memberId => { memberInfoMap[memberId] = memberInfoMap[memberId] || new ConversationMemberInfo({ conversation: this, memberId, role: ConversationMemberRole.MEMBER }); }); internal(this).memberInfoMap = memberInfoMap; return memberInfoMap; } /** * 获取所有成员的对话属性 * @since 4.0.0 * @return {Promise. } 所有成员的对话属性列表 */ async getAllMemberInfo({ noCache = false } = {}) { let { memberInfoMap } = internal(this); if (!memberInfoMap || noCache) { memberInfoMap = await this._fetchAllMemberInfo(); } return this.members.map(memberId => memberInfoMap[memberId]); } /** * 获取指定成员的对话属性 * @since 4.0.0 * @param {String} memberId 成员 Id * @return {Promise. } 指定成员的对话属性 */ async getMemberInfo(memberId) { if (this.members.indexOf(memberId) === -1) throw new Error(`${memberId} is not the mumber of conversation[${this.id}]`); const { memberInfoMap } = internal(this); if (!(memberInfoMap && memberInfoMap[memberId])) await this.getAllMemberInfo(); return internal(this).memberInfoMap[memberId]; } /** * 更新指定用户的角色 * @since 4.0.0 * @param {String} memberId 成员 Id * @param {module:leancloud-realtime.ConversationMemberRole | String} role 角色 * @return {Promise. } self */ async updateMemberRole(memberId, role) { this._debug('update member role'); if (role === ConversationMemberRole.OWNER) throw createError({ code: ErrorCode.OWNER_PROMOTION_NOT_ALLOWED }); await this._send(new GenericCommand({ op: OpType.member_info_update, convMessage: new ConvCommand({ targetClientId: memberId, info: new ConvMemberInfo({ pid: memberId, role }) }) })); const { memberInfos } = internal(this); if (memberInfos && memberInfos[memberId]) { internal(memberInfos[memberId]).role = role; } return this; } } /** * 聊天室。 * * 无法直接实例化,请使用 {@link IMClient#createChatRoom} 创建新的聊天室。 * @since 4.0.0 * @extends PersistentConversation * @public */ class ChatRoom extends PersistentConversation {} /** * 服务号。 * * 服务号不支持在客户端创建。 * @since 4.0.0 * @extends PersistentConversation * @public */ class ServiceConversation extends PersistentConversation { /** * 订阅该服务号 * @return {Promise. } self */ async subscribe() { return this.join(); } /** * 退订该服务号 * @return {Promise. } self */ async unsubscribe() { return this.quit(); } } const transformNotFoundError = error => error.code === ErrorCode.CONVERSATION_NOT_FOUND ? createError({ code: ErrorCode.TEMPORARY_CONVERSATION_EXPIRED }) : error; /** * 临时对话 * @since 4.0.0 * @extends ConversationBase * @public */ class TemporaryConversation extends ConversationBase { /** * 无法直接实例化,请使用 {@link IMClient#createTemporaryConversation} 创建新的临时对话。 */ constructor(data, { expiredAt }, client) { super({ ...data, expiredAt }, client); } /** * 对话失效时间 * @type {Date} */ set expiredAt(value) { this._expiredAt = decodeDate(value); } get expiredAt() { return this._expiredAt; } /** * 对话是否已失效 * @type {Boolean} */ get expired() { return this.expiredAt < new Date(); } async _send(...args) { if (this.expired) throw createError({ code: ErrorCode.TEMPORARY_CONVERSATION_EXPIRED }); try { return await super._send(...args); } catch (error) { throw transformNotFoundError(error); } } async send(...args) { try { return await super.send(...args); } catch (error) { throw transformNotFoundError(error); } } toFullJSON() { const { expiredAt } = this; return { ...super.toFullJSON(), expiredAt: getTime(expiredAt) }; } toJSON() { const { expiredAt, expired } = this; return { ...super.toJSON(), expiredAt, expired }; } } const debug$9 = d('LC:ConversationQuery'); class ConversationQuery { static _encode(value) { if (value instanceof Date) { return { __type: 'Date', iso: value.toJSON() }; } if (value instanceof RegExp) { return value.source; } return value; } static _quote(s) { return `\\Q${s.replace('\\E', '\\E\\\\E\\Q')}\\E`; } static _calculateFlag(options) { return ['withLastMessagesRefreshed', 'compact'].reduce( // eslint-disable-next-line no-bitwise (prev, key) => (prev << 1) + Boolean(options[key]), 0); } /** * 构造一个用 AND 连接所有查询的 ConversationQuery * @param {...ConversationQuery} queries * @return {ConversationQuery} */ static and(...queries) { if (queries.length < 2) { throw new Error('The queries must contain at least two elements'); } if (!queries.every(q => q instanceof ConversationQuery)) { throw new Error('The element of queries must be an instance of ConversationQuery'); } const combined = new ConversationQuery(queries[0]._client); combined._where.$and = queries.map(q => q._where); return combined; } /** * 构造一个用 OR 连接所有查询的 ConversationQuery * @param {...ConversationQuery} queries * @return {ConversationQuery} */ static or(...queries) { const combined = ConversationQuery.and(...queries); combined._where.$or = combined._where.$and; delete combined._where.$and; return combined; } /** * Create a ConversationQuery * @param {IMClient} client */ constructor(client) { this._client = client; this._where = {}; this._extraOptions = {}; } _addCondition(key, condition, value) { // Check if we already have a condition if (!this._where[key]) { this._where[key] = {}; } this._where[key][condition] = this.constructor._encode(value); return this; } toJSON() { const json = { where: this._where, flag: this.constructor._calculateFlag(this._extraOptions) }; if (typeof this._skip !== 'undefined') json.skip = this._skip; if (typeof this._limit !== 'undefined') json.limit = this._limit; if (typeof this._order !== 'undefined') json.sort = this._order; debug$9(json); return json; } /** * 增加查询条件,指定聊天室的组员包含某些成员即可返回 * @param {string[]} peerIds - 成员 ID 列表 * @return {ConversationQuery} self */ containsMembers(peerIds) { return this.containsAll('m', peerIds); } /** * 增加查询条件,指定聊天室的组员条件满足条件的才返回 * * @param {string[]} - 成员 ID 列表 * @param {Boolean} includeSelf - 是否包含自己 * @return {ConversationQuery} self */ withMembers(peerIds, includeSelf) { const peerIdsSet = new Set(peerIds); if (includeSelf) { peerIdsSet.add(this._client.id); } this.sizeEqualTo('m', peerIdsSet.size); return this.containsMembers(Array.from(peerIdsSet)); } /** * 增加查询条件,当 conversation 的属性中对应的字段满足等于条件时即可返回 * * @param {string} key * @param value * @return {ConversationQuery} self */ equalTo(key, value) { this._where[key] = this.constructor._encode(value); return this; } /** * 增加查询条件,当 conversation 的属性中对应的字段满足小于条件时即可返回 * @param {string} key * @param value * @return {ConversationQuery} self */ lessThan(key, value) { return this._addCondition(key, '$lt', value); } /** * 增加查询条件,当 conversation 的属性中对应的字段满足小于等于条件时即可返回 * @param {string} key * @param value * @return {ConversationQuery} self */ lessThanOrEqualTo(key, value) { return this._addCondition(key, '$lte', value); } /** * 增加查询条件,当 conversation 的属性中对应的字段满足大于条件时即可返回 * * @param {string} key * @param value * @return {ConversationQuery} self */ greaterThan(key, value) { return this._addCondition(key, '$gt', value); } /** * 增加查询条件,当 conversation 的属性中对应的字段满足大于等于条件时即可返回 * * @param {string} key * @param value * @return {ConversationQuery} self */ greaterThanOrEqualTo(key, value) { return this._addCondition(key, '$gte', value); } /** * 增加查询条件,当 conversation 的属性中对应的字段满足不等于条件时即可返回 * * @param {string} key * @param value * @return {ConversationQuery} self */ notEqualTo(key, value) { return this._addCondition(key, '$ne', value); } /** * 增加查询条件,当 conversation 存在指定的字段时即可返回 * * @since 3.5.0 * @param {string} key * @return {ConversationQuery} self */ exists(key) { return this._addCondition(key, '$exists', true); } /** * 增加查询条件,当 conversation 不存在指定的字段时即可返回 * * @since 3.5.0 * @param {string} key * @return {ConversationQuery} self */ doesNotExist(key) { return this._addCondition(key, '$exists', false); } /** * 增加查询条件,当 conversation 的属性中对应的字段对应的值包含在指定值中时即可返回 * * @param {string} key * @param values * @return {ConversationQuery} self */ containedIn(key, values) { return this._addCondition(key, '$in', values); } /** * 增加查询条件,当 conversation 的属性中对应的字段对应的值不包含在指定值中时即可返回 * * @param {string} key * @param values * @return {ConversationQuery} self */ notContainsIn(key, values) { return this._addCondition(key, '$nin', values); } /** * 增加查询条件,当conversation的属性中对应的字段中的元素包含所有的值才可返回 * * @param {string} key * @param values * @return {ConversationQuery} self */ containsAll(key, values) { return this._addCondition(key, '$all', values); } /** * 增加查询条件,当 conversation 的属性中对应的字段对应的值包含此字符串即可返回 * * @param {string} key * @param {string} subString * @return {ConversationQuery} self */ contains(key, subString) { return this._addCondition(key, '$regex', ConversationQuery._quote(subString)); } /** * 增加查询条件,当 conversation 的属性中对应的字段对应的值以此字符串起始即可返回 * * @param {string} key * @param {string} prefix * @return {ConversationQuery} self */ startsWith(key, prefix) { return this._addCondition(key, '$regex', `^${ConversationQuery._quote(prefix)}`); } /** * 增加查询条件,当 conversation 的属性中对应的字段对应的值以此字符串结束即可返回 * * @param {string} key * @param {string} suffix * @return {ConversationQuery} self */ endsWith(key, suffix) { return this._addCondition(key, '$regex', `${ConversationQuery._quote(suffix)}$`); } /** * 增加查询条件,当 conversation 的属性中对应的字段对应的值满足提供的正则表达式即可返回 * * @param {string} key * @param {RegExp} regex * @return {ConversationQuery} self */ matches(key, regex) { this._addCondition(key, '$regex', regex); // Javascript regex options support mig as inline options but store them // as properties of the object. We support mi & should migrate them to // modifiers let _modifiers = ''; if (regex.ignoreCase) { _modifiers += 'i'; } if (regex.multiline) { _modifiers += 'm'; } if (_modifiers && _modifiers.length) { this._addCondition(key, '$options', _modifiers); } return this; } /** * 添加查询约束条件,查找 key 类型是数组,该数组的长度匹配提供的数值 * * @param {string} key * @param {Number} length * @return {ConversationQuery} self */ sizeEqualTo(key, length) { return this._addCondition(key, '$size', length); } /** * 设置返回集合的大小上限 * * @param {Number} limit - 上限 * @return {ConversationQuery} self */ limit(limit) { this._limit = limit; return this; } /** * 设置返回集合的起始位置,一般用于分页 * * @param {Number} skip - 起始位置跳过几个对象 * @return {ConversationQuery} self */ skip(skip) { this._skip = skip; return this; } /** * 设置返回集合按照指定key进行增序排列 * * @param {string} key * @return {ConversationQuery} self */ ascending(key) { this._order = key; return this; } /** * 设置返回集合按照指定key进行增序排列,如果已设置其他排序,原排序的优先级较高 * * @param {string} key * @return {ConversationQuery} self */ addAscending(key) { if (this._order) { this._order += `,${key}`; } else { this._order = key; } return this; } /** * 设置返回集合按照指定 key 进行降序排列 * * @param {string} key * @return {ConversationQuery} self */ descending(key) { this._order = `-${key}`; return this; } /** * 设置返回集合按照指定 key 进行降序排列,如果已设置其他排序,原排序的优先级较高 * * @param {string} key * @return {ConversationQuery} self */ addDescending(key) { if (this._order) { this._order += `,-${key}`; } else { this._order = `-${key}`; } return this; } /** * 设置返回的 conversations 刷新最后一条消息 * @param {Boolean} [enabled=true] * @return {ConversationQuery} self */ withLastMessagesRefreshed(enabled = true) { this._extraOptions.withLastMessagesRefreshed = enabled; return this; } /** * 设置返回的 conversations 为精简模式,即不含成员列表 * @param {Boolean} [enabled=true] * @return {ConversationQuery} self */ compact(enabled = true) { this._extraOptions.compact = enabled; return this; } /** * 执行查询 * @return {Promise. } */ async find() { return this._client._executeQuery(this); } /** * 返回符合条件的第一个结果 * @return {Promise. } */ async first() { return (await this.limit(1).find())[0]; } } const debug$a = d('LC:SessionManager'); class SessionManager { constructor({ refresh, onBeforeGetSessionToken } = {}) { this.refresh = refresh; this._onBeforeGetSessionToken = onBeforeGetSessionToken; this.setSessionToken(null, 0); } setSessionToken(token, ttl) { debug$a('set session token', token, ttl); const sessionToken = new Expirable(token, ttl * 1000); this._sessionToken = sessionToken; delete this._pendingSessionTokenPromise; return sessionToken; } async setSessionTokenAsync(promise) { const currentSessionToken = this._sessionToken; this._pendingSessionTokenPromise = promise.catch(error => { // revert, otherwise the following getSessionToken calls // will all be rejected this._sessionToken = currentSessionToken; throw error; }); return this.setSessionToken(...(await this._pendingSessionTokenPromise)); } async getSessionToken({ autoRefresh = true } = {}) { debug$a('get session token'); if (this._onBeforeGetSessionToken) { this._onBeforeGetSessionToken(this); } const { value, originalValue } = this._sessionToken || (await this._pendingSessionTokenPromise); if (value === Expirable.EXPIRED && autoRefresh && this.refresh) { debug$a('refresh expired session token'); const { value: newValue } = await this.setSessionTokenAsync(this.refresh(this, originalValue)); debug$a('session token', newValue); return newValue; } debug$a('session token', value); return value; } revoke() { if (this._sessionToken) this._sessionToken.expiredAt = -1; } } var _dec$2, _dec2, _class$3; const debug$b = d('LC:IMClient'); const { INVITED: INVITED$1, KICKED: KICKED$1, MEMBERS_JOINED: MEMBERS_JOINED$1, MEMBERS_LEFT: MEMBERS_LEFT$1, MEMBER_INFO_UPDATED: MEMBER_INFO_UPDATED$1, BLOCKED: BLOCKED$1, UNBLOCKED: UNBLOCKED$1, MEMBERS_BLOCKED: MEMBERS_BLOCKED$1, MEMBERS_UNBLOCKED: MEMBERS_UNBLOCKED$1, MUTED: MUTED$1, UNMUTED: UNMUTED$1, MEMBERS_MUTED: MEMBERS_MUTED$1, MEMBERS_UNMUTED: MEMBERS_UNMUTED$1, MESSAGE: MESSAGE$2, UNREAD_MESSAGES_COUNT_UPDATE: UNREAD_MESSAGES_COUNT_UPDATE$1, CLOSE: CLOSE$1, CONFLICT: CONFLICT$1, UNHANDLED_MESSAGE: UNHANDLED_MESSAGE$1, CONVERSATION_INFO_UPDATED: CONVERSATION_INFO_UPDATED$1, MESSAGE_RECALL: MESSAGE_RECALL$1, MESSAGE_UPDATE: MESSAGE_UPDATE$1, INFO_UPDATED: INFO_UPDATED$1 } = IMEvent; const isTemporaryConversatrionId = id => /^_tmp:/.test(id); /** * 1 patch-msg * 1 temp-conv-msg * 0 auto-bind-deviceid-and-installation * 1 transient-msg-ack * 1 keep-notification * 1 partial-failed-msg * 0 group-chat-rcp * 1 omit-peer-id * @ignore */ const configBitmap = 0b10111011; let IMClient = (_dec$2 = throttle(1000), _dec2 = throttle(1000), (_class$3 = class IMClient extends EventEmitter { /** * 无法直接实例化,请使用 {@link Realtime#createIMClient} 创建新的 IMClient。 * * @extends EventEmitter */ constructor(id, options = {}, props) { if (!(id === undefined || typeof id === 'string')) { throw new TypeError(`Client id [${id}] is not a String`); } super(); Object.assign(this, { /** * @var id {String} 客户端 id * @memberof IMClient# */ id, options }, props); if (!this._messageParser) { throw new Error('IMClient must be initialized with a MessageParser'); } this._conversationCache = new Cache(`client:${this.id}`); this._ackMessageBuffer = {}; internal(this).lastPatchTime = Date.now(); internal(this).lastNotificationTime = undefined; internal(this)._eventemitter = new EventEmitter(); if (debug$b.enabled) { values(IMEvent).forEach(event => this.on(event, (...payload) => this._debug(`${event} event emitted. %o`, payload))); } // onIMClientCreate hook applyDecorators(this._plugins.onIMClientCreate, this); } _debug(...params) { debug$b(...params, `[${this.id}]`); } /** * @override * @private */ async _dispatchCommand(command) { this._debug(trim(command), 'received'); if (command.serverTs && command.notificationType === 1) { internal(this).lastNotificationTime = getTime(decodeDate(command.serverTs)); } switch (command.cmd) { case CommandType.conv: return this._dispatchConvMessage(command); case CommandType.direct: return this._dispatchDirectMessage(command); case CommandType.session: return this._dispatchSessionMessage(command); case CommandType.unread: return this._dispatchUnreadMessage(command); case CommandType.rcp: return this._dispatchRcpMessage(command); case CommandType.patch: return this._dispatchPatchMessage(command); default: return this.emit(UNHANDLED_MESSAGE$1, command); } } async _dispatchSessionMessage(message) { const { sessionMessage: { code, reason } } = message; switch (message.op) { case OpType.closed: { internal(this)._eventemitter.emit('close'); if (code === ErrorCode.SESSION_CONFLICT) { /** * 用户在其他客户端登录,当前客户端被服务端强行下线。详见文档「单点登录」章节。 * @event IMClient#CONFLICT * @param {Object} payload * @param {string} payload.reason 原因 */ return this.emit(CONFLICT$1, { reason }); } /** * 当前客户端被服务端强行下线 * @event IMClient#CLOSE * @param {Object} payload * @param {Number} payload.code 错误码 * @param {String} payload.reason 原因 */ return this.emit(CLOSE$1, { code, reason }); } default: this.emit(UNHANDLED_MESSAGE$1, message); throw new Error('Unrecognized session command'); } } _dispatchUnreadMessage({ unreadMessage: { convs, notifTime } }) { internal(this).lastUnreadNotifTime = notifTime; // ensure all converstions are cached return this.getConversations(convs.map(conv => conv.cid)).then(() => // update conversations data Promise.all(convs.map(({ cid, unread, mid, timestamp: ts, from, data, binaryMsg, patchTimestamp, mentioned }) => { const conversation = this._conversationCache.get(cid); // deleted conversation if (!conversation) return null; let timestamp; if (ts) { timestamp = decodeDate(ts); conversation.lastMessageAt = timestamp; // eslint-disable-line no-param-reassign } return (mid ? this._messageParser.parse(binaryMsg || data).then(message => { const messageProps = { id: mid, cid, timestamp, updatedAt: patchTimestamp, from }; Object.assign(message, messageProps); conversation.lastMessage = message; // eslint-disable-line no-param-reassign }) : Promise.resolve()).then(() => { conversation._setUnreadMessagesMentioned(mentioned); const countNotUpdated = unread === internal(conversation).unreadMessagesCount; if (countNotUpdated) return null; // to be filtered // manipulate internal property directly to skip unreadmessagescountupdate event internal(conversation).unreadMessagesCount = unread; return conversation; }); // filter conversations without unread count update })).then(conversations => conversations.filter(conversation => conversation))).then(conversations => { if (conversations.length) { /** * 未读消息数目更新 * @event IMClient#UNREAD_MESSAGES_COUNT_UPDATE * @since 3.4.0 * @param {Conversation[]} conversations 未读消息数目有更新的对话列表 */ this.emit(UNREAD_MESSAGES_COUNT_UPDATE$1, conversations); } }); } async _dispatchRcpMessage(message) { const { rcpMessage, rcpMessage: { read } } = message; const conversationId = rcpMessage.cid; const messageId = rcpMessage.id; const timestamp = decodeDate(rcpMessage.t); const conversation = this._conversationCache.get(conversationId); // conversation not cached means the client does not send the message // during this session if (!conversation) return; conversation._handleReceipt({ messageId, timestamp, read }); } _dispatchPatchMessage({ patchMessage: { patches } }) { // ensure all converstions are cached return this.getConversations(patches.map(patch => patch.cid)).then(() => Promise.all(patches.map(({ cid, mid, timestamp, recall, data, patchTimestamp, from, binaryMsg, mentionAll, mentionPids, patchCode, patchReason }) => { const conversation = this._conversationCache.get(cid); // deleted conversation if (!conversation) return null; return this._messageParser.parse(binaryMsg || data).then(message => { const patchTime = getTime(decodeDate(patchTimestamp)); const messageProps = { id: mid, cid, timestamp, updatedAt: patchTime, from, mentionList: mentionPids, mentionedAll: mentionAll }; Object.assign(message, messageProps); message._setStatus(MessageStatus.SENT); message._updateMentioned(this.id); if (internal(this).lastPatchTime < patchTime) { internal(this).lastPatchTime = patchTime; } // update conversation lastMessage if (conversation.lastMessage && conversation.lastMessage.id === mid) { conversation.lastMessage = message; // eslint-disable-line no-param-reassign } let reason; if (patchCode) { reason = { code: patchCode.toNumber(), detail: patchReason }; } if (recall) { /** * 消息被撤回 * @event IMClient#MESSAGE_RECALL * @param {AVMessage} message 被撤回的消息 * @param {ConversationBase} conversation 消息所在的会话 * @param {PatchReason} [reason] 撤回的原因,不存在代表是发送者主动撤回 */ this.emit(MESSAGE_RECALL$1, message, conversation, reason); /** * 消息被撤回 * @event ConversationBase#MESSAGE_RECALL * @param {AVMessage} message 被撤回的消息 * @param {PatchReason} [reason] 撤回的原因,不存在代表是发送者主动撤回 */ conversation.emit(MESSAGE_RECALL$1, message, reason); } else { /** * 消息被修改 * @event IMClient#MESSAGE_UPDATE * @param {AVMessage} message 被修改的消息 * @param {ConversationBase} conversation 消息所在的会话 * @param {PatchReason} [reason] 修改的原因,不存在代表是发送者主动修改 */ this.emit(MESSAGE_UPDATE$1, message, conversation, reason); /** * 消息被修改 * @event ConversationBase#MESSAGE_UPDATE * @param {AVMessage} message 被修改的消息 * @param {PatchReason} [reason] 修改的原因,不存在代表是发送者主动修改 */ conversation.emit(MESSAGE_UPDATE$1, message, reason); } }); }))); } async _dispatchConvMessage(message) { const { convMessage, convMessage: { initBy, m, info, attr } } = message; const conversation = await this.getConversation(convMessage.cid); switch (message.op) { case OpType.joined: { conversation._addMembers([this.id]); const payload = { invitedBy: initBy }; /** * 当前用户被添加至某个对话 * @event IMClient#INVITED * @param {Object} payload * @param {String} payload.invitedBy 邀请者 id * @param {ConversationBase} conversation */ this.emit(INVITED$1, payload, conversation); /** * 当前用户被添加至当前对话 * @event ConversationBase#INVITED * @param {Object} payload * @param {String} payload.invitedBy 该移除操作的发起者 id */ conversation.emit(INVITED$1, payload); return; } case OpType.left: { conversation._removeMembers([this.id]); const payload = { kickedBy: initBy }; /** * 当前用户被从某个对话中移除 * @event IMClient#KICKED * @param {Object} payload * @param {String} payload.kickedBy 该移除操作的发起者 id * @param {ConversationBase} conversation */ this.emit(KICKED$1, payload, conversation); /** * 当前用户被从当前对话中移除 * @event ConversationBase#KICKED * @param {Object} payload * @param {String} payload.kickedBy 该移除操作的发起者 id */ conversation.emit(KICKED$1, payload); return; } case OpType.members_joined: { conversation._addMembers(m); const payload = { invitedBy: initBy, members: m }; /** * 有用户被添加至某个对话 * @event IMClient#MEMBERS_JOINED * @param {Object} payload * @param {String[]} payload.members 被添加的用户 id 列表 * @param {String} payload.invitedBy 邀请者 id * @param {ConversationBase} conversation */ this.emit(MEMBERS_JOINED$1, payload, conversation); /** * 有成员被添加至当前对话 * @event ConversationBase#MEMBERS_JOINED * @param {Object} payload * @param {String[]} payload.members 被添加的成员 id 列表 * @param {String} payload.invitedBy 邀请者 id */ conversation.emit(MEMBERS_JOINED$1, payload); return; } case OpType.members_left: { conversation._removeMembers(m); const payload = { kickedBy: initBy, members: m }; /** * 有成员被从某个对话中移除 * @event IMClient#MEMBERS_LEFT * @param {Object} payload * @param {String[]} payload.members 被移除的成员 id 列表 * @param {String} payload.kickedBy 该移除操作的发起者 id * @param {ConversationBase} conversation */ this.emit(MEMBERS_LEFT$1, payload, conversation); /** * 有成员被从当前对话中移除 * @event ConversationBase#MEMBERS_LEFT * @param {Object} payload * @param {String[]} payload.members 被移除的成员 id 列表 * @param {String} payload.kickedBy 该移除操作的发起者 id */ conversation.emit(MEMBERS_LEFT$1, payload); return; } case OpType.members_blocked: { const payload = { blockedBy: initBy, members: m }; /** * 有成员被加入某个对话的黑名单 * @event IMClient#MEMBERS_BLOCKED * @param {Object} payload * @param {String[]} payload.members 成员 id 列表 * @param {String} payload.blockedBy 该操作的发起者 id * @param {ConversationBase} conversation */ this.emit(MEMBERS_BLOCKED$1, payload, conversation); /** * 有成员被加入当前对话的黑名单 * @event ConversationBase#MEMBERS_BLOCKED * @param {Object} payload * @param {String[]} payload.members 成员 id 列表 * @param {String} payload.blockedBy 该操作的发起者 id */ conversation.emit(MEMBERS_BLOCKED$1, payload); return; } case OpType.members_unblocked: { const payload = { unblockedBy: initBy, members: m }; /** * 有成员被移出某个对话的黑名单 * @event IMClient#MEMBERS_UNBLOCKED * @param {Object} payload * @param {String[]} payload.members 成员 id 列表 * @param {String} payload.unblockedBy 该操作的发起者 id * @param {ConversationBase} conversation */ this.emit(MEMBERS_UNBLOCKED$1, payload, conversation); /** * 有成员被移出当前对话的黑名单 * @event ConversationBase#MEMBERS_UNBLOCKED * @param {Object} payload * @param {String[]} payload.members 成员 id 列表 * @param {String} payload.unblockedBy 该操作的发起者 id */ conversation.emit(MEMBERS_UNBLOCKED$1, payload); return; } case OpType.blocked: { const payload = { blockedBy: initBy }; /** * 当前用户被加入某个对话的黑名单 * @event IMClient#BLOCKED * @param {Object} payload * @param {String} payload.blockedBy 该操作的发起者 id * @param {ConversationBase} conversation */ this.emit(BLOCKED$1, payload, conversation); /** * 当前用户被加入当前对话的黑名单 * @event ConversationBase#BLOCKED * @param {Object} payload * @param {String} payload.blockedBy 该操作的发起者 id */ conversation.emit(BLOCKED$1, payload); return; } case OpType.unblocked: { const payload = { unblockedBy: initBy }; /** * 当前用户被移出某个对话的黑名单 * @event IMClient#UNBLOCKED * @param {Object} payload * @param {String} payload.unblockedBy 该操作的发起者 id * @param {ConversationBase} conversation */ this.emit(UNBLOCKED$1, payload, conversation); /** * 当前用户被移出当前对话的黑名单 * @event ConversationBase#UNBLOCKED * @param {Object} payload * @param {String} payload.unblockedBy 该操作的发起者 id */ conversation.emit(UNBLOCKED$1, payload); return; } case OpType.members_shutuped: { const payload = { mutedBy: initBy, members: m }; /** * 有成员在某个对话中被禁言 * @event IMClient#MEMBERS_MUTED * @param {Object} payload * @param {String[]} payload.members 成员 id 列表 * @param {String} payload.mutedBy 该操作的发起者 id * @param {ConversationBase} conversation */ this.emit(MEMBERS_MUTED$1, payload, conversation); /** * 有成员在当前对话中被禁言 * @event ConversationBase#MEMBERS_MUTED * @param {Object} payload * @param {String[]} payload.members 成员 id 列表 * @param {String} payload.mutedBy 该操作的发起者 id */ conversation.emit(MEMBERS_MUTED$1, payload); return; } case OpType.members_unshutuped: { const payload = { unmutedBy: initBy, members: m }; /** * 有成员在某个对话中被解除禁言 * @event IMClient#MEMBERS_UNMUTED * @param {Object} payload * @param {String[]} payload.members 成员 id 列表 * @param {String} payload.unmutedBy 该操作的发起者 id * @param {ConversationBase} conversation */ this.emit(MEMBERS_UNMUTED$1, payload, conversation); /** * 有成员在当前对话中被解除禁言 * @event ConversationBase#MEMBERS_UNMUTED * @param {Object} payload * @param {String[]} payload.members 成员 id 列表 * @param {String} payload.unmutedBy 该操作的发起者 id */ conversation.emit(MEMBERS_UNMUTED$1, payload); return; } case OpType.shutuped: { const payload = { mutedBy: initBy }; /** * 有成员在某个对话中被禁言 * @event IMClient#MUTED * @param {Object} payload * @param {String} payload.mutedBy 该操作的发起者 id * @param {ConversationBase} conversation */ this.emit(MUTED$1, payload, conversation); /** * 有成员在当前对话中被禁言 * @event ConversationBase#MUTED * @param {Object} payload * @param {String} payload.mutedBy 该操作的发起者 id */ conversation.emit(MUTED$1, payload); return; } case OpType.unshutuped: { const payload = { unmutedBy: initBy }; /** * 有成员在某个对话中被解除禁言 * @event IMClient#UNMUTED * @param {Object} payload * @param {String} payload.unmutedBy 该操作的发起者 id * @param {ConversationBase} conversation */ this.emit(UNMUTED$1, payload, conversation); /** * 有成员在当前对话中被解除禁言 * @event ConversationBase#UNMUTED * @param {Object} payload * @param {String} payload.unmutedBy 该操作的发起者 id */ conversation.emit(UNMUTED$1, payload); return; } case OpType.member_info_changed: { const { pid, role } = info; const { memberInfoMap } = internal(conversation); // 如果不存在缓存,且不是 role 的更新,则不通知 if (!memberInfoMap && !role) return; const memberInfo = await conversation.getMemberInfo(pid); internal(memberInfo).role = role; const payload = { member: pid, memberInfo, updatedBy: initBy }; /** * 有成员的对话信息被更新 * @event IMClient#MEMBER_INFO_UPDATED * @param {Object} payload * @param {String} payload.member 被更新对话信息的成员 id * @param {ConversationMumberInfo} payload.memberInfo 被更新的成员对话信息 * @param {String} payload.updatedBy 该操作的发起者 id * @param {ConversationBase} conversation */ this.emit(MEMBER_INFO_UPDATED$1, payload, conversation); /** * 有成员的对话信息被更新 * @event ConversationBase#MEMBER_INFO_UPDATED * @param {Object} payload * @param {String} payload.member 被更新对话信息的成员 id * @param {ConversationMumberInfo} payload.memberInfo 被更新的成员对话信息 * @param {String} payload.updatedBy 该操作的发起者 id */ conversation.emit(MEMBER_INFO_UPDATED$1, payload); return; } case OpType.updated: { const attributes = decode(JSON.parse(attr.data)); conversation._updateServerAttributes(attributes); const payload = { attributes, updatedBy: initBy }; /** * 该对话信息被更新 * @event IMClient#CONVERSATION_INFO_UPDATED * @param {Object} payload * @param {Object} payload.attributes 被更新的属性 * @param {String} payload.updatedBy 该操作的发起者 id * @param {ConversationBase} conversation */ this.emit(CONVERSATION_INFO_UPDATED$1, payload, conversation); /** * 有对话信息被更新 * @event ConversationBase#INFO_UPDATED * @param {Object} payload * @param {Object} payload.attributes 被更新的属性 * @param {String} payload.updatedBy 该操作的发起者 id */ conversation.emit(INFO_UPDATED$1, payload); return; } default: this.emit(UNHANDLED_MESSAGE$1, message); throw new Error('Unrecognized conversation command'); } } _dispatchDirectMessage(originalMessage) { const { directMessage, directMessage: { id, cid, fromPeerId, timestamp, transient, patchTimestamp, mentionPids, mentionAll, binaryMsg, msg } } = originalMessage; const content = binaryMsg ? binaryMsg.toArrayBuffer() : msg; return Promise.all([this.getConversation(directMessage.cid), this._messageParser.parse(content)]).then(([conversation, message]) => { // deleted conversation if (!conversation) return undefined; const messageProps = { id, cid, timestamp, updatedAt: patchTimestamp, from: fromPeerId, mentionList: mentionPids, mentionedAll: mentionAll }; Object.assign(message, messageProps); message._updateMentioned(this.id); message._setStatus(MessageStatus.SENT); // filter outgoing message sent from another device if (message.from !== this.id) { if (!(transient || conversation.transient)) { this._sendAck(message); } } return this._dispatchParsedMessage(message, conversation); }); } _dispatchParsedMessage(message, conversation) { // beforeMessageDispatch hook return applyDispatcher(this._plugins.beforeMessageDispatch, [message, conversation]).then(shouldDispatch => { if (shouldDispatch === false) return; conversation.lastMessage = message; // eslint-disable-line no-param-reassign conversation.lastMessageAt = message.timestamp; // eslint-disable-line no-param-reassign // filter outgoing message sent from another device if (message.from !== this.id) { conversation.unreadMessagesCount += 1; // eslint-disable-line no-param-reassign if (message.mentioned) conversation._setUnreadMessagesMentioned(true); } /** * 当前用户收到消息 * @event IMClient#MESSAGE * @param {Message} message * @param {ConversationBase} conversation 收到消息的对话 */ this.emit(MESSAGE$2, message, conversation); /** * 当前对话收到消息 * @event ConversationBase#MESSAGE * @param {Message} message */ conversation.emit(MESSAGE$2, message); }); } _sendAck(message) { this._debug('send ack for %O', message); const { cid } = message; if (!cid) { throw new Error('missing cid'); } if (!this._ackMessageBuffer[cid]) { this._ackMessageBuffer[cid] = []; } this._ackMessageBuffer[cid].push(message); return this._doSendAck(); } // jsdoc-ignore-start // jsdoc-ignore-end _doSendAck() { // if not connected, just skip everything if (!this._connection.is('connected')) return; this._debug('do send ack %O', this._ackMessageBuffer); Promise.all(Object.keys(this._ackMessageBuffer).map(cid => { const convAckMessages = this._ackMessageBuffer[cid]; const timestamps = convAckMessages.map(message => message.timestamp); const command = new GenericCommand({ cmd: 'ack', ackMessage: new AckCommand({ cid, fromts: Math.min.apply(null, timestamps), tots: Math.max.apply(null, timestamps) }) }); delete this._ackMessageBuffer[cid]; return this._send(command, false).catch(error => { this._debug('send ack failed: %O', error); this._ackMessageBuffer[cid] = convAckMessages; }); })); } _omitPeerId(value) { internal(this).peerIdOmittable = value; } _send(cmd, ...args) { const command = cmd; if (!internal(this).peerIdOmittable && this.id) { command.peerId = this.id; } return this._connection.send(command, ...args); } async _open(appId, tag, deviceId, isReconnect = false) { this._debug('open session'); const { lastUnreadNotifTime, lastPatchTime, lastNotificationTime } = internal(this); const command = new GenericCommand({ cmd: 'session', op: 'open', appId, peerId: this.id, sessionMessage: new SessionCommand({ ua: `js/${version}`, r: isReconnect, lastUnreadNotifTime, lastPatchTime, configBitmap }) }); if (!isReconnect) { Object.assign(command.sessionMessage, trim({ tag, deviceId })); if (this.options.signatureFactory) { const signatureResult = await runSignatureFactory(this.options.signatureFactory, [this._identity]); Object.assign(command.sessionMessage, keyRemap({ signature: 's', timestamp: 't', nonce: 'n' }, signatureResult)); } } else { const sessionToken = await this._sessionManager.getSessionToken({ autoRefresh: false }); if (sessionToken && sessionToken !== Expirable.EXPIRED) { Object.assign(command.sessionMessage, { st: sessionToken }); } } let resCommand; try { resCommand = await this._send(command); } catch (error) { if (error.code === ErrorCode.SESSION_TOKEN_EXPIRED) { if (!this._sessionManager) { // let it fail if sessoinToken not cached but command rejected as token expired // to prevent session openning flood throw new Error('Unexpected session expiration'); } debug$b('Session token expired, reopening'); this._sessionManager.revoke(); return this._open(appId, tag, deviceId, isReconnect); } throw error; } const { peerId, sessionMessage, sessionMessage: { st: token, stTtl: tokenTTL, code }, serverTs } = resCommand; if (code) { throw createError(sessionMessage); } if (peerId) { this.id = peerId; if (!this._identity) this._identity = peerId; if (token) { this._sessionManager = this._sessionManager || this._createSessionManager(); this._sessionManager.setSessionToken(token, tokenTTL); } const serverTime = getTime(decodeDate(serverTs)); if (serverTs) { internal(this).lastPatchTime = serverTime; } if (lastNotificationTime) { // Do not await for it as this is failable this._syncNotifications(lastNotificationTime).catch(error => console.warn('Syncing notifications failed:', error)); } else { // Set timestamp to now for next reconnection internal(this).lastNotificationTime = serverTime; } } else { console.warn('Unexpected session opened without peerId.'); } return undefined; } async _syncNotifications(timestamp) { const { hasMore, notifications } = await this._fetchNotifications(timestamp); notifications.forEach(notification => { const { cmd, op, serverTs, notificationType, ...payload } = notification; this._dispatchCommand({ cmd: CommandType[cmd], op: OpType[op], serverTs, notificationType, [`${cmd}Message`]: payload }); }); if (hasMore) { return this._syncNotifications(internal(this).lastNotificationTime); } return undefined; } async _fetchNotifications(timestamp) { return this._requestWithSessionToken({ method: 'GET', path: '/rtm/notifications', query: { start_ts: timestamp, notification_type: 'permanent' } }); } _createSessionManager() { debug$b('create SessionManager'); return new SessionManager({ onBeforeGetSessionToken: this._connection.checkConnectionAvailability.bind(this._connection), refresh: (manager, expiredSessionToken) => manager.setSessionTokenAsync(Promise.resolve(new GenericCommand({ cmd: 'session', op: 'refresh', sessionMessage: new SessionCommand({ ua: `js/${version}`, st: expiredSessionToken }) })).then(async command => { if (this.options.signatureFactory) { const signatureResult = await runSignatureFactory(this.options.signatureFactory, [this._identity]); Object.assign(command.sessionMessage, keyRemap({ signature: 's', timestamp: 't', nonce: 'n' }, signatureResult)); } return command; }).then(this._send.bind(this)).then(({ sessionMessage: { st: token, stTtl: ttl } }) => [token, ttl])) }); } async _requestWithSessionToken({ headers, query, ...params }) { const sessionToken = await this._sessionManager.getSessionToken(); return this._request({ headers: { 'X-LC-IM-Session-Token': sessionToken, ...headers }, query: { client_id: this.id, ...query }, ...params }); } /** * 关闭客户端 * @return {Promise} */ async close() { this._debug('close session'); const _ee = internal(this)._eventemitter; _ee.emit('beforeclose'); if (this._connection.is('connected')) { const command = new GenericCommand({ cmd: 'session', op: 'close' }); await this._send(command); } _ee.emit('close'); this.emit(CLOSE$1, { code: 0 }); } /** * 获取 client 列表中在线的 client,每次查询最多 20 个 clientId,超出部分会被忽略 * @param {String[]} clientIds 要查询的 client ids * @return {Primse. } 在线的 client ids */ async ping(clientIds) { this._debug('ping'); if (!(clientIds instanceof Array)) { throw new TypeError(`clientIds ${clientIds} is not an Array`); } if (!clientIds.length) { return Promise.resolve([]); } const command = new GenericCommand({ cmd: 'session', op: 'query', sessionMessage: new SessionCommand({ sessionPeerIds: clientIds }) }); const resCommand = await this._send(command); return resCommand.sessionMessage.onlineSessionPeerIds; } /** * 获取某个特定的对话 * @param {String} id 对话 id,对应 _Conversation 表中的 objectId * @param {Boolean} [noCache=false] 强制不从缓存中获取 * @return {Promise. } 如果 id 对应的对话不存在则返回 null */ async getConversation(id, noCache = false) { if (typeof id !== 'string') { throw new TypeError(`${id} is not a String`); } if (!noCache) { const cachedConversation = this._conversationCache.get(id); if (cachedConversation) { return cachedConversation; } } if (isTemporaryConversatrionId(id)) { return (await this._getTemporaryConversations([id]))[0] || null; } return this.getQuery().equalTo('objectId', id).find().then(conversations => conversations[0] || null); } /** * 通过 id 批量获取某个特定的对话 * @since 3.4.0 * @param {String[]} ids 对话 id 列表,对应 _Conversation 表中的 objectId * @param {Boolean} [noCache=false] 强制不从缓存中获取 * @return {Promise. } 如果 id 对应的对话不存在则返回 null */ async getConversations(ids, noCache = false) { const remoteConversationIds = noCache ? ids : ids.filter(id => this._conversationCache.get(id) === null); if (remoteConversationIds.length) { const remoteTemporaryConversationIds = remove(remoteConversationIds, isTemporaryConversatrionId); const query = []; if (remoteConversationIds.length) { query.push(this.getQuery().containedIn('objectId', remoteConversationIds).limit(999).find()); } if (remoteTemporaryConversationIds.length) { const remoteTemporaryConversationsPromise = remoteTemporaryConversationIds.map(this._getTemporaryConversations.bind(this)); query.push(...remoteTemporaryConversationsPromise); } await Promise.all(query); } return ids.map(id => this._conversationCache.get(id)); } async _getTemporaryConversations(ids) { const command = new GenericCommand({ cmd: 'conv', op: 'query', convMessage: new ConvCommand({ tempConvIds: ids }) }); const resCommand = await this._send(command); return this._handleQueryResults(resCommand); } /** * 构造一个 ConversationQuery 来查询对话 * @return {ConversationQuery. } */ getQuery() { return new ConversationQuery(this); } /** * 构造一个 ConversationQuery 来查询聊天室 * @return {ConversationQuery. } */ getChatRoomQuery() { return this.getQuery().equalTo('tr', true); } /** * 构造一个 ConversationQuery 来查询服务号 * @return {ConversationQuery. } */ getServiceConversationQuery() { return this.getQuery().equalTo('sys', true); } async _executeQuery(query) { const queryJSON = query.toJSON(); queryJSON.where = new JsonObjectMessage({ data: JSON.stringify(encode(queryJSON.where)) }); const command = new GenericCommand({ cmd: 'conv', op: 'query', convMessage: new ConvCommand(queryJSON) }); const resCommand = await this._send(command); return this._handleQueryResults(resCommand); } async _handleQueryResults(resCommand) { let conversations; try { conversations = decode(JSON.parse(resCommand.convMessage.results.data)); } catch (error) { const commandString = JSON.stringify(trim(resCommand)); throw new Error(`Parse query result failed: ${error.message}. Command: ${commandString}`); } conversations = await Promise.all(conversations.map(this._parseConversationFromRawData.bind(this))); return conversations.map(this._upsertConversationToCache.bind(this)); } _upsertConversationToCache(fetchedConversation) { let conversation = this._conversationCache.get(fetchedConversation.id); if (!conversation) { conversation = fetchedConversation; this._debug('no match, set cache'); this._conversationCache.set(fetchedConversation.id, fetchedConversation); } else { this._debug('update cached conversation'); ['creator', 'createdAt', 'updatedAt', 'lastMessageAt', 'lastMessage', 'mutedMembers', 'members', '_attributes', 'transient', 'muted'].forEach(key => { const value = fetchedConversation[key]; if (value !== undefined) conversation[key] = value; }); if (conversation._reset) conversation._reset(); } return conversation; } /** * 反序列化消息,与 {@link Message#toFullJSON} 相对。 * @param {Object} * @return {AVMessage} 解析后的消息 * @since 4.0.0 */ async parseMessage({ data, bin = false, ...properties }) { const content = bin ? base64Arraybuffer.decode(data) : data; const message = await this._messageParser.parse(content); Object.assign(message, properties); message._updateMentioned(this.id); return message; } /** * 反序列化对话,与 {@link Conversation#toFullJSON} 相对。 * @param {Object} * @return {ConversationBase} 解析后的对话 * @since 4.0.0 */ async parseConversation({ id, lastMessageAt, lastMessage, lastDeliveredAt, lastReadAt, unreadMessagesCount, members, mentioned, ...properties }) { const conversationData = { id, lastMessageAt, lastMessage, lastDeliveredAt, lastReadAt, unreadMessagesCount, members, mentioned }; if (lastMessage) { conversationData.lastMessage = await this.parseMessage(lastMessage); conversationData.lastMessage._setStatus(MessageStatus.SENT); } const { transient, system, expiredAt } = properties; if (transient) return new ChatRoom(conversationData, properties, this); if (system) return new ServiceConversation(conversationData, properties, this); if (expiredAt || isTemporaryConversatrionId(id)) { return new TemporaryConversation(conversationData, { expiredAt }, this); } return new Conversation(conversationData, properties, this); } async _parseConversationFromRawData(rawData) { const data = keyRemap({ objectId: 'id', lm: 'lastMessageAt', m: 'members', tr: 'transient', sys: 'system', c: 'creator', mu: 'mutedMembers' }, rawData); if (data.msg) { data.lastMessage = { data: data.msg, bin: data.bin, from: data.msg_from, id: data.msg_mid, timestamp: data.msg_timestamp, updatedAt: data.patch_timestamp }; delete data.lastMessageFrom; delete data.lastMessageId; delete data.lastMessageTimestamp; delete data.lastMessagePatchTimestamp; } const { ttl } = data; if (ttl) data.expiredAt = Date.now() + ttl * 1000; return this.parseConversation(data); } /** * 创建一个对话 * @param {Object} options 除了下列字段外的其他字段将被视为对话的自定义属性 * @param {String[]} options.members 对话的初始成员列表,默认包含当前 client * @param {String} [options.name] 对话的名字 * @param {Boolean} [options.unique=true] 唯一对话,当其为 true 时,如果当前已经有相同成员的对话存在则返回该对话,否则会创建新的对话 * @return {Promise. } */ async createConversation({ members: m, name, transient, unique = true, _tempConv: tempConv, _tempConvTTL: tempConvTTL, ...properties } = {}) { if (!(transient || Array.isArray(m))) { throw new TypeError(`conversation members ${m} is not an array`); } let members = new Set(m); members.add(this.id); members = Array.from(members).sort(); let attr = properties || {}; if (name) { if (typeof name !== 'string') { throw new TypeError(`conversation name ${name} is not a string`); } attr.name = name; } attr = new JsonObjectMessage({ data: JSON.stringify(encode(attr)) }); const startCommandJson = { m: members, attr, transient, unique, tempConv, tempConvTTL }; const command = new GenericCommand({ cmd: 'conv', op: 'start', convMessage: new ConvCommand(startCommandJson) }); if (this.options.conversationSignatureFactory) { const params = [null, this._identity, members, 'create']; const signatureResult = await runSignatureFactory(this.options.conversationSignatureFactory, params); Object.assign(command.convMessage, keyRemap({ signature: 's', timestamp: 't', nonce: 'n' }, signatureResult)); } const { convMessage: { cid, cdate, tempConvTTL: ttl } } = await this._send(command); const data = { name, transient, unique, id: cid, createdAt: cdate, updatedAt: cdate, lastMessageAt: null, creator: this.id, members: transient ? [] : members, ...properties }; if (ttl) data.expiredAt = Date.now() + ttl * 1000; const conversation = await this.parseConversation(data); return this._upsertConversationToCache(conversation); } /** * 创建一个聊天室 * @since 4.0.0 * @param {Object} options 除了下列字段外的其他字段将被视为对话的自定义属性 * @param {String} [options.name] 对话的名字 * @return {Promise. } */ async createChatRoom(param) { return this.createConversation({ ...param, transient: true, members: null, unique: false, _tempConv: false }); } /** * 创建一个临时对话 * @since 4.0.0 * @param {Object} options * @param {String[]} options.members 对话的初始成员列表,默认包含当前 client * @param {String} [options.ttl] 对话存在时间,单位为秒,最大值与默认值均为 86400(一天),过期后该对话不再可用。 * @return {Promise. } */ async createTemporaryConversation({ ttl: _tempConvTTL, ...param }) { return this.createConversation({ ...param, _tempConv: true, _tempConvTTL }); } // jsdoc-ignore-start // jsdoc-ignore-end _doSendRead() { // if not connected, just skip everything if (!this._connection.is('connected')) return; const buffer = internal(this).readConversationsBuffer; const conversations = Array.from(buffer); if (!conversations.length) return; const ids = conversations.map(conversation => { if (!(conversation instanceof ConversationBase)) { throw new TypeError(`${conversation} is not a Conversation`); } return conversation.id; }); this._debug(`mark [${ids}] as read`); buffer.clear(); this._sendReadCommand(conversations).catch(error => { this._debug('send read failed: %O', error); conversations.forEach(buffer.add.bind(buffer)); }); } _sendReadCommand(conversations) { return this._send(new GenericCommand({ cmd: 'read', readMessage: new ReadCommand({ convs: conversations.map(conversation => new ReadTuple({ cid: conversation.id, mid: conversation.lastMessage && conversation.lastMessage.from !== this.id ? conversation.lastMessage.id : undefined, timestamp: (conversation.lastMessageAt || new Date()).getTime() })) }) }), false); } }, (_applyDecoratedDescriptor(_class$3.prototype, "_doSendAck", [_dec$2], Object.getOwnPropertyDescriptor(_class$3.prototype, "_doSendAck"), _class$3.prototype), _applyDecoratedDescriptor(_class$3.prototype, "_doSendRead", [_dec2], Object.getOwnPropertyDescriptor(_class$3.prototype, "_doSendRead"), _class$3.prototype)), _class$3)); /** * 修改、撤回消息的原因 * @typedef PatchReason * @type {Object} * @property {number} code 负数为内置 code,正数为开发者在 hook 中自定义的 code。比如因为敏感词过滤被修改的 code 为 -4408。 * @property {string} [detail] 具体的原因说明。 */ const RECONNECT_ERROR = 'reconnecterror'; var CoreEvent = /*#__PURE__*/Object.freeze({ __proto__: null, RECONNECT_ERROR: RECONNECT_ERROR, DISCONNECT: DISCONNECT, RECONNECT: RECONNECT, RETRY: RETRY, SCHEDULE: SCHEDULE, OFFLINE: OFFLINE, ONLINE: ONLINE }); var _class$4; let // jsdoc-ignore-end BinaryMessage = IE10Compatible(_class$4 = class BinaryMessage extends Message { /** * 二进制消息 * @extends Message * @param {ArrayBuffer} buffer * @since 4.0.0 */ constructor(buffer) { if (!(buffer instanceof ArrayBuffer)) { throw new TypeError(`${buffer} is not an ArrayBuffer`); } super(buffer); } /** * @type ArrayBuffer */ get buffer() { return this.content; } set buffer(buffer) { this.content = buffer; } static validate(target) { return target instanceof ArrayBuffer; } toJSON() { return { ...super._toJSON(), data: base64Arraybuffer.encode(this.content) }; } toFullJSON() { return { ...super.toFullJSON(), bin: true, data: base64Arraybuffer.encode(this.content) }; } }) || _class$4; var _dec$3, _class$5; let // jsdoc-ignore-end TextMessage = (_dec$3 = messageType(-1), _dec$3(_class$5 = IE10Compatible(_class$5 = class TextMessage extends TypedMessage { /** * 文类类型消息 * @extends TypedMessage * @param {String} [text=''] * @throws {TypeError} text 不是 String 类型 */ constructor(text = '') { if (typeof text !== 'string') { throw new TypeError(`${text} is not a string`); } super(); this.setText(text); } }) || _class$5) || _class$5); /** * @name TYPE * @memberof TextMessage * @type Number * @static * @const */ var _class$6; const debug$c = d('LC:MessageParser'); const tryParseJson = (target, key, descriptor) => { const fn = descriptor.value; // eslint-disable-next-line no-param-reassign descriptor.value = function wrapper(param) { let content; if (typeof param !== 'string') { content = param; } else { try { content = JSON.parse(param); } catch (error) { content = param; } } return fn.call(this, content); }; }; const applyPlugins = (target, key, descriptor) => { const fn = descriptor.value; // eslint-disable-next-line no-param-reassign descriptor.value = function wrapper(json) { return Promise.resolve(json).then(applyMiddlewares(this._plugins.beforeMessageParse)).then(decoratedJson => fn.call(this, decoratedJson)).then(applyMiddlewares(this._plugins.afterMessageParse)); }; }; let MessageParser = (_class$6 = class MessageParser { /** * 消息解析器 * @param {Object} plugins 插件,插件的 messageClasses 会自动被注册,在解析时 beforeMessageParse 与 afterMessageParse Middleware 会被应用。 */ constructor(plugins = {}) { this._plugins = plugins; this._messageClasses = []; this.register(plugins.messageClasses); } /** * 注册消息类 * * @param {Function | Function[]} messageClass 消息类,需要实现 {@link AVMessage} 接口, * 建议继承自 {@link TypedMessage},也可以传入一个消息类数组。 * @throws {TypeError} 如果 messageClass 没有实现 {@link AVMessage} 接口则抛出异常 */ register(messageClasses) { ensureArray(messageClasses).map(klass => this._register(klass)); } _register(messageClass) { if (messageClass && messageClass.parse && messageClass.prototype && messageClass.prototype.getPayload) { this._messageClasses.unshift(messageClass); } else { throw new TypeError('Invalid messageClass'); } } // jsdoc-ignore-start // jsdoc-ignore-end /** * 解析消息内容 * @param {Object | string | any} target 消息内容,如果是字符串会尝试 parse 为 JSON。 * @return {AVMessage} 解析后的消息 * @throws {Error} 如果不匹配任何注册的消息则抛出异常 */ parse(content) { debug$c('parsing message: %O', content); // eslint-disable-next-line for (const Klass of this._messageClasses) { const contentCopy = isPlainObject(content) ? { ...content } : content; let valid; let result; try { valid = Klass.validate(contentCopy); } catch (error) {// eslint-disable-line no-empty } if (valid) { try { result = Klass.parse(contentCopy); } catch (error) { console.warn('parsing a valid message content error', { error, Klass, content: contentCopy }); } if (result !== undefined) { debug$c('parse result: %O', result); return result; } } } throw new Error('No Message Class matched'); } }, (_applyDecoratedDescriptor(_class$6.prototype, "parse", [tryParseJson, applyPlugins], Object.getOwnPropertyDescriptor(_class$6.prototype, "parse"), _class$6.prototype)), _class$6); /** @module leancloud-realtime */ const debug$d = d('LC:IMPlugin'); /** * 消息优先级枚举 * @enum {Number} * @since 3.3.0 */ const MessagePriority = { /** 高 */ HIGH: 1, /** 普通 */ NORMAL: 2, /** 低 */ LOW: 3 }; Object.freeze(MessagePriority); /** * 为 Conversation 定义一个新属性 * @param {String} prop 属性名 * @param {Object} [descriptor] 属性的描述符,参见 {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor#Description getOwnPropertyDescriptor#Description - MDN},默认为该属性名对应的 Conversation 自定义属性的 getter/setter * @returns void * @example * * conversation.get('type'); * conversation.set('type', 1); * * // equals to * defineConversationProperty('type'); * conversation.type; * conversation.type = 1; */ const defineConversationProperty = (prop, descriptor = { get() { return this.get(prop); }, set(value) { this.set(prop, value); } }) => { Object.defineProperty(Conversation.prototype, prop, descriptor); }; const onRealtimeCreate = realtime => { /* eslint-disable no-param-reassign */ const deviceId = uuid(); realtime._IMClients = {}; realtime._IMClientsCreationCount = 0; const messageParser = new MessageParser(realtime._plugins); realtime._messageParser = messageParser; const signAVUser = async user => realtime._request({ method: 'POST', path: '/rtm/sign', data: { session_token: user.getSessionToken() } }); /** * 注册消息类 * * 在接收消息、查询消息时,会按照消息类注册顺序的逆序依次尝试解析消息内容 * * @memberof Realtime * @instance * @param {Function | Function[]} messageClass 消息类,需要实现 {@link AVMessage} 接口, * 建议继承自 {@link TypedMessage} * @throws {TypeError} 如果 messageClass 没有实现 {@link AVMessage} 接口则抛出异常 */ const register = messageParser.register.bind(messageParser); /** * 创建一个即时通讯客户端,多次创建相同 id 的客户端会返回同一个实例 * @memberof Realtime * @instance * @param {String|AV.User} [identity] 客户端 identity,如果不指定该参数,服务端会随机生成一个字符串作为 identity, * 如果传入一个已登录的 AV.User,则会使用该用户的 id 作为客户端 identity 登录。 * @param {Object} [options] * @param {Function} [options.signatureFactory] open session 时的签名方法 // TODO need details * @param {Function} [options.conversationSignatureFactory] 对话创建、增减成员操作时的签名方法 * @param {Function} [options.blacklistSignatureFactory] 黑名单操作时的签名方法 * @param {String} [options.tag] 客户端类型标记,以支持单点登录功能 * @param {String} [options.isReconnect=false] 单点登录时标记该次登录是不是应用启动时自动重新登录 * @return {Promise. } */ const createIMClient = async (identity, { tag, isReconnect, ...clientOptions } = {}, lagecyTag) => { let id; const buildinOptions = {}; if (identity) { if (typeof identity === 'string') { id = identity; } else if (identity.id && identity.getSessionToken) { ({ id } = identity); const sessionToken = identity.getSessionToken(); if (!sessionToken) { throw new Error('User must be authenticated'); } buildinOptions.signatureFactory = signAVUser; } else { throw new TypeError('Identity must be a String or an AV.User'); } if (realtime._IMClients[id] !== undefined) { return realtime._IMClients[id]; } } if (lagecyTag) { console.warn('DEPRECATION createIMClient tag param: Use options.tag instead.'); } const _tag = tag || lagecyTag; const promise = realtime._open().then(connection => { const client = new IMClient(id, { ...buildinOptions, ...clientOptions }, { _connection: connection, _request: realtime._request.bind(realtime), _messageParser: messageParser, _plugins: realtime._plugins, _identity: identity }); connection.on(RECONNECT, () => client._open(realtime._options.appId, _tag, deviceId, true) /** * 客户端连接恢复正常,该事件通常在 {@link Realtime#event:RECONNECT} 之后发生 * @event IMClient#RECONNECT * @see Realtime#event:RECONNECT * @since 3.2.0 */ /** * 客户端重新登录发生错误(网络连接已恢复,但重新登录错误) * @event IMClient#RECONNECT_ERROR * @since 3.2.0 */ .then(() => client.emit(RECONNECT), error => client.emit(RECONNECT_ERROR, error))); internal(client)._eventemitter.on('beforeclose', () => { delete realtime._IMClients[client.id]; if (realtime._firstIMClient === client) { delete realtime._firstIMClient; } }, realtime); internal(client)._eventemitter.on('close', () => { realtime._deregister(client); }, realtime); return client._open(realtime._options.appId, _tag, deviceId, isReconnect).then(() => { realtime._IMClients[client.id] = client; realtime._IMClientsCreationCount += 1; if (realtime._IMClientsCreationCount === 1) { client._omitPeerId(true); realtime._firstIMClient = client; } else if (realtime._IMClientsCreationCount > 1 && realtime._firstIMClient) { realtime._firstIMClient._omitPeerId(false); } realtime._register(client); return client; }).catch(error => { delete realtime._IMClients[client.id]; throw error; }); }).then(...finalize(() => { realtime._deregisterPending(promise); })).catch(error => { delete realtime._IMClients[id]; throw error; }); if (identity) { realtime._IMClients[id] = promise; } realtime._registerPending(promise); return promise; }; Object.assign(realtime, { register, createIMClient }); /* eslint-enable no-param-reassign */ }; const beforeCommandDispatch = (command, realtime) => { const isIMCommand = command.service === null || command.service === 2; if (!isIMCommand) return true; const targetClient = command.peerId ? realtime._IMClients[command.peerId] : realtime._firstIMClient; if (targetClient) { Promise.resolve(targetClient).then(client => client._dispatchCommand(command)).catch(debug$d); } else { debug$d('[WARN] Unexpected message received without any live client match: %O', trim(command)); } return false; }; const IMPlugin = { name: 'leancloud-realtime-plugin-im', onRealtimeCreate, beforeCommandDispatch, messageClasses: [Message, BinaryMessage, RecalledMessage, TextMessage] }; /** @module leancloud-realtime */ Realtime.defineConversationProperty = defineConversationProperty; Realtime.__preRegisteredPlugins = [IMPlugin]; const Event = { ...CoreEvent, ...IMEvent }; /** core + plugins + platform adapters */ setAdapters({ WebSocket: platformAdaptersNode.WebSocket, request: platformAdaptersNode.request }); exports.EventEmitter = EventEmitter; exports.BinaryMessage = BinaryMessage; exports.ChatRoom = ChatRoom; exports.Conversation = Conversation; exports.ConversationMemberRole = ConversationMemberRole; exports.ConversationQuery = ConversationQuery; exports.ErrorCode = ErrorCode; exports.Event = Event; exports.IE10Compatible = IE10Compatible; exports.IMPlugin = IMPlugin; exports.Message = Message; exports.MessageParser = MessageParser; exports.MessagePriority = MessagePriority; exports.MessageQueryDirection = MessageQueryDirection; exports.MessageStatus = MessageStatus; exports.Promise = polyfilledPromise; exports.Protocals = message; exports.Protocols = message; exports.Realtime = Realtime; exports.RecalledMessage = RecalledMessage; exports.ServiceConversation = ServiceConversation; exports.TemporaryConversation = TemporaryConversation; exports.TextMessage = TextMessage; exports.TypedMessage = TypedMessage; exports.debug = debug$2; exports.defineConversationProperty = defineConversationProperty; exports.getAdapter = getAdapter; exports.messageField = messageField; exports.messageType = messageType; exports.setAdapters = setAdapters; //# sourceMappingURL=im-node.js.map