@dongdev/fca-unofficial 3.0.28 → 3.0.30

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +229 -132
  2. package/DOCS.md +82 -3
  3. package/README.md +524 -632
  4. package/func/logAdapter.js +33 -0
  5. package/index.d.ts +6 -0
  6. package/module/config.js +11 -1
  7. package/module/loginHelper.js +63 -7
  8. package/package.json +88 -81
  9. package/src/api/action/changeAvatar.js +1 -1
  10. package/src/api/action/changeBio.js +1 -1
  11. package/src/api/action/handleFriendRequest.js +1 -1
  12. package/src/api/action/logout.js +1 -1
  13. package/src/api/action/refreshFb_dtsg.js +1 -1
  14. package/src/api/action/setPostReaction.js +1 -1
  15. package/src/api/action/unfriend.js +1 -1
  16. package/src/api/http/postFormData.js +1 -1
  17. package/src/api/messaging/changeArchivedStatus.js +1 -1
  18. package/src/api/messaging/changeBlockedStatus.js +1 -1
  19. package/src/api/messaging/changeGroupImage.js +1 -1
  20. package/src/api/messaging/changeNickname.js +1 -1
  21. package/src/api/messaging/changeThreadEmoji.js +1 -1
  22. package/src/api/messaging/createNewGroup.js +1 -1
  23. package/src/api/messaging/createThemeAI.js +1 -1
  24. package/src/api/messaging/deleteMessage.js +1 -1
  25. package/src/api/messaging/deleteThread.js +1 -1
  26. package/src/api/messaging/getFriendsList.js +1 -1
  27. package/src/api/messaging/getMessage.js +1 -1
  28. package/src/api/messaging/getThemePictures.js +1 -1
  29. package/src/api/messaging/handleMessageRequest.js +1 -1
  30. package/src/api/messaging/markAsDelivered.js +1 -1
  31. package/src/api/messaging/markAsRead.js +1 -1
  32. package/src/api/messaging/markAsReadAll.js +1 -1
  33. package/src/api/messaging/markAsSeen.js +1 -1
  34. package/src/api/messaging/muteThread.js +1 -1
  35. package/src/api/messaging/resolvePhotoUrl.js +1 -1
  36. package/src/api/messaging/sendMessage.js +1 -1
  37. package/src/api/messaging/setTitle.js +1 -1
  38. package/src/api/messaging/unsendMessage.js +1 -1
  39. package/src/api/messaging/uploadAttachment.js +1 -1
  40. package/src/api/socket/core/connectMqtt.js +16 -8
  41. package/src/api/socket/core/emitAuth.js +4 -0
  42. package/src/api/socket/core/getSeqID.js +6 -8
  43. package/src/api/socket/core/getTaskResponseData.js +3 -0
  44. package/src/api/socket/core/parseDelta.js +9 -0
  45. package/src/api/socket/detail/buildStream.js +11 -4
  46. package/src/api/socket/detail/constants.js +4 -0
  47. package/src/api/socket/listenMqtt.js +11 -5
  48. package/src/api/threads/getThreadHistory.js +1 -1
  49. package/src/api/threads/getThreadInfo.js +245 -388
  50. package/src/api/threads/getThreadList.js +1 -1
  51. package/src/api/threads/getThreadPictures.js +1 -1
  52. package/src/api/users/getUserID.js +1 -1
  53. package/src/api/users/getUserInfo.js +80 -8
  54. package/src/database/models/thread.js +5 -0
  55. package/src/remote/remoteClient.js +123 -0
  56. package/src/utils/broadcast.js +51 -0
  57. package/src/utils/loginParser.js +19 -1
  58. package/src/utils/request.js +33 -6
  59. package/.gitattributes +0 -2
  60. package/Fca_Database/database.sqlite +0 -0
  61. package/LICENSE-MIT +0 -21
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
 
3
- const log = require("npmlog");
3
+ const log = require("../../../func/logAdapter");
4
4
  const { parseAndCheckLogin } = require("../../utils/client");
