Skip to content

Commit c66ac83

Browse files
NordskogBjornskjald
authored andcommitted
Added sending of presence state, typing state, and read receipts.
1 parent d5cdb11 commit c66ac83

File tree

6 files changed

+241
-0
lines changed

6 files changed

+241
-0
lines changed

src/Client.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import EventEmitter from 'events'
1616
import { AttachmentNotFoundError, AttachmentURLMissingError } from './types/Errors'
1717
import StrictEventEmitter from 'strict-event-emitter-types'
1818
import ClientEvents from './ClientEvents'
19+
import * as Payloads from './mqtt/payloads'
1920

2021
const debugLog = debug('fblib')
2122

@@ -135,6 +136,32 @@ export default class Client extends (EventEmitter as { new(): ClientEmitter }) {
135136
return this.mqttApi.sendMessage(threadId, message, options)
136137
}
137138

139+
/**
140+
* Indicate that the user is currently present in the conversation.
141+
* Only relevant for non-group conversations
142+
*/
143+
sendPresenceState = async (recipientUserId: string, present: boolean) => {
144+
const payload = new Payloads.PresenceState(recipientUserId, present)
145+
this.mqttApi.sendPublish(payload.getTopic(), await Payloads.encodePayload(payload))
146+
}
147+
148+
/**
149+
* Send "User is typing" message.
150+
* In a non-group conversation, sendPresenceState() must be called first.
151+
*/
152+
sendTypingState = async (threadOrRecipientUserId: string, present: boolean) => {
153+
const payload = new Payloads.TypingState(this.session.tokens.uid, present, threadOrRecipientUserId)
154+
this.mqttApi.sendPublish(payload.getTopic(), await Payloads.encodePayload(payload))
155+
}
156+
157+
/**
158+
* Mark a message as read.
159+
*/
160+
sendReadReceipt = async (message: Message) => {
161+
const payload = new Payloads.ReadReceipt(message)
162+
this.mqttApi.sendPublish(payload.getTopic(), await Payloads.encodePayload(payload))
163+
}
164+
138165
getThreadList = async (count: number): Promise<Thread[]> => {
139166
const threads = await this.httpApi.threadListQuery(count)
140167
return threads.viewer.message_threads.nodes.map(parseThread)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import Payload from './Payload'
2+
import { Thrift, TCompactProtocol, Int64 } from 'thrift'
3+
import Long from 'long'
4+
5+
export default class PresenceStatePayload extends Payload {
6+
recipient: string
7+
sender: number // Number because always 0
8+
state: boolean
9+
10+
constructor (recipientId: string, state: boolean) {
11+
super()
12+
this.recipient = recipientId.toString()
13+
this.sender = 0
14+
this.state = state
15+
}
16+
17+
encode (proto: TCompactProtocol): Promise<void> {
18+
/////////////////////
19+
// Tracking info
20+
/////////////////////
21+
22+
proto.writeFieldBegin('traceInfo', Thrift.Type.STRING, 1)
23+
proto.writeString('') // Empty string in our case
24+
25+
// Note lack of end-of-object null
26+
27+
/////////////////////
28+
// Typing presence
29+
/////////////////////
30+
31+
proto.writeFieldBegin('recipient', Thrift.Type.I64, 1)
32+
proto.writeI64(new Int64(Buffer.from(Long.fromString(this.recipient).toBytes())))
33+
34+
// sender always 0
35+
proto.writeFieldBegin('sender', Thrift.Type.I64, 2)
36+
proto.writeI64(this.sender)
37+
38+
proto.writeFieldBegin('state', Thrift.Type.I32, 3)
39+
proto.writeI32(this.state ? 1 : 0)
40+
41+
// denotes end of object
42+
proto.writeByte(0)
43+
44+
return null
45+
}
46+
47+
getTopic (): string {
48+
return '/t_stp'
49+
}
50+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import Payload from './Payload'
2+
import { Thrift, TCompactProtocol, Int64 } from 'thrift'
3+
import Message from '../../types/message'
4+
import RandomIntGenerator from '../../RandomIntGenerator'
5+
import Long from 'long'
6+
7+
export default class ReadReceiptPayload extends Payload {
8+
9+
otherUserFbId: string // Used for individuals
10+
threadFbId: string // Used for groups, same as threadId ( usually ? )
11+
watermarkTimestamp: number// Must match timestamp of a message
12+
13+
public constructor (message: Message) {
14+
super()
15+
16+
if (message.authorId.toString() === message.threadId.toString()) {
17+
// If author id equals thread id, then this is not a group convo
18+
this.otherUserFbId = message.authorId.toString()
19+
} else {
20+
// Otherwise it is
21+
this.threadFbId = message.threadId.toString()
22+
}
23+
24+
this.watermarkTimestamp = message.timestamp
25+
26+
}
27+
28+
encode (proto: TCompactProtocol): Promise<void> {
29+
/////////////////////
30+
// thrift header?
31+
/////////////////////
32+
33+
// Always completely empty.
34+
proto.writeByte(0)
35+
36+
// Note lack of end-of-object null
37+
38+
/////////////////////
39+
// Typing presence
40+
/////////////////////
41+
42+
proto.writeFieldBegin('mark', Thrift.Type.STRING, 1)
43+
proto.writeString('read')
44+
45+
proto.writeFieldBegin('state', Thrift.Type.BOOL, 2)
46+
proto.writeBool(true)
47+
48+
if (this.threadFbId != null) {
49+
proto.writeFieldBegin('threadFbId', Thrift.Type.I64, 6)
50+
proto.writeI64(new Int64(Buffer.from(Long.fromString(this.threadFbId).toBytes())))
51+
} else if (this.otherUserFbId != null) {
52+
proto.writeFieldBegin('otherUserFbId', Thrift.Type.I64, 7)
53+
proto.writeI64(new Int64(Buffer.from(Long.fromString(this.otherUserFbId).toBytes())))
54+
} else {
55+
// Throw?
56+
}
57+
58+
proto.writeFieldBegin('watermarkTimestamp', Thrift.Type.I64, 9)
59+
proto.writeI64(this.watermarkTimestamp)
60+
61+
proto.writeFieldBegin('attemptId', Thrift.Type.I64, 13)
62+
proto.writeI64(new Int64(Buffer.from(RandomIntGenerator.getAttemptId().toBytes())))
63+
64+
// denotes end of object
65+
proto.writeByte(0)
66+
67+
return null
68+
}
69+
70+
getTopic (): string {
71+
return '/t_mt_req'
72+
}
73+
}

src/mqtt/payloads/ThreadKey.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import Payload from './Payload'
2+
import { Thrift, TCompactProtocol } from 'thrift'
3+
4+
export default class ThreadKeyPayload extends Payload {
5+
threadId: string
6+
threadType: number // 0: CANONICAL, 1: GROUP
7+
8+
constructor (threadId: string, threadType: 0 | 1) {
9+
super()
10+
this.threadId = threadId.toString()
11+
this.threadType = threadType
12+
}
13+
14+
encode (proto: TCompactProtocol): Promise<void> {
15+
16+
proto.writeFieldBegin('threadId', Thrift.Type.STRING, 1)
17+
proto.writeString(this.threadId)
18+
19+
proto.writeFieldBegin('threadType', Thrift.Type.I32, 2)
20+
proto.writeI64(this.threadType)
21+
22+
// denotes end of object
23+
proto.writeByte(0)
24+
25+
return null
26+
}
27+
28+
getTopic (): string {
29+
return null
30+
}
31+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import Payload from './Payload'
2+
import ThreadKey from './ThreadKey'
3+
import { Thrift, TCompactProtocol, Int64 } from 'thrift'
4+
import Long from 'long'
5+
6+
export default class TypingStatePayload extends Payload {
7+
recipient: string
8+
sender: string
9+
state: boolean
10+
threadKey: ThreadKey
11+
12+
constructor (senderUserId: string, state: boolean, recipientUserOrThreadId: string) {
13+
super()
14+
15+
this.sender = senderUserId.toString()
16+
this.state = state
17+
18+
if (recipientUserOrThreadId.toString().startsWith('100')) {
19+
this.recipient = recipientUserOrThreadId.toString()
20+
} else {
21+
this.threadKey = new ThreadKey(recipientUserOrThreadId.toString(), 1)
22+
}
23+
}
24+
25+
encode (proto: TCompactProtocol): Promise<void> {
26+
// Added explicitly outside of thrift code
27+
proto.writeByte(0)
28+
29+
if (this.recipient != null) {
30+
proto.writeFieldBegin('recipient', Thrift.Type.I64, 1)
31+
proto.writeI64(new Int64(Buffer.from(Long.fromString(this.recipient).toBytes())))
32+
}
33+
34+
proto.writeFieldBegin('sender', Thrift.Type.I64, 2)
35+
proto.writeI64(new Int64(Buffer.from(Long.fromString(this.sender).toBytes())))
36+
37+
proto.writeFieldBegin('state', Thrift.Type.I32, 3)
38+
proto.writeI32(this.state ? 1 : 0)
39+
40+
if (this.threadKey != null) {
41+
proto.writeFieldBegin('threadKey', Thrift.Type.STRUCT, 5)
42+
proto.writeStructBegin('threadKey')
43+
this.threadKey.encode(proto)
44+
proto.writeStructEnd()
45+
proto.writeFieldEnd()
46+
}
47+
48+
// denotes end of object
49+
proto.writeByte(0)
50+
51+
return null
52+
}
53+
54+
getTopic (): string {
55+
return '/t_st'
56+
}
57+
}

src/mqtt/payloads/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import Payload from './Payload'
22

33
export { Payload }
4+
export { default as PresenceState } from './PresenceStatePayload'
5+
export { default as ReadReceipt } from './ReadReceiptPayload'
6+
export { default as TypingState } from './TypingStatePayload'
47

58
export const encodePayload = Payload.encodePayload

0 commit comments

Comments
 (0)