forked from unixpickle/fbmsgr
/
threads.go
404 lines (359 loc) · 11.4 KB
/
threads.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
package fbmsgr
import (
"encoding/json"
"errors"
"strconv"
"time"
"github.com/unixpickle/essentials"
)
const actionBufferSize = 500
const actionLogDocID = "1547392382048831"
const threadDocID = "1349387578499440"
// ThreadInfo stores information about a chat thread.
// A chat thread is facebook's internal name for a
// conversation (either a group chat or a 1-on-1).
type ThreadResponse struct {
ThreadKey struct {
ThreadFbid string `json:"thread_fbid"`
OtherUserID string `json:"other_user_id"`
} `json:"thread_key"`
Name string `json:"name"`
LastMessage struct {
Nodes []struct {
Snippet string `json:"snippet"`
MessageSender struct {
MessagingActor struct {
ID string `json:"id"`
} `json:"messaging_actor"`
} `json:"message_sender"`
TimestampPrecise string `json:"timestamp_precise"`
} `json:"nodes"`
} `json:"last_message"`
UnreadCount int `json:"unread_count"`
MessagesCount int `json:"messages_count"`
UpdatedTimePrecise string `json:"updated_time_precise"`
IsPinProtected bool `json:"is_pin_protected"`
IsViewerSubscribed bool `json:"is_viewer_subscribed"`
ThreadQueueEnabled bool `json:"thread_queue_enabled"`
Folder string `json:"folder"`
HasViewerArchived bool `json:"has_viewer_archived"`
IsPageFollowUp bool `json:"is_page_follow_up"`
CannotReplyReason string `json:"cannot_reply_reason"`
EphemeralTTLMode int `json:"ephemeral_ttl_mode"`
EventReminders struct {
Nodes []interface{} `json:"nodes"`
} `json:"event_reminders"`
MontageThread struct {
ID string `json:"id"`
} `json:"montage_thread"`
LastReadReceipt struct {
Nodes []struct {
TimestampPrecise string `json:"timestamp_precise"`
} `json:"nodes"`
} `json:"last_read_receipt"`
RelatedPageThread interface{} `json:"related_page_thread"`
AssociatedObject interface{} `json:"associated_object"`
PrivacyMode int `json:"privacy_mode"`
CustomizationEnabled bool `json:"customization_enabled"`
ThreadType string `json:"thread_type"`
ParticipantAddModeAsString interface{} `json:"participant_add_mode_as_string"`
ParticipantsEventStatus []interface{} `json:"participants_event_status"`
AllParticipants struct {
Nodes []struct {
MessagingActor struct {
ID string `json:"id"`
Typename string `json:"__typename"`
Name string `json:"name"`
Gender string `json:"gender"`
URL string `json:"url"`
BigImageSrc struct {
URI string `json:"uri"`
} `json:"big_image_src"`
ShortName string `json:"short_name"`
Username string `json:"username"`
IsViewerFriend bool `json:"is_viewer_friend"`
IsMessengerUser bool `json:"is_messenger_user"`
IsVerified bool `json:"is_verified"`
IsMessageBlockedByViewer bool `json:"is_message_blocked_by_viewer"`
IsViewerCoworker bool `json:"is_viewer_coworker"`
IsEmployee interface{} `json:"is_employee"`
} `json:"messaging_actor"`
} `json:"nodes"`
} `json:"all_participants"`
ReadReceipts struct {
Nodes []struct {
Watermark string `json:"watermark"`
Action string `json:"action"`
Actor struct {
ID string `json:"id"`
} `json:"actor"`
} `json:"nodes"`
} `json:"read_receipts"`
DeliveryReceipts struct {
Nodes []struct {
TimestampPrecise string `json:"timestamp_precise"`
} `json:"nodes"`
} `json:"delivery_receipts"`
}
type ThreadInfo struct {
ThreadID string `json:"thread_id"`
ThreadFBID string `json:"thread_fbid"`
Name string `json:"name"`
// OtherUserFBID is nil for group chats.
OtherUserFBID *string `json:"other_user_fbid"`
// Participants contains a list of FBIDs.
Participants []string `json:"participants"`
// Snippet stores the last message sent in the thread.
Snippet string `json:"snippet"`
SnippetSender string `json:"snippet_sender"`
UnreadCount int `json:"unread_count"`
MessageCount int `json:"message_count"`
Timestamp float64 `json:"timestamp"`
ServerTimestamp float64 `json:"server_timestamp"`
}
func (t *ThreadInfo) canonicalizeFBIDs() {
t.SnippetSender = stripFBIDPrefix(t.SnippetSender)
for i, p := range t.Participants {
t.Participants[i] = stripFBIDPrefix(p)
}
}
// ParticipantInfo stores information about a user.
type ParticipantInfo struct {
// ID is typically "fbid:..."
ID string `json:"id"`
FBID string `json:"fbid"`
Gender int `json:"gender"`
HREF string `json:"href"`
ImageSrc string `json:"image_src"`
BigImageSrc string `json:"big_image_src"`
Name string `json:"name"`
ShortName string `json:"short_name"`
}
// A ThreadListResult stores the result of listing the
// user's chat threads.
type ThreadListResult struct {
Threads []*ThreadInfo `json:"threads"`
Participants []*ParticipantInfo `json:"participants"`
}
func (s *Session) Thread(threadID string) (res *ThreadInfo, err error) {
defer essentials.AddCtxTo("fbmsgr: thread", &err)
params := map[string]interface{}{
"id": threadID,
"before": nil,
"message_limit": 0,
"load_messages": 0,
"load_read_receipts": false,
}
var respObj struct {
MessageThread *ThreadResponse `json:"message_thread"`
}
s.graphQLDoc("1498317363570230", params, &respObj)
thread := marshalThreadInfo(respObj.MessageThread)
return thread, nil
}
func (s *Session) Threads(limit int) (res *ThreadListResult, err error) {
defer essentials.AddCtxTo("fbmsgr: threads", &err)
params := map[string]interface{}{
"limit": limit,
"before": nil,
"tags": []string{"INBOX"},
"includeDeliveryReceipts": true,
"includeSeqID": false,
}
var respObj struct {
Viewer struct {
MessageThreads struct {
Threads []*ThreadResponse `json:"nodes"`
} `json:"message_threads"`
} `json:"viewer"`
}
s.graphQLDoc(threadDocID, params, &respObj)
threads := marshalThreadList(respObj.Viewer.MessageThreads.Threads)
result := &ThreadListResult{
Threads: threads,
Participants: []*ParticipantInfo{},
}
// TODO: handle participants json
// log.Printf("%s\n", respObj)
return result, nil
}
func marshalThreadList(respObj []*ThreadResponse) []*ThreadInfo {
out := []*ThreadInfo{}
for _, resp := range respObj {
out = append(out, marshalThreadInfo(resp))
}
return out
}
func marshalThreadInfo(resp *ThreadResponse) *ThreadInfo {
participantIDs := []string{}
for _, participant := range resp.AllParticipants.Nodes {
participantIDs = append(participantIDs, participant.MessagingActor.ID)
}
threadInfo := &ThreadInfo{
ThreadID: canonicalFBID(resp.ThreadKey.ThreadFbid),
ThreadFBID: canonicalFBID(resp.ThreadKey.ThreadFbid),
OtherUserFBID: &resp.ThreadKey.OtherUserID,
Participants: participantIDs,
UnreadCount: resp.UnreadCount,
MessageCount: resp.MessagesCount,
}
if resp.Name == "" && len(participantIDs) == 2 {
threadInfo.Name = resp.AllParticipants.Nodes[0].MessagingActor.Name
} else if resp.Name == "" {
threadInfo.Name = "Unnamed Group DM"
} else {
threadInfo.Name = resp.Name
}
if len(resp.LastMessage.Nodes) > 0 {
node := resp.LastMessage.Nodes[0]
threadInfo.Snippet = node.Snippet
threadInfo.SnippetSender = node.MessageSender.MessagingActor.ID
timestamp, err := strconv.ParseFloat(node.TimestampPrecise, 64)
if err != nil {
timestamp = 0
}
threadInfo.Timestamp = timestamp
threadInfo.ServerTimestamp = timestamp
}
return threadInfo
}
// Threads reads a range of the user's chat threads.
// The offset specifiecs the index of the first thread
// to fetch, starting at 0.
// The limit specifies the maximum number of threads.
func (s *Session) ThreadsDeprecated(offset, limit int) (res *ThreadListResult, err error) {
defer essentials.AddCtxTo("fbmsgr: threads", &err)
params, err := s.commonParams()
if err != nil {
return nil, err
}
params.Set("inbox[filter]", "")
params.Set("inbox[offset]", strconv.Itoa(offset))
params.Set("inbox[limit]", strconv.Itoa(limit))
reqURL := BaseURL + "/ajax/mercury/threadlist_info.php?dpr=1"
body, err := s.jsonForPost(reqURL, params)
if err != nil {
return nil, err
}
var respObj struct {
Payload ThreadListResult `json:"payload"`
}
if err := json.Unmarshal(body, &respObj); err != nil {
return nil, errors.New("parse json: " + err.Error())
}
for _, x := range respObj.Payload.Participants {
x.FBID = stripFBIDPrefix(x.FBID)
}
for _, x := range respObj.Payload.Threads {
x.canonicalizeFBIDs()
}
return &respObj.Payload, nil
}
// ActionLog reads the contents of a thread.
//
// The fbid parameter specifies the other user ID or the
// group thread ID.
//
// The timestamp parameter specifies the timestamp of the
// earliest action seen from the last call to ActionLog.
// It may be the 0 time, in which case the most recent
// actions will be fetched.
//
// The limit parameter indicates the maximum number of
// actions to fetch.
func (s *Session) ActionLog(fbid string, timestamp time.Time,
limit int) (log []Action, err error) {
defer essentials.AddCtxTo("fbmsgr: action log", &err)
var response struct {
Thread struct {
Messages struct {
Nodes []map[string]interface{} `json:"nodes"`
} `json:"messages"`
} `json:"message_thread"`
}
params := map[string]interface{}{
"id": fbid,
"message_limit": limit,
"load_messages": 1,
"load_read_receipts": true,
}
if timestamp.IsZero() {
params["before"] = nil
} else {
params["before"] = strconv.FormatInt(timestamp.UnixNano()/1e6, 10)
}
if err := s.graphQLDoc(actionLogDocID, params, &response); err != nil {
return nil, err
}
for _, x := range response.Thread.Messages.Nodes {
log = append(log, decodeAction(x))
}
return
}
// FullActionLog fetches all of the actions in a thread
// and returns them in reverse chronological order over
// a channel.
//
// The cancel channel, if non-nil, can be closed to stop
// the fetch early.
//
// The returned channels will both be closed once the
// fetch has completed or been cancelled.
// If an error is encountered during the fetch, it is sent
// over the (buffered) error channel and the fetch will be
// aborted.
func (s *Session) FullActionLog(fbid string, cancel <-chan struct{}) (<-chan Action, <-chan error) {
if cancel == nil {
cancel = make(chan struct{})
}
res := make(chan Action, actionBufferSize)
errRes := make(chan error, 1)
go func() {
defer close(res)
defer close(errRes)
var lastTime time.Time
var offset int
for {
listing, err := s.ActionLog(fbid, lastTime, actionBufferSize)
if err != nil {
errRes <- err
return
}
// Remove the one overlapping action.
if offset > 0 && len(listing) > 0 {
listing = listing[:len(listing)-1]
}
if len(listing) == 0 {
return
}
for i := len(listing) - 1; i >= 0; i-- {
x := listing[i]
select {
case <-cancel:
return
default:
}
select {
case res <- x:
case <-cancel:
return
}
}
offset += len(listing)
lastTime = listing[0].ActionTime()
}
}()
return res, errRes
}
// DeleteMessage deletes a message given its ID.
func (s *Session) DeleteMessage(id string) (err error) {
defer essentials.AddCtxTo("fbmsgr: delete message", &err)
url := BaseURL + "/ajax/mercury/delete_messages.php?dpr=1"
values, err := s.commonParams()
if err != nil {
return err
}
values.Set("message_ids[0]", id)
_, err = s.jsonForPost(url, values)
return err
}