5
5
  const { formatID, getType } = require("../../utils/format");
6
6
  function createProfileUrl(url, username, id) {
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
 
3
- const log = require("npmlog");
3
+ const log = require("../../../func/logAdapter");
4
4
  const { parseAndCheckLogin } = require("../../utils/client");
5
5
  module.exports = function(defaultFuncs, api, ctx) {
6
6
  return function getThreadPictures(threadID, offset, limit, callback) {
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
 
3
- const log = require("npmlog");
3
+ const log = require("../../../func/logAdapter");
4
4
  const { formatID } = require("../../utils/format");
5
5
  function formatData(data) {
6
6
  return {
@@ -2,7 +2,7 @@
2
2
 
3
3
  const fs = require("fs");
4
4
  const path = require("path");
5
- const log = require("npmlog");
5
+ const log = require("../../../func/logAdapter");
6
6
  const logger = require("../../../func/logger");
7
7
  const { parseAndCheckLogin } = require("../../utils/client.js");
8
8
 
@@ -118,13 +118,81 @@ function mergeUserEntry(a, b) {
118
118
  };
119
119
  }
120
120
 
121
- const queue = [];
122
- let isProcessingQueue = false;
123
- const processingUsers = new Set();
124
- const queuedUsers = new Set();
125
- const cooldown = new Map();
126
-
127
121
  module.exports = function (defaultFuncs, api, ctx) {
122
+ // Read FastConfig-style anti-get-info flag from global config (if available)
123
+ const cfg = global.fca && global.fca.config;
124
+ const antiCfg = cfg && cfg.antiGetInfo;
125
+ const disableAntiUserInfo = !!(antiCfg && antiCfg.AntiGetUserInfo === true);
126
+
127
+ // Lightweight, Horizon-style implementation without database/queue
128
+ if (disableAntiUserInfo) {
129
+ function formatLegacyData(data) {
130
+ const retObj = {};
131
+ for (const prop in data) {
132
+ if (!Object.prototype.hasOwnProperty.call(data, prop)) continue;
133
+ const innerObj = data[prop] || {};
134
+ retObj[prop] = {
135
+ name: innerObj.name || null,
136
+ firstName: innerObj.firstName || null,
137
+ vanity: innerObj.vanity || null,
138
+ thumbSrc: innerObj.thumbSrc || null,
139
+ profileUrl: innerObj.uri || innerObj.profileUrl || null,
140
+ gender: innerObj.gender || null,
141
+ type: innerObj.type || null,
142
+ isFriend: !!innerObj.is_friend,
143
+ isBirthday: !!innerObj.is_birthday
144
+ };
145
+ }
146
+ return retObj;
147
+ }
148
+
149
+ return function getUserInfo(idsOrId, callback) {
150
+ let resolveFunc;
151
+ let rejectFunc;
152
+ const returnPromise = new Promise((resolve, reject) => {
153
+ resolveFunc = resolve;
154
+ rejectFunc = reject;
155
+ });
156
+
157
+ if (typeof callback !== "function") {
158
+ callback = (err, userInfo) => {
159
+ if (err) return rejectFunc(err);
160
+ resolveFunc(userInfo);
161
+ };
162
+ }
163
+
164
+ const ids = Array.isArray(idsOrId) ? idsOrId : [idsOrId];
165
+ const form = {};
166
+ ids.forEach((v, i) => {
167
+ form[`ids[${i}]`] = v;
168
+ });
169
+
170
+ defaultFuncs
171
+ .post("https://www.facebook.com/chat/user_info/", ctx.jar, form)
172
+ .then(parseAndCheckLogin(ctx, defaultFuncs))
173
+ .then(resData => {
174
+ if (resData.error) throw resData;
175
+ const profiles = resData?.payload?.profiles || {};
176
+ return callback(null, formatLegacyData(profiles));
177
+ })
178
+ .catch(err => {
179
+ log.error(
180
+ "getUserInfo",
181
+ "Lỗi: getUserInfo Có Thể Do Bạn Spam Quá Nhiều !,Hãy Thử Lại !"
182
+ );
183
+ return callback(err);
184
+ });
185
+
186
+ return returnPromise;
187
+ };
188
+ }
189
+
190
+ const queue = [];
191
+ let isProcessingQueue = false;
192
+ const processingUsers = new Set();
193
+ const queuedUsers = new Set();
194
+ const cooldown = new Map();
195
+
128
196
  const dbFiles = fs.readdirSync(path.join(__dirname, "../../database")).filter(f => path.extname(f) === ".js").reduce((acc, file) => {
129
197
  acc[path.basename(file, ".js")] = require(path.join(__dirname, "../../database", file))(api);
130
198
  return acc;
@@ -319,7 +387,11 @@ module.exports = function (defaultFuncs, api, ctx) {
319
387
  }
320
388
  return callback(null, ret);
321
389
  }).catch(err => {
322
- log.error("getUserInfo", "Error: " + (err?.message || "Unknown"));
390
+ // Horizon-style anti-get-info message to hint rate limiting / spam block
391
+ log.error(
392
+ "getUserInfo",
393
+ "Lỗi: getUserInfo Có Thể Do Bạn Spam Quá Nhiều !,Hãy Thử Lại !"
394
+ );
323
395
  callback(err);
324
396
  });
325
397
  return returnPromise;
@@ -16,6 +16,11 @@ module.exports = function(sequelize) {
16
16
  allowNull: false,
17
17
  unique: true
18
18
  },
19
+ messageCount: {
20
+ type: DataTypes.INTEGER,
21
+ allowNull: false,
22
+ defaultValue: 0
23
+ },
19
24
  data: {
20
25
  type: DataTypes.TEXT,
21
26
  allowNull: true,
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+
3
+ const WebSocket = require("ws");
4
+ const logger = require("../../func/logger");
5
+
6
+ function createRemoteClient(api, ctx, cfg) {
7
+ if (!cfg || !cfg.enabled || !cfg.url) return null;
8
+
9
+ const url = String(cfg.url);
10
+ const token = cfg.token ? String(cfg.token) : null;
11
+ const autoReconnect = cfg.autoReconnect !== false;
12
+ const emitter = ctx && ctx._emitter;
13
+
14
+ let ws = null;
15
+ let closed = false;
16
+ let reconnectTimer = null;
17
+
18
+ function log(message, level = "info") {
19
+ logger(`[remote] ${message}`, level);
20
+ }
21
+
22
+ function scheduleReconnect() {
23
+ if (!autoReconnect || closed) return;
24
+ if (reconnectTimer) return;
25
+ reconnectTimer = setTimeout(() => {
26
+ reconnectTimer = null;
27
+ if (!closed) connect();
28
+ }, 5000);
29
+ }
30
+
31
+ function safeEmit(event, payload) {
32
+ try {
33
+ if (emitter && typeof emitter.emit === "function") {
34
+ emitter.emit(event, payload);
35
+ }
36
+ } catch { }
37
+ }
38
+
39
+ function connect() {
40
+ try {
41
+ ws = new WebSocket(url, {
42
+ headers: token ? { Authorization: `Bearer ${token}` } : undefined
43
+ });
44
+ } catch (e) {
45
+ log(`connect error: ${e && e.message ? e.message : String(e)}`, "warn");
46
+ scheduleReconnect();
47
+ return;
48
+ }
49
+
50
+ ws.on("open", () => {
51
+ log("connected", "info");
52
+ const payload = {
53
+ type: "hello",
54
+ userID: ctx && ctx.userID,
55
+ region: ctx && ctx.region,
56
+ version: require("../../package.json").version
57
+ };
58
+ try {
59
+ ws.send(JSON.stringify(payload));
60
+ } catch { }
61
+ safeEmit("remoteConnected", payload);
62
+ });
63
+
64
+ ws.on("message", data => {
65
+ let msg;
66
+ try {
67
+ msg = JSON.parse(data.toString());
68
+ } catch {
69
+ return;
70
+ }
71
+ if (!msg || typeof msg !== "object") return;
72
+
73
+ switch (msg.type) {
74
+ case "ping":
75
+ try {
76
+ ws.send(JSON.stringify({ type: "pong" }));
77
+ } catch { }
78
+ break;
79
+ case "stop":
80
+ safeEmit("remoteStop", msg);
81
+ break;
82
+ case "broadcast":
83
+ safeEmit("remoteBroadcast", msg.payload || {});
84
+ break;
85
+ default:
86
+ safeEmit("remoteMessage", msg);
87
+ break;
88
+ }
89
+ });
90
+
91
+ ws.on("close", () => {
92
+ log("disconnected", "warn");
93
+ safeEmit("remoteDisconnected");
94
+ if (!closed) scheduleReconnect();
95
+ });
96
+
97
+ ws.on("error", err => {
98
+ log(`error: ${err && err.message ? err.message : String(err)}`, "warn");
99
+ });
100
+ }
101
+
102
+ connect();
103
+
104
+ return {
105
+ close() {
106
+ closed = true;
107
+ if (reconnectTimer) {
108
+ clearTimeout(reconnectTimer);
109
+ reconnectTimer = null;
110
+ }
111
+ try {
112
+ if (ws && ws.readyState === WebSocket.OPEN) {
113
+ ws.close();
114
+ }
115
+ } catch { }
116
+ }
117
+ };
118
+ }
119
+
120
+ module.exports = {
121
+ createRemoteClient
122
+ };
123
+
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+
3
+ const logger = require("../../func/logger");
4
+
5
+ function delay(ms) {
6
+ return new Promise(resolve => setTimeout(resolve, ms));
7
+ }
8
+
9
+ async function broadcast(api, threadIDs, message, options) {
10
+ const opts = options || {};
11
+ const delayMs = typeof opts.delayMs === "number" ? opts.delayMs : 1000;
12
+ const skipBlocked = opts.skipBlocked !== false;
13
+ const onResult = typeof opts.onResult === "function" ? opts.onResult : null;
14
+
15
+ if (!api || typeof api.sendMessage !== "function") {
16
+ throw new Error("broadcast: api.sendMessage is required.");
17
+ }
18
+
19
+ const ids = Array.isArray(threadIDs) ? threadIDs : [threadIDs];
20
+ const results = [];
21
+
22
+ for (const id of ids) {
23
+ try {
24
+ const res = await api.sendMessage(message, id);
25
+ const item = { threadID: id, ok: true, res };
26
+ results.push(item);
27
+ if (onResult) onResult(null, item);
28
+ } catch (e) {
29
+ const msg = e && e.error ? e.error : e && e.message ? e.message : String(e);
30
+ logger(`broadcast: failed for ${id}: ${msg}`, "warn");
31
+ const item = { threadID: id, ok: false, error: e };
32
+ results.push(item);
33
+ if (onResult) onResult(e, item);
34
+
35
+ if (
36
+ skipBlocked &&
37
+ /permission|blocked|not allowed|cannot send message|not authorized/i.test(msg)
38
+ ) {
39
+ // Skip only this target, continue with others
40
+ }
41
+ }
42
+ if (delayMs > 0) {
43
+ await delay(delayMs);
44
+ }
45
+ }
46
+
47
+ return results;
48
+ }
49
+
50
+ module.exports = broadcast;
51
+
@@ -20,6 +20,13 @@ function makeParsable(html) {
20
20
 
21
21
  function parseAndCheckLogin(ctx, http, retryCount = 0) {
22
22
  const delay = ms => new Promise(r => setTimeout(r, ms));
23
+ const emit = (event, payload) => {
24
+ try {
25
+ if (ctx && ctx._emitter && typeof ctx._emitter.emit === "function") {
26
+ ctx._emitter.emit(event, payload);
27
+ }
28
+ } catch { }
29
+ };
23
30
  const headerOf = (headers, name) => {
24
31
  if (!headers) return;
25
32
  const k = Object.keys(headers).find(k => k.toLowerCase() === name.toLowerCase());
@@ -59,11 +66,13 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
59
66
  // Set flag to prevent concurrent auto login attempts
60
67
  ctx.auto_login = true;
61
68
  logger("Login session expired, attempting auto login...", "warn");
69
+ emit("sessionExpired", { res: resData });
62
70
 
63
71
  try {
64
72
  const ok = await ctx.performAutoLogin();
65
73
  if (ok) {
66
74
  logger("Auto login successful! Retrying request...", "info");
75
+ emit("autoLoginSuccess", { res: resData });
67
76
  ctx.auto_login = false;
68
77
 
69
78
  // After successful auto login, retry the original request
@@ -128,6 +137,7 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
128
137
  const e = new Error("Not logged in. Auto login failed.");
129
138
  e.error = "Not logged in.";
130
139
  e.res = resData;
140
+ emit("autoLoginFailed", { error: e, res: resData });
131
141
  throw e;
132
142
  }
133
143
  } catch (autoLoginErr) {
@@ -147,6 +157,7 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
147
157
  e.error = "Not logged in.";
148
158
  e.res = resData;
149
159
  e.originalError = autoLoginErr;
160
+ emit("autoLoginFailed", { error: e, res: resData });
150
161
  throw e;
151
162
  }
152
163
  };
@@ -304,7 +315,9 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
304
315
  }
305
316
  if (parsed?.error === 1357001) {
306
317
  const err = new Error("Facebook blocked the login");
307
- err.error = "Not logged in.";
318
+ err.error = "login_blocked";
319
+ err.res = parsed;
320
+ emit("loginBlocked", { res: parsed });
308
321
  throw err;
309
322
  }
310
323
  const resData = parsed;
@@ -313,6 +326,7 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
313
326
  resStr.includes("XCheckpointFBScrapingWarningController") ||
314
327
  resStr.includes("601051028565049")
315
328
  ) {
329
+ emit("checkpoint", { type: "scraping_warning", res: resData });
316
330
  return await maybeAutoLogin(resData, res?.config);
317
331
  }
318
332
  if (
@@ -326,6 +340,8 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
326
340
  const err = new Error("Checkpoint 282 detected");
327
341
  err.error = "checkpoint_282";
328
342
  err.res = resData;
343
+ emit("checkpoint", { type: "282", res: resData });
344
+ emit("checkpoint_282", { res: resData });
329
345
  throw err;
330
346
  }
331
347
  if (resStr.includes("828281030927956")) {
@@ -333,6 +349,8 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
333
349
  const err = new Error("Checkpoint 956 detected");
334
350
  err.error = "checkpoint_956";
335
351
  err.res = resData;
352
+ emit("checkpoint", { type: "956", res: resData });
353
+ emit("checkpoint_956", { res: resData });
336
354
  throw err;
337
355
  }
338
356
  return parsed;
@@ -85,8 +85,15 @@ const client = wrapper(axios.create({
85
85
 
86
86
  const delay = ms => new Promise(r => setTimeout(r, ms));
87
87
 
88
- async function requestWithRetry(fn, retries = 3, baseDelay = 1000) {
88
+ async function requestWithRetry(fn, retries = 3, baseDelay = 1000, ctx) {
89
89
  let lastError;
90
+ const emit = (event, payload) => {
91
+ try {
92
+ if (ctx && ctx._emitter && typeof ctx._emitter.emit === "function") {
93
+ ctx._emitter.emit(event, payload);
94
+ }
95
+ } catch { }
96
+ };
90
97
  for (let i = 0; i < retries; i++) {
91
98
  try {
92
99
  return await fn();
@@ -104,6 +111,11 @@ async function requestWithRetry(fn, retries = 3, baseDelay = 1000) {
104
111
 
105
112
  // Don't retry on client errors (4xx) except 429 (rate limit)
106
113
  const status = e?.response?.status || e?.statusCode || 0;
114
+ const url = e?.config?.url || "";
115
+ const method = String(e?.config?.method || "").toUpperCase();
116
+ if (status === 429) {
117
+ emit("rateLimit", { status, url, method });
118
+ }
107
119
  if (status >= 400 && status < 500 && status !== 429) {
108
120
  return e.response || Promise.reject(e);
109
121
  }
@@ -111,6 +123,21 @@ async function requestWithRetry(fn, retries = 3, baseDelay = 1000) {
111
123
  if (i === retries - 1) {
112
124
  return e.response || Promise.reject(e);
113
125
  }
126
+ // Network errors (no status code)
127
+ const netCode = e?.code || "";
128
+ const msg = e && e.message ? e.message : String(e || "");
129
+ if (
130
+ !status &&
131
+ (netCode === "UND_ERR_CONNECT_TIMEOUT" ||
132
+ netCode === "ETIMEDOUT" ||
133
+ netCode === "ECONNRESET" ||
134
+ netCode === "ECONNREFUSED" ||
135
+ netCode === "ENOTFOUND" ||
136
+ /timeout|connect timeout|network error|fetch failed/i.test(msg))
137
+ ) {
138
+ emit("networkError", { code: netCode, message: msg, url, method });
139
+ }
140
+
114
141
  // Exponential backoff with jitter
115
142
  const backoffDelay = Math.min(
116
143
  baseDelay * Math.pow(2, i) + Math.floor(Math.random() * 200),
@@ -158,13 +185,13 @@ function isPairArrayList(arr) {
158
185
  return Array.isArray(arr) && arr.length > 0 && arr.every(x => Array.isArray(x) && x.length === 2 && typeof x[0] === "string");
159
186
  }
160
187
 
161
- function cleanGet(url) {
162
- return requestWithRetry(() => client.get(url, cfg()), 3, 1000);
188
+ function cleanGet(url, ctx) {
189
+ return requestWithRetry(() => client.get(url, cfg()), 3, 1000, ctx);
163
190
  }
164
191
 
165
192
  function get(url, reqJar, qs, options, ctx, customHeader) {
166
193
  const headers = getHeaders(url, options, ctx, customHeader);
167
- return requestWithRetry(() => client.get(url, cfg({ reqJar, headers, params: qs })), 3, 1000);
194
+ return requestWithRetry(() => client.get(url, cfg({ reqJar, headers, params: qs })), 3, 1000, ctx);
168
195
  }
169
196
 
170
197
  function post(url, reqJar, form, options, ctx, customHeader) {
@@ -197,7 +224,7 @@ function post(url, reqJar, form, options, ctx, customHeader) {
197
224
  data = p.toString();
198
225
  headers["Content-Type"] = "application/x-www-form-urlencoded";
199
226
  }
200
- return requestWithRetry(() => client.post(url, data, cfg({ reqJar, headers })), 3, 1000);
227
+ return requestWithRetry(() => client.post(url, data, cfg({ reqJar, headers })), 3, 1000, ctx);
201
228
  }
202
229
 
203
230
  async function postFormData(url, reqJar, form, qs, options, ctx) {
@@ -250,7 +277,7 @@ async function postFormData(url, reqJar, form, qs, options, ctx) {
250
277
  }
251
278
  }
252
279
  const headers = { ...getHeaders(url, options, ctx), ...fd.getHeaders() };
253
- return requestWithRetry(() => client.post(url, fd, cfg({ reqJar, headers, params: qs })), 3, 1000);
280
+ return requestWithRetry(() => client.post(url, fd, cfg({ reqJar, headers, params: qs })), 3, 1000, ctx);
254
281
  }
255
282
 
256
283
  function makeDefaults(html, userID, ctx) {
package/.gitattributes DELETED
@@ -1,2 +0,0 @@
1
- # Auto detect text files and perform LF normalization
2
- * text=auto
Binary file
package/LICENSE-MIT DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 DongDev (Donix-VN)
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.