@dongdev/fca-unofficial 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +142 -102
- package/package.json +4 -3
- package/src/getThreadList.js +175 -224
- package/src/listenMqtt.js +230 -138
- package/utils.js +71 -40
package/src/listenMqtt.js
CHANGED
@@ -3,97 +3,137 @@
|
|
3
3
|
const utils = require("../utils");
|
4
4
|
const log = require("npmlog");
|
5
5
|
const mqtt = require('mqtt');
|
6
|
-
const
|
6
|
+
const WebSocket = require('ws');
|
7
7
|
const HttpsProxyAgent = require('https-proxy-agent');
|
8
8
|
const EventEmitter = require('events');
|
9
|
+
const Duplexify = require('duplexify');
|
10
|
+
const {
|
11
|
+
Transform
|
12
|
+
} = require('stream');
|
9
13
|
const debugSeq = false;
|
10
|
-
var identity = function
|
14
|
+
var identity = function() {};
|
11
15
|
var form = {};
|
12
|
-
var getSeqId = function
|
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
|
-
|
16
|
+
var getSeqId = function() {};
|
17
|
+
const topics = ['/ls_req', '/ls_resp', '/legacy_web', '/webrtc', '/rtc_multi', '/onevc', '/br_sr', '/sr_res', '/t_ms', '/thread_typing', '/orca_typing_notifications', '/notify_disconnect', '/orca_presence', '/inbox', '/mercury', '/messaging_events', '/orca_message_notifications', '/pp', '/webrtc_response'];
|
18
|
+
let WebSocket_Global;
|
19
|
+
function buildProxy() {
|
20
|
+
const Proxy = new Transform({
|
21
|
+
objectMode: false,
|
22
|
+
transform(chunk, enc, next) {
|
23
|
+
if (WebSocket_Global.readyState !== WebSocket_Global.OPEN) {
|
24
|
+
return next();
|
25
|
+
}
|
26
|
+
let data;
|
27
|
+
if (typeof chunk === 'string') {
|
28
|
+
data = Buffer.from(chunk, 'utf8');
|
29
|
+
} else {
|
30
|
+
data = chunk;
|
31
|
+
}
|
32
|
+
WebSocket_Global.send(data);
|
33
|
+
next();
|
34
|
+
},
|
35
|
+
flush(done) {
|
36
|
+
WebSocket_Global.close();
|
37
|
+
done();
|
38
|
+
},
|
39
|
+
writev(chunks, cb) {
|
40
|
+
const buffers = chunks.map(({ chunk }) => {
|
41
|
+
if (typeof chunk === 'string') {
|
42
|
+
return Buffer.from(chunk, 'utf8');
|
43
|
+
}
|
44
|
+
return chunk;
|
45
|
+
});
|
46
|
+
this._write(Buffer.concat(buffers), 'binary', cb);
|
47
|
+
},
|
48
|
+
});
|
49
|
+
return Proxy;
|
50
|
+
}
|
51
|
+
function buildStream(options, WebSocket, Proxy) {
|
52
|
+
const Stream = Duplexify(undefined, undefined, options);
|
53
|
+
Stream.socket = WebSocket;
|
54
|
+
WebSocket.onclose = () => {
|
55
|
+
Stream.end();
|
56
|
+
Stream.destroy();
|
57
|
+
};
|
58
|
+
WebSocket.onerror = (err) => {
|
59
|
+
Stream.destroy(err);
|
60
|
+
};
|
61
|
+
WebSocket.onmessage = (event) => {
|
62
|
+
const data = event.data instanceof ArrayBuffer ? Buffer.from(event.data) : Buffer.from(event.data, 'utf8');
|
63
|
+
Stream.push(data);
|
64
|
+
};
|
65
|
+
WebSocket.onopen = () => {
|
66
|
+
Stream.setReadable(Proxy);
|
67
|
+
Stream.setWritable(Proxy);
|
68
|
+
Stream.emit('connect');
|
69
|
+
};
|
70
|
+
WebSocket_Global = WebSocket;
|
71
|
+
Proxy.on('close', () => WebSocket.close());
|
72
|
+
return Stream;
|
73
|
+
}
|
39
74
|
function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
40
75
|
const chatOn = ctx.globalOptions.online;
|
41
76
|
const foreground = false;
|
42
|
-
const sessionID = Math.floor(Math.random() *
|
77
|
+
const sessionID = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER) + 1;
|
78
|
+
const GUID = utils.getGUID();
|
43
79
|
const username = {
|
44
|
-
u: ctx.
|
80
|
+
u: ctx.userID,
|
45
81
|
s: sessionID,
|
46
82
|
chat_on: chatOn,
|
47
83
|
fg: foreground,
|
48
|
-
d:
|
49
|
-
ct:
|
50
|
-
aid:
|
51
|
-
|
84
|
+
d: GUID,
|
85
|
+
ct: 'websocket',
|
86
|
+
aid: '219994525426954',
|
87
|
+
aids: null,
|
88
|
+
mqtt_sid: '',
|
52
89
|
cp: 3,
|
53
90
|
ecp: 10,
|
54
91
|
st: [],
|
55
92
|
pm: [],
|
56
|
-
dc:
|
93
|
+
dc: '',
|
57
94
|
no_auto_fg: true,
|
58
95
|
gas: null,
|
59
96
|
pack: [],
|
60
|
-
|
61
|
-
|
97
|
+
p: null,
|
98
|
+
php_override: ""
|
62
99
|
};
|
63
|
-
const cookies = ctx.jar.getCookies(
|
100
|
+
const cookies = ctx.jar.getCookies('https://www.facebook.com').join('; ');
|
64
101
|
let host;
|
65
102
|
if (ctx.mqttEndpoint) {
|
66
|
-
host = `${ctx.mqttEndpoint}&sid=${sessionID}`;
|
103
|
+
host = `${ctx.mqttEndpoint}&sid=${sessionID}&cid=${GUID}`;
|
67
104
|
} else if (ctx.region) {
|
68
|
-
host = `wss://edge-chat.facebook.com/chat?region=${ctx.region.
|
105
|
+
host = `wss://edge-chat.facebook.com/chat?region=${ctx.region.toLowerCase()}&sid=${sessionID}&cid=${GUID}`;
|
69
106
|
} else {
|
70
|
-
host = `wss://edge-chat.facebook.com/chat?sid=${sessionID}`;
|
107
|
+
host = `wss://edge-chat.facebook.com/chat?sid=${sessionID}&cid=${GUID}`;
|
71
108
|
}
|
72
109
|
const options = {
|
73
|
-
clientId:
|
110
|
+
clientId: 'mqttwsclient',
|
74
111
|
protocolId: 'MQIsdp',
|
75
112
|
protocolVersion: 3,
|
76
113
|
username: JSON.stringify(username),
|
77
114
|
clean: true,
|
78
115
|
wsOptions: {
|
79
116
|
headers: {
|
80
|
-
|
81
|
-
|
82
|
-
'User-Agent': ctx.globalOptions.userAgent,
|
83
|
-
|
84
|
-
|
117
|
+
Cookie: cookies,
|
118
|
+
Origin: 'https://www.facebook.com',
|
119
|
+
'User-Agent': ctx.globalOptions.userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36',
|
120
|
+
Referer: 'https://www.facebook.com/',
|
121
|
+
Host: new URL(host).hostname,
|
85
122
|
},
|
86
123
|
origin: 'https://www.facebook.com',
|
87
|
-
protocolVersion: 13
|
124
|
+
protocolVersion: 13,
|
125
|
+
binaryType: 'arraybuffer',
|
88
126
|
},
|
89
|
-
keepalive:
|
90
|
-
reschedulePings:
|
127
|
+
keepalive: 60,
|
128
|
+
reschedulePings: true,
|
129
|
+
reconnectPeriod: 2000,
|
130
|
+
connectTimeout: 10000,
|
91
131
|
};
|
92
132
|
if (typeof ctx.globalOptions.proxy != "undefined") {
|
93
133
|
const agent = new HttpsProxyAgent(ctx.globalOptions.proxy);
|
94
134
|
options.wsOptions.agent = agent;
|
95
135
|
}
|
96
|
-
ctx.mqttClient = new mqtt.Client(
|
136
|
+
ctx.mqttClient = new mqtt.Client(() => buildStream(options, new WebSocket(host, options.wsOptions), buildProxy()), options);
|
97
137
|
const mqttClient = ctx.mqttClient;
|
98
138
|
global.mqttClient = mqttClient;
|
99
139
|
mqttClient.on('error', function (err) {
|
@@ -117,6 +157,7 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
117
157
|
});
|
118
158
|
}
|
119
159
|
});
|
160
|
+
|
120
161
|
mqttClient.on('close', function () {
|
121
162
|
|
122
163
|
});
|
@@ -125,6 +166,7 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
125
166
|
topics.forEach(function (topicsub) {
|
126
167
|
mqttClient.subscribe(topicsub);
|
127
168
|
});
|
169
|
+
|
128
170
|
let topic;
|
129
171
|
const queue = {
|
130
172
|
sync_api_version: 10,
|
@@ -148,6 +190,8 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
148
190
|
qos: 1,
|
149
191
|
retain: false
|
150
192
|
});
|
193
|
+
// set status online
|
194
|
+
// fix by NTKhang
|
151
195
|
mqttClient.publish("/foreground_state", JSON.stringify({
|
152
196
|
foreground: chatOn
|
153
197
|
}), {
|
@@ -158,10 +202,12 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
158
202
|
}), {
|
159
203
|
qos: 1
|
160
204
|
});
|
205
|
+
|
161
206
|
const rTimeout = setTimeout(function () {
|
162
207
|
mqttClient.end();
|
163
208
|
listenMqtt(defaultFuncs, api, ctx, globalCallback);
|
164
209
|
}, 5000);
|
210
|
+
|
165
211
|
ctx.tmsWait = function () {
|
166
212
|
clearTimeout(rTimeout);
|
167
213
|
ctx.globalOptions.emitReady ? globalCallback({
|
@@ -170,6 +216,7 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
170
216
|
}) : "";
|
171
217
|
delete ctx.tmsWait;
|
172
218
|
};
|
219
|
+
|
173
220
|
});
|
174
221
|
|
175
222
|
mqttClient.on('message', function (topic, message, _packet) {
|
@@ -179,6 +226,7 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
179
226
|
} catch (e) {
|
180
227
|
jsonMessage = {};
|
181
228
|
}
|
229
|
+
|
182
230
|
if (jsonMessage.type === "jewel_requests_add") {
|
183
231
|
globalCallback(null, {
|
184
232
|
type: "friend_request_received",
|
@@ -204,6 +252,8 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
204
252
|
if (jsonMessage.lastIssuedSeqId) {
|
205
253
|
ctx.lastSeqId = parseInt(jsonMessage.lastIssuedSeqId);
|
206
254
|
}
|
255
|
+
|
256
|
+
//If it contains more than 1 delta
|
207
257
|
for (const i in jsonMessage.deltas) {
|
208
258
|
const delta = jsonMessage.deltas[i];
|
209
259
|
parseDelta(defaultFuncs, api, ctx, globalCallback, {
|
@@ -225,9 +275,11 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
225
275
|
for (const i in jsonMessage.list) {
|
226
276
|
const data = jsonMessage.list[i];
|
227
277
|
const userID = data["u"];
|
278
|
+
|
228
279
|
const presence = {
|
229
280
|
type: "presence",
|
230
281
|
userID: userID.toString(),
|
282
|
+
//Convert to ms
|
231
283
|
timestamp: data["l"] * 1000,
|
232
284
|
statuses: data["p"]
|
233
285
|
};
|
@@ -237,12 +289,19 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
237
289
|
}
|
238
290
|
}
|
239
291
|
}
|
292
|
+
|
240
293
|
});
|
294
|
+
|
241
295
|
}
|
242
296
|
|
243
297
|
function parseDelta(defaultFuncs, api, ctx, globalCallback, v) {
|
244
298
|
if (v.delta.class == "NewMessage") {
|
245
|
-
|
299
|
+
//Not tested for pages
|
300
|
+
if (ctx.globalOptions.pageID &&
|
301
|
+
ctx.globalOptions.pageID != v.queue
|
302
|
+
)
|
303
|
+
return;
|
304
|
+
|
246
305
|
(function resolveAttachmentUrl(i) {
|
247
306
|
if (i == (v.delta.attachments || []).length) {
|
248
307
|
let fmtMsg;
|
@@ -335,6 +394,7 @@ function parseDelta(defaultFuncs, api, ctx, globalCallback, v) {
|
|
335
394
|
});
|
336
395
|
})();
|
337
396
|
} else if (delta.deltaMessageReply) {
|
397
|
+
//Mention block - #1
|
338
398
|
let mdata =
|
339
399
|
delta.deltaMessageReply.message === undefined ? [] :
|
340
400
|
delta.deltaMessageReply.message.data === undefined ? [] :
|
@@ -746,82 +806,123 @@ function markDelivery(ctx, api, threadID, messageID) {
|
|
746
806
|
|
747
807
|
module.exports = function (defaultFuncs, api, ctx) {
|
748
808
|
let globalCallback = identity;
|
809
|
+
let sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
749
810
|
getSeqId = function getSeqId() {
|
750
811
|
ctx.t_mqttCalled = false;
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
812
|
+
async function attemptRequest(retries = 3) {
|
813
|
+
try {
|
814
|
+
if (!ctx.fb_dtsg) {
|
815
|
+
const dtsg = await api.getFreshDtsg();
|
816
|
+
if (!dtsg) {
|
817
|
+
if (retries > 0) {
|
818
|
+
console.log("Failed to get fb_dtsg, retrying...");
|
819
|
+
await sleep(2000);
|
820
|
+
return attemptRequest(retries - 1);
|
821
|
+
}
|
822
|
+
throw {
|
823
|
+
error: "Could not obtain fb_dtsg after multiple attempts"
|
824
|
+
};
|
825
|
+
}
|
826
|
+
ctx.fb_dtsg = dtsg;
|
827
|
+
}
|
828
|
+
const form = {
|
829
|
+
av: ctx.userID,
|
830
|
+
fb_dtsg: ctx.fb_dtsg,
|
831
|
+
queries: JSON.stringify({
|
832
|
+
o0: {
|
833
|
+
doc_id: '3336396659757871',
|
834
|
+
query_params: {
|
835
|
+
limit: 1,
|
836
|
+
before: null,
|
837
|
+
tags: ['INBOX'],
|
838
|
+
includeDeliveryReceipts: false,
|
839
|
+
includeSeqID: true
|
840
|
+
}
|
841
|
+
}
|
842
|
+
}),
|
843
|
+
__user: ctx.userID,
|
844
|
+
__a: '1',
|
845
|
+
__req: '8',
|
846
|
+
__hs: '19577.HYP:comet_pkg.2.1..2.1',
|
847
|
+
dpr: '1',
|
848
|
+
fb_api_caller_class: 'RelayModern',
|
849
|
+
fb_api_req_friendly_name: 'MessengerGraphQLThreadlistFetcher'
|
850
|
+
};
|
851
|
+
const headers = {
|
852
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
853
|
+
'Referer': 'https://www.facebook.com/',
|
854
|
+
'Origin': 'https://www.facebook.com',
|
855
|
+
'sec-fetch-site': 'same-origin',
|
856
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
|
857
|
+
'Cookie': ctx.jar.getCookieString('https://www.facebook.com'),
|
858
|
+
'accept': '*/*',
|
859
|
+
'accept-encoding': 'gzip, deflate, br'
|
860
|
+
};
|
861
|
+
|
862
|
+
const resData = await defaultFuncs
|
863
|
+
.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form, {
|
864
|
+
headers
|
865
|
+
})
|
866
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
867
|
+
if (debugSeq) {
|
868
|
+
console.log('GraphQL SeqID Response:', JSON.stringify(resData, null, 2));
|
869
|
+
}
|
870
|
+
|
871
|
+
if (resData.error === 1357004 || resData.error === 1357001) {
|
872
|
+
if (retries > 0) {
|
873
|
+
console.log("Session error, refreshing token and retrying...");
|
874
|
+
ctx.fb_dtsg = null;
|
875
|
+
await sleep(2000);
|
876
|
+
return attemptRequest(retries - 1);
|
877
|
+
}
|
789
878
|
throw {
|
790
|
-
error: "
|
791
|
-
res: resData,
|
879
|
+
error: "Session refresh failed after retries"
|
792
880
|
};
|
793
881
|
}
|
794
|
-
|
795
|
-
|
796
|
-
}
|
797
|
-
if (resData[resData.length - 1].successful_results === 0) {
|
882
|
+
|
883
|
+
if (!Array.isArray(resData)) {
|
798
884
|
throw {
|
799
|
-
error: "
|
800
|
-
res: resData
|
885
|
+
error: "Invalid response format",
|
886
|
+
res: resData
|
801
887
|
};
|
802
888
|
}
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
listenMqtt(defaultFuncs, api, ctx, globalCallback);
|
807
|
-
} else {
|
889
|
+
|
890
|
+
const seqID = resData[0]?.o0?.data?.viewer?.message_threads?.sync_sequence_id;
|
891
|
+
if (!seqID) {
|
808
892
|
throw {
|
809
|
-
error: "
|
810
|
-
res: resData
|
893
|
+
error: "Missing sync_sequence_id",
|
894
|
+
res: resData
|
811
895
|
};
|
812
896
|
}
|
813
|
-
|
897
|
+
|
898
|
+
ctx.lastSeqId = seqID;
|
899
|
+
if (debugSeq) {
|
900
|
+
console.log('Got SeqID:', ctx.lastSeqId);
|
901
|
+
}
|
902
|
+
|
903
|
+
return listenMqtt(defaultFuncs, api, ctx, globalCallback);
|
904
|
+
|
905
|
+
} catch (err) {
|
906
|
+
if (retries > 0) {
|
907
|
+
console.log("Request failed, retrying...");
|
908
|
+
|
909
|
+
return attemptRequest(retries - 1);
|
910
|
+
}
|
911
|
+
throw err;
|
912
|
+
}
|
913
|
+
}
|
914
|
+
|
915
|
+
return attemptRequest()
|
814
916
|
.catch((err) => {
|
815
917
|
log.error("getSeqId", err);
|
816
|
-
if (utils.getType(err)
|
817
|
-
ctx.loggedIn = false;
|
818
|
-
}
|
918
|
+
if (utils.getType(err) == "Object" && err.error === "Not logged in") ctx.loggedIn = false;
|
819
919
|
return globalCallback(err);
|
820
920
|
});
|
821
|
-
}
|
921
|
+
}
|
822
922
|
return function (callback) {
|
823
923
|
class MessageEmitter extends EventEmitter {
|
824
924
|
stopListening(callback) {
|
925
|
+
|
825
926
|
callback = callback || (() => { });
|
826
927
|
globalCallback = identity;
|
827
928
|
if (ctx.mqttClient) {
|
@@ -829,50 +930,41 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
829
930
|
ctx.mqttClient.unsubscribe("/rtc_multi");
|
830
931
|
ctx.mqttClient.unsubscribe("/onevc");
|
831
932
|
ctx.mqttClient.publish("/browser_close", "{}");
|
832
|
-
ctx.mqttClient.end(false, ()
|
933
|
+
ctx.mqttClient.end(false, function (...data) {
|
934
|
+
callback(data);
|
833
935
|
ctx.mqttClient = undefined;
|
834
|
-
callback();
|
835
936
|
});
|
836
|
-
} else {
|
837
|
-
callback();
|
838
937
|
}
|
839
938
|
}
|
939
|
+
|
840
940
|
async stopListeningAsync() {
|
841
941
|
return new Promise((resolve) => {
|
842
942
|
this.stopListening(resolve);
|
843
943
|
});
|
844
944
|
}
|
845
945
|
}
|
946
|
+
|
846
947
|
const msgEmitter = new MessageEmitter();
|
847
|
-
globalCallback = callback || function (error, message) {
|
948
|
+
globalCallback = (callback || function (error, message) {
|
848
949
|
if (error) {
|
849
950
|
return msgEmitter.emit("error", error);
|
850
951
|
}
|
851
952
|
msgEmitter.emit("message", message);
|
852
|
-
};
|
853
|
-
|
953
|
+
});
|
954
|
+
|
955
|
+
if (!ctx.firstListen)
|
956
|
+
ctx.lastSeqId = null;
|
854
957
|
ctx.syncToken = undefined;
|
855
958
|
ctx.t_mqttCalled = false;
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
includeDeliveryReceipts: false,
|
866
|
-
includeSeqID: true,
|
867
|
-
},
|
868
|
-
},
|
869
|
-
}),
|
870
|
-
};
|
871
|
-
if (!ctx.firstListen || !ctx.lastSeqId) getSeqId();
|
872
|
-
else listenMqtt(defaultFuncs, api, ctx, globalCallback);
|
873
|
-
ctx.firstListen = false;
|
874
|
-
api.stopListening = msgEmitter.stopListening.bind(msgEmitter);
|
875
|
-
api.stopListeningAsync = msgEmitter.stopListeningAsync.bind(msgEmitter);
|
959
|
+
|
960
|
+
if (!ctx.firstListen || !ctx.lastSeqId) {
|
961
|
+
getSeqId(defaultFuncs, api, ctx, globalCallback);
|
962
|
+
} else {
|
963
|
+
listenMqtt(defaultFuncs, api, ctx, globalCallback);
|
964
|
+
}
|
965
|
+
|
966
|
+
api.stopListening = msgEmitter.stopListening;
|
967
|
+
api.stopListeningAsync = msgEmitter.stopListeningAsync;
|
876
968
|
return msgEmitter;
|
877
969
|
};
|
878
970
|
};
|