Commit b1a9feea authored by chenqikuai's avatar chenqikuai

update

parent 2240f3d0
...@@ -144,3 +144,6 @@ export const target = `${query.to}` || 'null-token' ...@@ -144,3 +144,6 @@ export const target = `${query.to}` || 'null-token'
// export const target = `${query.orderid}_${token}` || 'null-orderid' // export const target = `${query.orderid}_${token}` || 'null-orderid'
/** orderid */ /** orderid */
export const orderid = query.orderid || 'null-orderid' export const orderid = query.orderid || 'null-orderid'
export const publicKey = query.publicKey
export const privateKey = query.privateKey
// protoc -I=. -I=$GOPATH/src --go_out=plugins=grpc:. *.proto
syntax = "proto3"; syntax = "proto3";
package dtalk.proto; package dtalk.proto;
option go_package = ".;proto"; option go_package = "proto";
enum Device { enum Device {
Android = 0; Android = 0;
...@@ -13,11 +14,31 @@ message Login { ...@@ -13,11 +14,31 @@ message Login {
string username = 2; string username = 2;
} }
// event define
enum EventType {
commonMsg = 0;
commonMsgAck = 1;
Notice = 2;
}
message Proto { message Proto {
EventType eventType = 1; EventType eventType = 1;
bytes body = 2; bytes body = 2;
} }
// common msg define
enum MsgType {
System = 0;
Text = 1;
Audio = 2;
Image = 3;
Video = 4;
File = 5;
Card = 6;
Alert = 7;
Forward = 8;
}
message CommonMsg { message CommonMsg {
int32 channelType = 1; int32 channelType = 1;
int64 logId = 2; int64 logId = 2;
...@@ -27,21 +48,18 @@ message CommonMsg { ...@@ -27,21 +48,18 @@ message CommonMsg {
int32 msgType = 6; int32 msgType = 6;
bytes msg = 7; bytes msg = 7;
uint64 datetime = 8; uint64 datetime = 8;
Source source = 9;
} }
enum EventType { message Source {
commonMsg = 0; int32 channelType=1;
commonMsgAck = 1; SourceUser from=2;
SourceUser target=3;
} }
enum MsgType { message SourceUser {
System = 0; string id=1;
Text = 1; string name=2;
Audio = 2;
Image = 3;
Video = 4;
File = 5;
Card = 6;
} }
message CommonMsgAck { message CommonMsgAck {
...@@ -49,6 +67,10 @@ message CommonMsgAck { ...@@ -49,6 +67,10 @@ message CommonMsgAck {
uint64 datetime = 8; uint64 datetime = 8;
} }
message EncryptMsg {
string content = 1;
}
message TextMsg { message TextMsg {
string content = 1; string content = 1;
} }
...@@ -83,3 +105,183 @@ message CardMsg { ...@@ -83,3 +105,183 @@ message CardMsg {
string name = 2; string name = 2;
string account = 3; string account = 3;
} }
message AlertMsg {
AlertType type = 1;
bytes body = 2;
}
message ForwardMsg {
repeated ForwardItem items = 1;
}
message ForwardItem {
string avatar=1;
string name=2;
int32 msgType=3;
bytes msg=4;
uint64 datetime=5;
}
enum AlertType {
UpdateGroupNameAlert = 0;
SignInGroupAlert = 1;
SignOutGroupAlert = 2;
KickOutGroupAlert = 3;
DeleteGroupAlert = 4;
UpdateGroupMutedAlert = 5;
UpdateGroupMemberMutedAlert = 6;
UpdateGroupOwnerAlert = 7;
}
message AlertUpdateGroupName {
int64 group = 1;
string operator = 2;
string name = 3;
}
message AlertSignInGroup {
int64 group = 1;
string inviter = 2;
repeated string members = 3;
}
message AlertSignOutGroup {
int64 group = 1;
string operator = 2;
}
message AlertKickOutGroup {
int64 group = 1;
string operator = 2;
repeated string members = 3;
}
message AlertDeleteGroup {
int64 group = 1;
string operator = 2;
}
message AlertUpdateGroupMuted {
int64 group = 1;
string operator = 2;
proto.MuteType type = 3;
}
message AlertUpdateGroupMemberMutedTime {
int64 group = 1;
string operator = 2;
repeated string members = 3;
}
message AlertUpdateGroupOwner {
int64 group = 1;
string newOwner = 2;
}
//alert msg define
message NotifyMsg {
ActionType action = 1;
bytes body = 2;
}
enum ActionType {
Received = 0;
SignInGroup = 10;
SignOutGroup = 11;
DeleteGroup = 12;
//
UpdateGroupJoinType = 20;
UpdateGroupFriendType = 21;
UpdateGroupMuteType = 22;
UpdateGroupMemberType = 23;
UpdateGroupMemberMuteTime = 24;
UpdateGroupName = 25;
UpdateGroupAvatar = 26;
}
message ActionReceived {
repeated uint64 logs = 1;
}
message ActionSignInGroup {
repeated string uid = 1;
int64 group = 2;
uint64 time = 3;
}
message ActionSignOutGroup {
repeated string uid = 1;
int64 group = 2;
uint64 time = 3;
}
message ActionDeleteGroup {
int64 group = 1;
uint64 time = 2;
}
enum JoinType {
JoinAllow = 0;
JoinDeny = 1;
}
message ActionUpdateGroupJoinType {
int64 group = 1;
JoinType type = 2;
uint64 time = 3;
}
enum FriendType {
FriendAllow = 0;
FriendDeny = 1;
}
message ActionUpdateGroupFriendType {
int64 group = 1;
FriendType type = 2;
uint64 time = 3;
}
enum MuteType {
MuteAllow = 0;
MuteDeny = 1;
}
message ActionUpdateGroupMuteType {
int64 group = 1;
MuteType type = 2;
uint64 time = 3;
}
enum MemberType {
Normal = 0;
Admin = 1;
Owner = 2;
}
message ActionUpdateGroupMemberType {
int64 group = 1;
string uid = 2;
MemberType type = 3;
uint64 time = 4;
}
message ActionUpdateGroupMemberMuteTime {
int64 group = 1;
repeated string uid = 2;
int64 muteTime = 3;
uint64 time = 4;
}
message ActionUpdateGroupName {
int64 group = 1;
string name = 2;
uint64 time = 3;
}
message ActionUpdateGroupAvatar {
int64 group = 1;
string avatar = 2;
uint64 time = 3;
}
...@@ -12,9 +12,15 @@ interface DecodedMessage { ...@@ -12,9 +12,15 @@ interface DecodedMessage {
orderid?: string orderid?: string
} }
export default (data: Uint8Array): DecodedMessage => { export default (data: Uint8Array): DecodedMessage | null => {
const commonMsg: dtalk.proto.ICommonMsg = dtalk.proto.CommonMsg.decode(data) const protoMsg = dtalk.proto.Proto.decode(data)
console.log(commonMsg)
if (protoMsg.eventType === 2) {
return null // temporary workaround
}
const commonMsg: dtalk.proto.ICommonMsg = dtalk.proto.CommonMsg.decode(protoMsg.body)
let content: MessageContent let content: MessageContent
const TextMsg = dtalk.proto.TextMsg const TextMsg = dtalk.proto.TextMsg
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -87,7 +87,6 @@ export default class FzmMessageProtocolConnection { ...@@ -87,7 +87,6 @@ export default class FzmMessageProtocolConnection {
console.log('WebSocket: 收到新消息, 对方的 seq: ' + (responseAck == 0 ? '离线消息' : responseAck)) console.log('WebSocket: 收到新消息, 对方的 seq: ' + (responseAck == 0 ? '离线消息' : responseAck))
const msgData = encodeMessage(null, FzmMessageTypes.ReceiveMessageResponse, this.seq, responseSeq) const msgData = encodeMessage(null, FzmMessageTypes.ReceiveMessageResponse, this.seq, responseSeq)
this.webSocket.send(msgData) this.webSocket.send(msgData)
this.onReceiveMessage && this.onReceiveMessage(response.body) this.onReceiveMessage && this.onReceiveMessage(response.body)
} }
......
import base64 from 'base64-js' import base64 from 'base64-js'
import { sign } from './sign' import { sign } from './sign'
import { hexToArray } from 'enc-utils'
let latestSig = '' let latestSig = ''
let forAccount = '' let forAccount = ''
...@@ -20,10 +21,7 @@ export function generateToken(account: { ...@@ -20,10 +21,7 @@ export function generateToken(account: {
return latestSig return latestSig
const message = String(Date.now()) + '*' const message = String(Date.now()) + '*'
const signature = sign( const signature = sign(message, hexToArray(account.privateKeyHex))
message,
new TextEncoder().encode(account.privateKeyHex),
)
const base64Signature = base64.fromByteArray(signature) const base64Signature = base64.fromByteArray(signature)
updateTime = Date.now() updateTime = Date.now()
forAccount = account.address forAccount = account.address
......
import Compressor from 'compressorjs'
const defaultOptions = {
maxWidth: 1920,
maxHeight: 1920,
quality: 1,
convertSize: 500000,
}
Compressor.setDefaults(defaultOptions)
/**
* 简单封装了 compressorjs,来压缩用户试图上传的图片。
* @param file 图片文件
* @param options 配置参数
* @param callback 成功回调函数
* @param errorCallback 失败回调函数
* 参考 https://github.com/fengyuanchen/compressorjs/blob/master/README.md
*/
export default function (file: Blob): Promise<Blob> {
return new Promise((resolve, reject) => {
new Compressor(file, {
success: (result) => {
resolve(result)
},
error: (result) => {
reject(result)
},
})
})
}
syntax = "proto3";
package dtalk.proto;
option go_package = ".;proto";
enum Device {
Android = 0;
IOS = 1;
}
message Login {
Device device = 1;
string username = 2;
}
message Proto {
EventType eventType = 1;
bytes body = 2;
}
message CommonMsg {
int32 channelType = 1;
int64 logId = 2;
string msgId = 3;
string from = 4;
string target = 5;
int32 msgType = 6;
bytes msg = 7;
uint64 datetime = 8;
}
enum EventType {
commonMsg = 0;
commonMsgAck = 1;
}
enum MsgType {
System = 0;
Text = 1;
Audio = 2;
Image = 3;
Video = 4;
File = 5;
Card = 6;
}
message CommonMsgAck {
int64 logId = 2;
uint64 datetime = 8;
}
message TextMsg {
string content = 1;
}
message AudioMsg {
string mediaUrl = 1;
int32 time = 2;
}
message ImageMsg {
string mediaUrl = 1;
int32 height = 2;
int32 width = 3;
}
message VideoMsg {
string mediaUrl = 1;
int32 time = 2;
int32 height = 3;
int32 width = 4;
}
message FileMsg {
string mediaUrl = 1;
string name = 2;
string md5 = 3;
int64 size = 4;
}
message CardMsg {
string bank = 1;
string name = 2;
string account = 3;
}
import { dtalk } from './protobuf'
import { MessageContent } from '@/types/chat-message'
interface DecodedMessage {
content: MessageContent
from: string
uuid: string
type: dtalk.proto.MsgType
datetime: number
logid: string
/** 收到的 target 值是该笔订单 id */
orderid?: string
}
export default (data: Uint8Array): DecodedMessage => {
const commonMsg: dtalk.proto.ICommonMsg = dtalk.proto.CommonMsg.decode(data)
console.log(commonMsg)
let content: MessageContent
const TextMsg = dtalk.proto.TextMsg
const AudioMsg = dtalk.proto.AudioMsg
const CardMsg = dtalk.proto.CardMsg
const ImageMsg = dtalk.proto.ImageMsg
const VideoMsg = dtalk.proto.VideoMsg
switch (commonMsg.msgType) {
case 1:
content = TextMsg.toObject(TextMsg.decode(commonMsg.msg || new Uint8Array()))
break
case 2:
content = AudioMsg.toObject(AudioMsg.decode(commonMsg.msg || new Uint8Array()))
break
case 3:
content = ImageMsg.toObject(ImageMsg.decode(commonMsg.msg || new Uint8Array()))
break
case 4:
content = VideoMsg.toObject(VideoMsg.decode(commonMsg.msg || new Uint8Array()))
break
case 6:
content = CardMsg.toObject(CardMsg.decode(commonMsg.msg || new Uint8Array()))
break
default:
throw '解码消息时发现未知的消息类型:' + commonMsg.msgType
}
if (!commonMsg.from || !commonMsg.msgType || !commonMsg.target) {
throw '解码消息时发现空字段,理论上不可能出现这种情况'
}
return {
content,
from: commonMsg.from,
uuid: JSON.stringify(commonMsg.logId),
type: commonMsg.msgType,
datetime: commonMsg.datetime,
logid: commonMsg.logId,
orderid: commonMsg.target,
}
}
import { ChatMessageTypes } from '@/types/chatMessageTypes'
import { MessageContent } from '@/types/chat-message'
import { dtalk } from './protobuf'
/**
* 编码消息需要传的参数
*/
interface ChatMessageEncoderArgs {
/** 发送者 */
from: string
/** 接收者 */
target: string
/** 消息类型 */
msgType: ChatMessageTypes
/** 消息内容 */
msg: MessageContent
/** 消息的全数据库唯一 id */
uuid: string
}
export default (msg: ChatMessageEncoderArgs): Uint8Array => {
let content
switch (msg.msgType) {
case ChatMessageTypes.Text:
content = dtalk.proto.TextMsg.encode({
content: (msg.msg as dtalk.proto.ITextMsg).content,
}).finish()
break
case ChatMessageTypes.Audio:
content = dtalk.proto.AudioMsg.encode({
mediaUrl: (msg.msg as dtalk.proto.AudioMsg).mediaUrl,
time: (msg.msg as dtalk.proto.AudioMsg).time,
}).finish()
break
case ChatMessageTypes.Image:
content = dtalk.proto.ImageMsg.encode({
mediaUrl: (msg.msg as dtalk.proto.ImageMsg).mediaUrl,
width: (msg.msg as dtalk.proto.ImageMsg).width,
height: (msg.msg as dtalk.proto.ImageMsg).height,
}).finish()
break
case ChatMessageTypes.Video:
content = dtalk.proto.VideoMsg.encode({
mediaUrl: (msg.msg as dtalk.proto.IVideoMsg).mediaUrl,
width: (msg.msg as dtalk.proto.IVideoMsg).width,
height: (msg.msg as dtalk.proto.IVideoMsg).height,
time: (msg.msg as dtalk.proto.IVideoMsg).time,
}).finish()
break
case ChatMessageTypes.Card:
content = dtalk.proto.CardMsg.encode({
bank: (msg.msg as dtalk.proto.CardMsg).bank,
name: (msg.msg as dtalk.proto.CardMsg).name,
account: (msg.msg as dtalk.proto.CardMsg).account,
}).finish()
break
default:
throw '未知的消息类型:' + msg.msgType
}
const body: dtalk.proto.ICommonMsg = {
channelType: 0,
logId: 0,
msgId: msg.uuid,
from: msg.from,
target: msg.target,
msgType: msg.msgType,
msg: content,
datetime: Date.now(),
}
console.log(body);
const bodyData = dtalk.proto.CommonMsg.encode(body).finish()
const container = {
eventType: 0,
body: bodyData,
}
return dtalk.proto.Proto.encode(container).finish()
}
This source diff could not be displayed because it is too large. You can view the blob instead.
import decodeMessage from './decodeMessage'
import encodeMessage from './encodeMessage'
import { FzmMessageTypes } from './FzmMessageTypes'
interface IPendingMessage {
seq: number
uuid?: string
type: FzmMessageTypes
resolve?: () => void
reject?: (reason?: string) => void
timeout?: NodeJS.Timeout
}
class PendingMessage implements IPendingMessage {
seq: number
uuid?: string
type: FzmMessageTypes
resolve?: () => void
reject?: (reason?: string) => void
timeout?: NodeJS.Timeout
constructor(seq: number, type: FzmMessageTypes, uuid: string) {
this.seq = seq
this.type = type
this.uuid = uuid
}
}
export default class FzmMessageProtocolConnection {
private webSocket: WebSocket
onReceiveMessage?: (messageBody: Uint8Array) => void
onLoseConnection?: () => void
seq: number
// 心跳
private heartBeatInterval: number
private timer: NodeJS.Timeout
// 调试模式
private debug: boolean
// 消息发送队列
queue: PendingMessage[]
constructor(ws: WebSocket) {
console.log('WebSocket: 已连接')
this.queue = [] as PendingMessage[]
this.seq = 0
this.debug = process.env.NODE_ENV !== 'production'
this.webSocket = ws
// 定时发送心跳
this.heartBeatInterval = 15 // 单位秒;后端目前设置 4 分钟超时
this.timer = setInterval(() => {
// 下次将要发送心跳前,队列中尚有未发送成功的心跳,则表明断线
if (this.queue.some((item) => item.type === FzmMessageTypes.HeartBeat)) {
console.log('WebSocket: 失去与服务器的连接')
this.onLoseConnection && this.onLoseConnection()
this.disconnect()
} else {
this.sendHeartBeat()
}
}, this.heartBeatInterval * 1000)
// 监听 webSocket 收到消息
this.webSocket.onmessage = (event) => {
decodeMessage(event.data).then((response) => {
const responseType = response.header.operation
const responseSeq = response.header.seq
const responseAck = response.header.ack
// 答复类型:心跳答复
if (responseType === FzmMessageTypes.HeartBeatResponse) {
// 从队列中剔除
this.queue.splice(
this.queue.findIndex((i) => i.seq == responseSeq),
1
)
}
// 接收到对方用户发来的消息,回复“收到”
else if (responseType === FzmMessageTypes.ReceiveMessage) {
// 发送确认接收到消息的响应
this.seq++
console.log('WebSocket: 收到新消息, 对方的 seq: ' + (responseAck == 0 ? '离线消息' : responseAck))
const msgData = encodeMessage(null, FzmMessageTypes.ReceiveMessageResponse, this.seq, responseSeq)
this.webSocket.send(msgData)
this.onReceiveMessage && this.onReceiveMessage(response.body)
}
// 接收到本用户发送消息成功的确认
else if (responseType === FzmMessageTypes.SendMessageResponse) {
console.log(`WebSocket: 发送消息成功, seq: ${responseSeq}`)
const queueItem = this.queue.find((i) => i.seq == responseAck) as IPendingMessage
queueItem.resolve && queueItem.resolve()
if (queueItem.timeout) clearTimeout(queueItem.timeout)
// 从队列中剔除
this.queue.splice(
this.queue.findIndex((i) => i.seq == responseAck),
1
)
}
})
}
this.webSocket.onclose = () => {
console.log('WebSocket: 连接已关闭')
this.queue.forEach((m) => m.reject && m.reject())
}
this.webSocket.onerror = (event) => {
console.log(event)
this.queue.forEach((m) => m.reject && m.reject())
}
}
/** WebSocket 状态 */
get readyState(): number {
return this.webSocket.readyState
}
/** WebSocket 连接的 url */
get url(): string {
return this.webSocket.url
}
/** 主动断开连接 */
disconnect(): void {
this.seq++
if (this.debug) console.log(`WebSocket: 主动断开连接, seq: ${this.seq}`)
this.webSocket.send(encodeMessage(null, FzmMessageTypes.Disconnect, this.seq))
this.webSocket.close()
clearInterval(this.timer)
}
/**
* 发送消息,需将消息内容转换为 protobuf 格式的二进制
* @param msg 发送的消息,以及制定该条消息的唯一 id
* @param msg.body 消息对象
* @param msg.uuid 唯一id
* @returns promise
*/
sendMessage(msg: { body: Uint8Array; uuid: string }): Promise<void> {
return new Promise((resolve, reject) => {
if (this.debug) console.log(`发送消息, seq: ${this.seq}`)
const failedMessage = this.queue.find((m) => m.uuid === msg.uuid) // 是否为之前发送失败的消息
let pendingMessage: IPendingMessage
if (failedMessage) {
pendingMessage = failedMessage
} else {
this.seq++
pendingMessage = new PendingMessage(this.seq, FzmMessageTypes.SendMessage, msg.uuid)
this.queue.push(pendingMessage)
}
pendingMessage.resolve = resolve
pendingMessage.reject = reject
this.webSocket.send(encodeMessage(msg.body, FzmMessageTypes.SendMessage, pendingMessage.seq))
})
}
private sendHeartBeat(): void {
this.seq++
if (this.debug) console.log(`WebSocket: 发送心跳, seq: ${this.seq}`)
this.queue.push({ seq: this.seq, type: FzmMessageTypes.HeartBeat })
this.webSocket.send(encodeMessage(null, FzmMessageTypes.HeartBeat, this.seq))
}
}
// header 长度,用于解码
export const header = {
packageLength: 4,
headerLength: 2,
ver: 2,
operation: 4,
seq: 4,
ack: 4,
}
// 用于编码
export default class FzmMessageProtocolHeader {
header: { [headerProp: string]: { value: number; length: number } }
constructor(messageType: number, seq: number, ack: number) {
this.header = {
packageLength: { value: 0, length: 4 },
headerLength: { value: 0, length: 2 },
ver: { value: 1, length: 2 },
operation: { value: messageType, length: 4 },
seq: { value: seq, length: 4 },
ack: { value: ack, length: 4 },
}
// 重新计算 headerLength
this.header.headerLength.value = Object.values(this.header).reduce((prev, curr) => {
return prev + curr.length
}, 0)
}
/** 生成 body 后一定要拿着 body 调用一次该方法,否则包长度无法获得! */
updatePackageLength(bodyData: Uint8Array): void {
this.header.packageLength.value = this.header.headerLength.value + bodyData.length
}
}
export enum FzmMessageTypes {
Auth = 1,
AuthResponse,
HeartBeat,
HeartBeatResponse,
Disconnect,
DisconnectResponse,
SendMessage,
SendMessageResponse,
ReceiveMessage,
ReceiveMessageResponse,
}
# protobufjs 笔记
> Google Protocol Buffer (简称 Protobuf) 是一种轻便高效的结构化数据存储格式,平台无关、语言无关、可扩展,可用于通讯协议和数据存储等领域。
`protobufjs` 是谷歌官方提供的,用于在 JavaScript 中使用 `Protobuf`
如果你还不了解 protobufjs,推荐先阅读[这篇文章](https://juejin.cn/post/6844903699458818062)
## 使用 `pbjs`
本目录下的 `protobuf.js``protobuf.d.ts``protobufjs` 自带的命令行工具 `pbjs` 生成,源文件是目录下的 `comet.proto`
执行下面的命令之前先确保局部安装了 [protobufjs](https://www.npmjs.com/package/protobufjs)
### 生成的 `.js` 文件
生成的 `.js` 文件可以将相应的数据结构构造成 `protobuf` 格式的二进制数据(二进制数据在 JavaScript 中使用 Uint8Array 类型表示)。
```shell
npx pbjs -t static-module -w es6 -o src/utils/fzm-message-protocol/protobuf.js src/utils/fzm-message-protocol/comet.proto
```
### 生成的 `.d.ts` 文件
生成的 `.d.ts` 文件使得 TypeScript 项目获得完整的代码提示。
```shell
npx pbts -o src/utils/fzm-message-protocol/protobuf.d.ts src/utils/fzm-message-protocol/protobuf.js
```
## 使用方法
例如有个 `proto 文件` 如下:
```proto
package demo.proto;
message Person {
string name = 1;
int32 age = 2;
}
```
其中第一行为包名,下面的 `message Proto {...}` 为一个数据结构。要在 TypeScript 中将对应的 Object 编码成 proto 格式的二进制流,只需要:
```TypeScript
import { demo } from '...'
const person = { name: '张三', age: 18 }
const dataEncoded = dtalk.proto.Person.encode(person).finish()
// dataEncoded 为 UInt8Array 类型,可以被解码回 `{ name: '张三', age: 18 }`
```
由于 protobuf 为我们生成了 `.d.ts` 文件,因此在书写代码时能获得智能提示:
![](http://image-hosting-service.oss-cn-hangzhou.aliyuncs.com/20210512_%E5%B1%8F%E5%B9%95%E5%BD%95%E5%88%B62021-05-12%2011.13.17.jpg)
上图中的 `dtalk.proto.Proto` 是一个 [Type](https://protobufjs.github.io/protobuf.js/Type.html), 可以直接对目标 Object 编码。
syntax = "proto3";
package chat33.comet;
message AuthMsg {
string appId = 1;
string token = 2;
bytes ext = 3; // 其它业务方可能需要的信息
}
import { arrayToNumber } from 'enc-utils'
import { header } from './FzmMessageProtocolHeader'
interface Header {
[headerProp: string]: number
}
const decode = (data: Uint8Array): { header: Header; body: Uint8Array } => {
// 先解析 header
let key: keyof typeof header
// 计算 header 中所有字段的开始位置
const startIndexesMap: number[] = []
Object.values(header).reduce((prev, curr, currIndex, arr) => {
let currValue: number
if (currIndex === 0) {
currValue = 0
} else {
currValue = arr[currIndex - 1]
}
startIndexesMap.push(prev + currValue)
return prev + currValue
}, 0)
// 计算 header 中所有字段的字段名、开始位置、结束位置
const headerPropsPointer: { propName: string; startIndex: number; endIndex: number }[] = []
let i = 0
for (key in header) {
const length = header[key]
headerPropsPointer.push({
propName: key,
startIndex: startIndexesMap[i],
endIndex: startIndexesMap[i] + length,
})
i++
}
// 计算 header 中所有字段的值
const headerObj: { [headerProp: string]: number } = {}
headerPropsPointer.forEach((prop) => {
Object.defineProperty(headerObj, prop.propName, {
value: arrayToNumber(data.slice(prop.startIndex, prop.endIndex)),
configurable: true,
enumerable: true,
writable: true,
})
})
// 拿到 body 部分
const bodyData = data.slice(headerObj.headerLength)
let body: Uint8Array
if (bodyData.length) {
body = bodyData
} else {
body = new Uint8Array()
}
return {
header: headerObj,
body,
}
}
export default (rawMsg: Blob): Promise<{ header: Header; body: Uint8Array }> => {
return new Promise((resolve) => {
rawMsg.arrayBuffer().then((bufferMsg: ArrayBuffer) => {
const msg = decode(new Uint8Array(bufferMsg))
resolve(msg)
})
})
}
// 兼容部分安卓无法调用 Blob.prototype.arrayBuffer
// https://gist.github.com/hanayashiki/8dac237671343e7f0b15de617b0051bd
;(function () {
File.prototype.arrayBuffer = File.prototype.arrayBuffer || myArrayBuffer
Blob.prototype.arrayBuffer = Blob.prototype.arrayBuffer || myArrayBuffer
function myArrayBuffer(this: Blob) {
// this: File or Blob
return new Promise((resolve) => {
const fr = new FileReader()
fr.onload = () => {
resolve(fr.result)
}
fr.readAsArrayBuffer(this)
})
}
})()
import { numberToArray, concatArrays } from 'enc-utils'
import { AuthMsg } from '.'
import FzmMessageProtocolHeader from './FzmMessageProtocolHeader'
import { FzmMessageTypes } from './FzmMessageTypes'
import { chat33 } from './protobuf'
/**
* 将一个数字转换为一定长度的二进制数据
* @param num 想转换的数字
* @param byteLength 转换后的二进制数据长度(单位为字节)。
* `byteLength` 不能小于 `num` 的字节长度。
* 例如输入的 num 为 256,而 256 用二进制表示为 `00000001 00000000`,因此 `num` 的字节长度为 2,即至少需要 2 个字节来表示。
* 因此 `byteLength` 不能为 1,只能等于或大于 2。
* @returns 长度为 `byteLength` 的二进制的 `num`
*/
const encodeNumberWithFixedLength = (num: number, byteLength: number): Uint8Array => {
const uint8arr = numberToArray(num)
if (uint8arr.length > byteLength) throw '输入的数字过大!'
if (uint8arr.length == byteLength) return uint8arr
const arr = Array.from(uint8arr)
while (arr.length < byteLength) {
arr.splice(0, 0, 0) // 在数组的最前面插入一个 0
}
return Uint8Array.from(arr)
}
export type MessageEncoderPayload = AuthMsg | Uint8Array | null
/** 对消息进行编码 */
export default (payload: MessageEncoderPayload, messageType: FzmMessageTypes, seq = 0, ack?: number): Uint8Array => {
// 1. 构造 body
let bodyData: Uint8Array
// 判断 `messageType`:
// 如果是初次连接鉴权, 则内部解析成二进制流;
// 如果是正常发送消息, 则直接接收二进制流。
if (payload && messageType === FzmMessageTypes.Auth) {
bodyData = chat33.comet.AuthMsg.encode(payload as AuthMsg).finish()
} else if (payload && messageType !== FzmMessageTypes.Auth) {
bodyData = payload as Uint8Array
} else {
bodyData = new Uint8Array()
}
// 2. 基于 body 构造对应的 header
const header = new FzmMessageProtocolHeader(messageType, seq, ack || 0)
header.updatePackageLength(bodyData)
// 3. 对 header 编码
const headerData = Object.values(header.header).reduce((prev, curr) => {
return concatArrays(prev, encodeNumberWithFixedLength(curr.value, curr.length))
}, new Uint8Array())
// 4. 拼接 header 和 body
const data = concatArrays(headerData, bodyData)
return data
}
import encodeMessage from './encodeMessage'
import FzmMessageProtocolConnection from './FzmMessageProtocolConnection'
import { FzmMessageTypes } from './FzmMessageTypes'
export interface AuthMsg {
appId: string
token: string
ext?: Uint8Array
}
export default class FzmMessageProtocol {
url: string
ws?: WebSocket
constructor(url: string) {
this.url = url
}
authorize(authMsg: AuthMsg): Promise<FzmMessageProtocolConnection> {
return new Promise((resolve, reject) => {
this.ws = new WebSocket(this.url)
this.ws.onopen = () => {
// 发送鉴权消息
this.ws?.send(encodeMessage(authMsg, FzmMessageTypes.Auth))
}
this.ws.onmessage = () => {
resolve(new FzmMessageProtocolConnection(this.ws as WebSocket))
}
this.ws.onclose = () => {
reject(`WebSocket 连接被服务器端关闭,这大概率是鉴权失败导致的`)
}
})
}
}
/* eslint-disable */
import * as $protobuf from 'protobufjs'
/** Namespace chat33. */
export namespace chat33 {
/** Namespace comet. */
namespace comet {
/** Properties of an AuthMsg. */
interface IAuthMsg {
/** AuthMsg appId */
appId?: string | null
/** AuthMsg token */
token?: string | null
/** AuthMsg ext */
ext?: Uint8Array | null
}
/** Represents an AuthMsg. */
class AuthMsg implements IAuthMsg {
/**
* Constructs a new AuthMsg.
* @param [properties] Properties to set
*/
constructor(properties?: chat33.comet.IAuthMsg)
/** AuthMsg appId. */
public appId: string
/** AuthMsg token. */
public token: string
/** AuthMsg ext. */
public ext: Uint8Array
/**
* Creates a new AuthMsg instance using the specified properties.
* @param [properties] Properties to set
* @returns AuthMsg instance
*/
public static create(properties?: chat33.comet.IAuthMsg): chat33.comet.AuthMsg
/**
* Encodes the specified AuthMsg message. Does not implicitly {@link chat33.comet.AuthMsg.verify|verify} messages.
* @param message AuthMsg message or plain object to encode
* @param [writer] Writer to encode to
* @returns Writer
*/
public static encode(message: chat33.comet.IAuthMsg, writer?: $protobuf.Writer): $protobuf.Writer
/**
* Encodes the specified AuthMsg message, length delimited. Does not implicitly {@link chat33.comet.AuthMsg.verify|verify} messages.
* @param message AuthMsg message or plain object to encode
* @param [writer] Writer to encode to
* @returns Writer
*/
public static encodeDelimited(message: chat33.comet.IAuthMsg, writer?: $protobuf.Writer): $protobuf.Writer
/**
* Decodes an AuthMsg message from the specified reader or buffer.
* @param reader Reader or buffer to decode from
* @param [length] Message length if known beforehand
* @returns AuthMsg
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
public static decode(reader: $protobuf.Reader | Uint8Array, length?: number): chat33.comet.AuthMsg
/**
* Decodes an AuthMsg message from the specified reader or buffer, length delimited.
* @param reader Reader or buffer to decode from
* @returns AuthMsg
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
public static decodeDelimited(reader: $protobuf.Reader | Uint8Array): chat33.comet.AuthMsg
/**
* Verifies an AuthMsg message.
* @param message Plain object to verify
* @returns `null` if valid, otherwise the reason why it is not
*/
public static verify(message: { [k: string]: any }): string | null
/**
* Creates an AuthMsg message from a plain object. Also converts values to their respective internal types.
* @param object Plain object
* @returns AuthMsg
*/
public static fromObject(object: { [k: string]: any }): chat33.comet.AuthMsg
/**
* Creates a plain object from an AuthMsg message. Also converts values to other types if specified.
* @param message AuthMsg
* @param [options] Conversion options
* @returns Plain object
*/
public static toObject(
message: chat33.comet.AuthMsg,
options?: $protobuf.IConversionOptions
): { [k: string]: any }
/**
* Converts this AuthMsg to JSON.
* @returns JSON object
*/
public toJSON(): { [k: string]: any }
}
}
}
This diff is collapsed.
/**
* 为满足业务需求,连接 WebSocket 时需要携带 ext 字段
* 在本文件计算
*/
import { from, OrderInfo } from '@/store/appCallerStore'
import { Platform } from 'quasar'
import { dtalk } from './fzm-message-protocol-chat/protobuf'
function getDevice() {
if (Platform.is.ios) {
return dtalk.proto.Device.IOS
} else if (Platform.is.android) {
return dtalk.proto.Device.Android
} else return null
}
/**
* 鉴权时需要携带的额外信息
*/
interface Ext {
device: dtalk.proto.Device | null
username: string
}
export default function (orderInfo: OrderInfo): Uint8Array {
const ext: Ext = {
device: getDevice(),
username: from == orderInfo.userZbId ? orderInfo.userNick : orderInfo.merchantNick,
}
const extData = dtalk.proto.Login.encode(ext).finish()
return extData
}
const protocol = '(((ws(s)?)|(http(s)?))\\:\\/\\/)'
const domain = '[a-zA-Z0-9_-]+'
const other = '([a-zA-Z/0-9$-/:-?{#-~!"^_`\\[\\]]+)?'
const ext = '(\\.' + other + ')'
const port = '(\\:[0-9]+)?'
const ip = '([a-zA-Z0-9]{4}:)+[a-zA-Z0-9]'
/**
* 检查一串字符串是否为有效的 url
* Checks if a string is a valid url
*
* @param {string} val - the string to check
* @param {object} [options] - defaults to `{}`
* - **options.requireProtocol** {boolean} - set to true if you only want URLs with a protocol to be considered valid. Defaults to `false`
* @returns {boolean} valid - `true` if *val* is a valid url, `false` otherwise
*
*/
export default function(val: string | undefined, options?: { requireProtocol?: boolean }): boolean {
if (!val) return false
if (!options) {
options = {}
}
const re = new RegExp(
'^' +
protocol +
(!options.requireProtocol ? '?' : '') +
'(' +
domain +
ext +
'|localhost|' +
ip +
')' +
port +
other +
'$'
)
return typeof val === 'string' && re.test(val)
}
declare module "@/utils/jsBridge"
\ No newline at end of file
/*eslint-disable*/
const ANDROID = 1
const IOS = 2
// app return callback response
// standard:
// { code: 0, error: 'error msg'}
const SUCCESS_CODE = 0
// const SUCCESS_RES = { code: SUCCESS_CODE, error: 'error msg' }
class jsBridge {
constructor(bridgeName = 'WebViewJavascriptBridge', bridgeScheme = 'https') {
this.bridgeName = bridgeName
this.bridgeScheme = bridgeScheme
let ua = navigator.userAgent || navigator.vendor || window.opera
// ua = "Mozilla/5.0 (Linux; Android 6.0.1; Redmi 4A Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/55.0.2883.91 Mobile Safari/537.36;wallet;1.2.9"
ua = ua.toLowerCase()
if (/android/.test(ua)) {
this.app = ANDROID
} else if (/(iphone|ipad|ipod|ios)/.test(ua)) {
this.app = IOS
}
// 1. 判断 是否在 钱包app 内
// this.app.isWallet = true/false
// 2. 记录 app 版本号
// ua = Mozilla/5.0 (Linux; Android 6.0.1; Redmi 4A Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/55.0.2883.91 Mobile Safari/537.36;wallet;1.2.9
// ua = Mozilla/5.0 (iPhone; CPU iPhone OS 11_2 like Mac OS X) AppleWebKit/604.4.7 (KHTML, like Gecko) Mobile/15C114;wallet;1.2.8
// this.app.version = '1.2.0'
this.registDefaultHandlerForApp()
}
app = null
appType = {
isWallet: false,
version: ''
}
/* 与ios交互 */
setupWebViewJavascriptBridge(callback) {
// WebViewJavascriptBridge 由native在注入
// https://github.com/marcuswestin/WebViewJavascriptBridge
if (window[this.bridgeName]) {
return callback(window[this.bridgeName]);
}
if (window.WVJBCallbacks) {
return window.WVJBCallbacks.push(callback);
}
window.WVJBCallbacks = [callback];
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = `https://__bridge_loaded__`;
document.documentElement.appendChild(WVJBIframe);
setTimeout(function () {
document.documentElement.removeChild(WVJBIframe);
}, 0)
}
/* 与android交互 */
getAndroidBridge() {
return new Promise(resolve => {
if (window[this.bridgeName]) {
resolve(window[this.bridgeName])
} else {
document.addEventListener(
'WebViewJavascriptBridgeReady', () => {
resolve(window[this.bridgeName])
},
false
)
}
})
}
/**
* 给ios注册方法 jsHandler需要返回一个promise
* @param {string} event
* @param {fun} jsHandler
* @return JSON对象
*/
registerHandlerForIOS(event, jsHandler) {
this.setupWebViewJavascriptBridge(function (bridge) {
bridge.registerHandler(event, (data2js, responseCallback = () => {}) => {
jsHandler(data2js).then(responseCallback)
})
})
}
/**
* 给安卓注册方法 jsHandler需要返回一个promise
* @param {string} event
* @param {fun} jsHandler
* @return JSON对象
*/
registerHandlerForAndroid(event, jsHandler) {
this.getAndroidBridge().then(bridge => {
bridge.registerHandler(event, (data2js, responseCallback = () => {}) => {
jsHandler(data2js).then(responseCallback)
})
})
}
/**
* 调用ios
* @param {string} event
* @param {object} params
* @param {fun} callback
* @return JSON对象
*/
callIOSHandler(event, params = {}) {
return new Promise(resolve => {
this.setupWebViewJavascriptBridge(function (bridge) {
bridge.callHandler(event, params, resolve)
})
})
}
/**
* 调用android
* @param {string} event
* @param {object} params
* @param {fun} callback
* @return JSON对象
*/
callAndroidHandler(event, params = {}) {
return new Promise(resolve => {
this.getAndroidBridge().then(bridge => {
bridge.callHandler(event, params, (res) => {
if (typeof res === "string" && /{|\[/.test(res)) {
try {
res = JSON.parse(res)
} catch (error) {
// console.error(error)
throw error
}
}
resolve(res)
})
})
})
}
/**
* 注册一堆事件处理函数
*
* @memberof jsBridge
*/
registDefaultHandlerForApp() {
this.on('reload', () => {
location.reload()
return Promise.resolve('done!')
})
}
/**
* 给ios和android注册事件监听
*
* @memberof jsBridge
*/
on(...args) {
if (this.app === ANDROID) {
this.registerHandlerForAndroid(...args)
} else if (this.app === IOS) {
this.registerHandlerForIOS(...args)
}
}
/**
* 调用app方法
*
* @param {*} args
* @returns
* @memberof jsBridge
*/
callHandler(...args) {
if (this.app === ANDROID) {
return this.callAndroidHandler(...args)
} else if (this.app === IOS) {
return this.callIOSHandler(...args)
}
}
/**
* 关闭当前webview
*
* @memberof jsBridge
*/
closeCurrentWebview() {
return this.callHandler('closeCurrentWebview')
}
}
window.jsBridge=jsBridge
// console.log(window)
export default jsBridge;
\ No newline at end of file
...@@ -60,13 +60,14 @@ import ChatContentVue from "./ChatContent.vue"; ...@@ -60,13 +60,14 @@ import ChatContentVue from "./ChatContent.vue";
import ChatInputVue from "./ChatInput.vue"; import ChatInputVue from "./ChatInput.vue";
import { connectionState } from "@/store/connectionStore"; import { connectionState } from "@/store/connectionStore";
import FzmMessageProtocol from "@/utils/fzm-message-protocol"; import FzmMessageProtocol from "@/utils/fzm-message-protocol";
import { getOrderInfo } from "@/store/appCallerStore"; import { getOrderInfo, privateKey, publicKey } from "@/store/appCallerStore";
import { token, from, orderid } from "@/store/appCallerStore"; import { token, from, orderid } from "@/store/appCallerStore";
import computeExt from "@/utils/getFzmMesageProtocolExt"; import computeExt from "@/utils/getFzmMesageProtocolExt";
import { watch } from "@vue/runtime-core"; import { watch } from "@vue/runtime-core";
import decodeChatMessage from "@/utils/fzm-message-protocol-chat/decodeChatMessage"; import decodeChatMessage from "@/utils/fzm-message-protocol-chat/decodeChatMessage";
import { messageStore } from "@/store/messagesStore"; import { messageStore } from "@/store/messagesStore";
import { ChatMessageTypes } from "@/types/chatMessageTypes"; import { ChatMessageTypes } from "@/types/chatMessageTypes";
import { generateToken } from "@/utils/generateToken/generate-token";
export default defineComponent({ export default defineComponent({
components: { ChatContentVue, ChatInputVue, NavBar }, components: { ChatContentVue, ChatInputVue, NavBar },
...@@ -88,7 +89,11 @@ export default defineComponent({ ...@@ -88,7 +89,11 @@ export default defineComponent({
fmp fmp
.authorize({ .authorize({
appId: "dtalk", appId: "dtalk",
token: token, token: generateToken({
address: from,
privateKeyHex: privateKey,
publicKeyHex: publicKey,
}),
}) })
.then((conn) => { .then((conn) => {
connectionState.connection = conn; connectionState.connection = conn;
...@@ -125,12 +130,11 @@ export default defineComponent({ ...@@ -125,12 +130,11 @@ export default defineComponent({
// 连接后,指定每次收到消息要做的事 // 连接后,指定每次收到消息要做的事
connectionState.connection.onReceiveMessage = (msgData) => { connectionState.connection.onReceiveMessage = (msgData) => {
const msg = decodeChatMessage(msgData); const msg = decodeChatMessage(msgData);
console.log(msg, "show msg");
// 收到的非本笔订单的消息不处理 // 收到的非本笔订单的消息不处理
if (msg.orderid !== orderid) return; // if (msg.orderid !== orderid) return;
messageStore.displayNewMessage({ msg && messageStore.displayNewMessage({
content: msg.content, content: msg.content,
from: msg.from, from: msg.from,
uuid: msg.uuid, uuid: msg.uuid,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment