leancloud-realtime
Version:
LeanCloud Realtime Message JavaScript SDK
8,194 lines • 201 kB
JavaScript
'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;
}
/**
* 解析处理消息内容
* <pre>
* 如果子类提供了 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();
}
/**
* 在客户端需要以文本形式展示该消息时显示的文案,
* 如 <code>[红包] 新春快乐</code>。
* 默认值为消息的 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()
};
}
/**
* 解析处理消息内容
* <pre>
* 为给定的 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 {
/**
* 在客户端需要以文本形式展示该消息时显示的文案,值为 <code>[该消息已撤回]</code>
* @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.<Message>} 发送的消息
*/
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.<Number>}
*/
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.<AVMessage>} 更新后的消息
*/
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.<RecalledMessage>} 一条已撤回的消息
*/
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.<Message[]>} 消息列表
*/
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.<Promise.<IteratorResult<Message[]>>>} [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.<this>} 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>} 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.<this>} 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.<this>} self
*/
async fetch() {
const query = this._client.getQuery().equalTo('objectId', this.id);
await query.find();
return this;
}
/**
* 静音,客户端拒绝收到服务器端的离线推送通知
* @return {Promise.<this>} 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.<this>} 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.<PartiallySuccess>} 部分成功结果,包含了成功的 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.<PartiallySuccess>} 部分成功结果,包含了成功的 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.<this>} self
*/
async join() {
this._debug('join');
return this.add(this._client.id).then(({
failures
}) => {
if (failures[0]) throw failures[0];
return this;
});
}
/**
* (当前用户)退出该对话
* @return {Promise.<this>} 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.<PartiallySuccess>} 部分成功结果,包含了成功的 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.<PartiallySuccess>} 部分成功结果,包含了成功的 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.<string>} 查询结果。其中的 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.<PartiallySuccess>} 部分成功结果,包含了成功的 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.<PartiallySuccess>} 部分成功结果,包含了成功的 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.<string>} 查询结果。其中的 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.<ConversationMemberInfo[]>} 所有成员的对话属性列表
*/
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.<ConversationMemberInfo>} 指定成员的对话属性
*/
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.<this>} 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.<this>} self
*/
async subscribe() {
return this.join();
}
/**
* 退订该服务号
* @return {Promise.<this>} 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.<ConversationBase[]>}
*/
async find() {
return this._client._executeQuery(this);
}
/**
* 返回符合条件的第一个结果
* @return {Promise.<ConversationBase>}
*/
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.<String[]>} 在线的 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.<ConversationBase>} 如果 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.<ConversationBase[]>} 如果 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.<PersistentConversation>}
*/
getQuery() {
return new ConversationQuery(this);
}
/**
* 构造一个 ConversationQuery 来查询聊天室
* @return {ConversationQuery.<ChatRoom>}
*/
getChatRoomQuery() {
return this.getQuery().equalTo('tr', true);
}
/**
* 构造一个 ConversationQuery 来查询服务号
* @return {ConversationQuery.<ServiceConversation>}
*/
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.<Conversation>}
*/
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.<ChatRoom>}
*/
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.<TemporaryConversation>}
*/
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.<IMClient>}
*/
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