sagor-fca 0.0.8 → 0.0.9

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.
@@ -1071,7 +1071,7 @@ function loginHelper(appState, Cookie, email, password, globalOptions, callback)
1071
1071
  const userDataMatch = String(html).match(/\["CurrentUserInitialData",\[\],({.*?}),\d+\]/);
1072
1072
  if (userDataMatch) {
1073
1073
  const info = JSON.parse(userDataMatch[1]);
1074
- logger(`Đăng nhập tài khoản: ${info.NAME} (${info.USER_ID})`, "info");
1074
+ logger(`Log in to your account: ${info.NAME} (${info.USER_ID})`, "info");
1075
1075
 
1076
1076
  // Check if Facebook response shows USER_ID = 0 (session dead)
1077
1077
  if (!isValidUID(info.USER_ID)) {
@@ -1120,7 +1120,7 @@ function loginHelper(appState, Cookie, email, password, globalOptions, callback)
1120
1120
  logger(`Database connection failed: ${errorMsg}`, "warn");
1121
1121
  }
1122
1122
  });
1123
- logger("FCA fix/update by DongDev (Donix-VN)", "info");
1123
+ logger("FCA FIXED BY SAGOR 🍒", "info");
1124
1124
  const emitter = new EventEmitter();
1125
1125
  const ctxMain = {
1126
1126
  userID,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sagor-fca",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "The most powerful and updated Facebook Chat API (FCA) with reverse-engineered fixes for GraphQL and MQTT. Enhanced by SaGor.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -0,0 +1,173 @@
1
+ // ============================================================
2
+ // SAGOR-FCA v2.0 — send VoicebMessage
3
+ // © 2025 Sagor. All Rights Reserved.
4
+ // ============================================================
5
+
6
+ const fs = require("fs-extra");
7
+ const path = require("path");
8
+ const axios = require("axios");
9
+
10
+ module.exports = function (defaultFuncs, api, ctx) {
11
+
12
+ const sleep = ms => new Promise(res => setTimeout(res, ms));
13
+
14
+ return async function sendVoiceMessage(threadID, input, options = {}) {
15
+ try {
16
+ if (!threadID) throw new Error("threadID required");
17
+ if (!input) throw new Error("input required");
18
+
19
+ const {
20
+ delay = 800,
21
+ retry = 2,
22
+ parallel = 1,
23
+ voice = "en-US",
24
+ onProgress = null
25
+ } = options;
26
+
27
+ const list = Array.isArray(input) ? input : [input];
28
+
29
+ let success = 0;
30
+ let failed = 0;
31
+
32
+ const queue = [...list];
33
+
34
+ async function worker() {
35
+ while (queue.length) {
36
+ const item = queue.shift();
37
+ if (!item) continue;
38
+
39
+ let filePath;
40
+
41
+ try {
42
+ if (isURL(item)) {
43
+ filePath = await downloadFile(item);
44
+ } else {
45
+ filePath = await textToVoice(item, voice);
46
+ }
47
+
48
+ const ok = await sendAudio(defaultFuncs, ctx, threadID, filePath, retry);
49
+ ok ? success++ : failed++;
50
+
51
+ fs.unlinkSync(filePath);
52
+
53
+ } catch (e) {
54
+ failed++;
55
+ }
56
+
57
+ if (onProgress && typeof onProgress === "function") {
58
+ onProgress({ success, failed, remaining: queue.length });
59
+ }
60
+
61
+ await sleep(delay);
62
+ }
63
+ }
64
+
65
+ const workers = [];
66
+ for (let i = 0; i < parallel; i++) {
67
+ workers.push(worker());
68
+ }
69
+
70
+ await Promise.all(workers);
71
+
72
+ return {
73
+ total: list.length,
74
+ success,
75
+ failed
76
+ };
77
+
78
+ } catch (e) {
79
+ return {
80
+ total: 0,
81
+ success: 0,
82
+ failed: 0
83
+ };
84
+ }
85
+ };
86
+ };
87
+
88
+ function isURL(str) {
89
+ return /^https?:\/\//.test(str);
90
+ }
91
+
92
+ async function downloadFile(url) {
93
+ const filePath = path.join(__dirname, "cache", `voice_${Date.now()}.mp3`);
94
+ const res = await axios({
95
+ url,
96
+ method: "GET",
97
+ responseType: "stream"
98
+ });
99
+
100
+ const writer = fs.createWriteStream(filePath);
101
+ res.data.pipe(writer);
102
+
103
+ return new Promise((resolve, reject) => {
104
+ writer.on("finish", () => resolve(filePath));
105
+ writer.on("error", reject);
106
+ });
107
+ }
108
+
109
+ async function textToVoice(text, lang) {
110
+ const filePath = path.join(__dirname, "cache", `tts_${Date.now()}.mp3`);
111
+
112
+ const url = `https://translate.google.com/translate_tts?ie=UTF-8&q=${encodeURIComponent(text)}&tl=${lang}&client=tw-ob`;
113
+
114
+ const res = await axios({
115
+ url,
116
+ method: "GET",
117
+ responseType: "stream"
118
+ });
119
+
120
+ const writer = fs.createWriteStream(filePath);
121
+ res.data.pipe(writer);
122
+
123
+ return new Promise((resolve, reject) => {
124
+ writer.on("finish", () => resolve(filePath));
125
+ writer.on("error", reject);
126
+ });
127
+ }
128
+
129
+ async function sendAudio(defaultFuncs, ctx, threadID, filePath, retry) {
130
+ let attempt = 0;
131
+
132
+ while (attempt <= retry) {
133
+ try {
134
+ const form = {
135
+ av: ctx.userID,
136
+ fb_api_req_friendly_name: "MessageSendMutation",
137
+ fb_api_caller_class: "RelayModern",
138
+ doc_id: "6155357817922795",
139
+ variables: JSON.stringify({
140
+ input: {
141
+ actor_id: ctx.userID,
142
+ message: {
143
+ audio: {
144
+ filename: "voice.mp3"
145
+ }
146
+ },
147
+ thread_id: threadID,
148
+ offline_threading_id: Date.now().toString()
149
+ }
150
+ })
151
+ };
152
+
153
+ const formData = {
154
+ ...form,
155
+ file: fs.createReadStream(filePath)
156
+ };
157
+
158
+ await defaultFuncs.postFormData(
159
+ "https://www.facebook.com/messaging/send/",
160
+ ctx.jar,
161
+ formData
162
+ );
163
+
164
+ return true;
165
+
166
+ } catch (e) {
167
+ attempt++;
168
+ await new Promise(res => setTimeout(res, 700));
169
+ }
170
+ }
171
+
172
+ return false;
173
+ }
@@ -6,41 +6,70 @@
6
6
 
7
7
  const log = require("../../../func/logAdapter");
8
8
  const { parseAndCheckLogin } = require("../../utils/client");
9
-
10
9
  module.exports = function(defaultFuncs, api, ctx) {
11
10
  return function getThreadPictures(threadID, offset, limit, callback) {
12
- let resolve, reject;
13
- const p = new Promise((res, rej) => { resolve = res; reject = rej; });
14
- callback = callback || ((err, data) => err ? reject(err) : resolve(data));
11
+ let resolveFunc = function() {};
12
+ let rejectFunc = function() {};
13
+ const returnPromise = new Promise(function(resolve, reject) {
14
+ resolveFunc = resolve;
15
+ rejectFunc = reject;
16
+ });
17
+
18
+ if (!callback) {
19
+ callback = function(err, friendList) {
20
+ if (err) {
21
+ return rejectFunc(err);
22
+ }
23
+ resolveFunc(friendList);
24
+ };
25
+ }
15
26
 
16
- defaultFuncs.post(
17
- "https://www.facebook.com/ajax/messaging/attachments/sharedphotos.php",
18
- ctx.jar,
19
- { thread_id: threadID, offset, limit }
20
- )
27
+ let form = {
28
+ thread_id: threadID,
29
+ offset: offset,
30
+ limit: limit
31
+ };
32
+
33
+ defaultFuncs
34
+ .post(
35
+ "https://www.facebook.com/ajax/messaging/attachments/sharedphotos.php",
36
+ ctx.jar,
37
+ form
38
+ )
21
39
  .then(parseAndCheckLogin(ctx, defaultFuncs))
22
- .then(res => {
23
- if (res.error) throw res;
40
+ .then(function(resData) {
41
+ if (resData.error) {
42
+ throw resData;
43
+ }
24
44
  return Promise.all(
25
- (res.payload?.imagesData || []).map(image =>
26
- defaultFuncs.post(
27
- "https://www.facebook.com/ajax/messaging/attachments/sharedphotos.php",
28
- ctx.jar, { thread_id: threadID, image_id: image.fbid }
29
- )
45
+ resData.payload.imagesData.map(function(image) {
46
+ form = {
47
+ thread_id: threadID,
48
+ image_id: image.fbid
49
+ };
50
+ return defaultFuncs
51
+ .post(
52
+ "https://www.facebook.com/ajax/messaging/attachments/sharedphotos.php",
53
+ ctx.jar,
54
+ form
55
+ )
30
56
  .then(parseAndCheckLogin(ctx, defaultFuncs))
31
- .then(r => {
32
- const qID = r.jsmods?.require?.[0]?.[3]?.[1]?.query_metadata?.query_path?.[0]?.message_thread;
33
- return r.jsmods?.require?.[0]?.[3]?.[1]?.query_results?.[qID]?.message_images?.edges?.[0]?.node?.image2;
34
- })
35
- )
57
+ .then(function(resData) {
58
+ if (resData.error) {
59
+ throw resData;
60
+ }
61
+ // the response is pretty messy
62
+ const queryThreadID =
63
+ resData.jsmods.require[0][3][1].query_metadata.query_path[0]
64
+ .message_thread;
65
+ const imageData =
66
+ resData.jsmods.require[0][3][1].query_results[queryThreadID]
67
+ .message_images.edges[0].node.image2;
68
+ return imageData;
69
+ });
70
+ })
36
71
  );
37
72
  })
38
- .then(data => callback(null, data))
39
- .catch(err => { log.error("getThreadPictures", err); callback(err); });
40
-
41
- return p;
42
- };
43
- };
44
73
  .then(function(resData) {
45
74
  callback(null, resData);
46
75
  })
@@ -1,36 +1,53 @@
1
- // ============================================================
2
- // SAGOR-FCA v2.0 — Database Helpers
3
- // © 2025 Sagor. All Rights Reserved.
4
- // ============================================================
5
1
  "use strict";
6
2
 
3
+ /**
4
+ * Shared helpers for database layer (userData, threadData).
5
+ * Keeps validation and payload normalization in one place.
6
+ */
7
+
7
8
  const DB_NOT_INIT = "Database not initialized";
8
9
 
9
10
  function validateId(value, fieldName = "id") {
10
- if (value == null) throw new Error(`${fieldName} Desirable`);
11
- if (typeof value !== "string" && typeof value !== "number")
12
- throw new Error(`${fieldName} It should be string or number`);
11
+ if (value == null) {
12
+ throw new Error(`${fieldName} is required and cannot be undefined`);
13
+ }
14
+ if (typeof value !== "string" && typeof value !== "number") {
15
+ throw new Error(`Invalid ${fieldName}: must be a string or number`);
16
+ }
13
17
  return String(value);
14
18
  }
15
19
 
16
20
  function validateData(data) {
17
- if (!data || typeof data !== "object" || Array.isArray(data))
18
- throw new Error("data The object must not be empty.");
21
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
22
+ throw new Error("Invalid data: must be a non-empty object");
23
+ }
19
24
  }
20
25
 
26
+ /**
27
+ * @param {string|string[]|null} keys - "userID" | ["userID","data"] | null
28
+ * @returns {string[]|undefined}
29
+ */
21
30
  function normalizeAttributes(keys) {
22
31
  if (keys == null) return undefined;
23
32
  return typeof keys === "string" ? [keys] : Array.isArray(keys) ? keys : undefined;
24
33
  }
25
34
 
35
+ /**
36
+ * Normalize payload: accept either { data } or raw object.
37
+ */
26
38
  function normalizePayload(data, key = "data") {
27
39
  return Object.prototype.hasOwnProperty.call(data, key) ? data : { [key]: data };
28
40
  }
29
41
 
30
- function wrapError(msg, err) {
31
- const e = new Error(`${msg}: ${err?.message || String(err)}`);
32
- e.original = err;
33
- return e;
42
+ function wrapError(message, cause) {
43
+ return new Error(`${message}: ${cause && cause.message ? cause.message : cause}`);
34
44
  }
35
45
 
36
- module.exports = { DB_NOT_INIT, validateId, validateData, normalizeAttributes, normalizePayload, wrapError };
46
+ module.exports = {
47
+ DB_NOT_INIT,
48
+ validateId,
49
+ validateData,
50
+ normalizeAttributes,
51
+ normalizePayload,
52
+ wrapError
53
+ };
@@ -1,55 +1,88 @@
1
- // ============================================================
2
- // SAGOR-FCA v2.0 — Database Models
3
- // © 2025 Sagor. All Rights Reserved.
4
- // ============================================================
5
- "use strict";
6
-
7
1
  const { Sequelize } = require("sequelize");
8
- const fs = require("fs");
2
+ const fs = require("fs");
9
3
  const path = require("path");
10
4
 
11
5
  let sequelize = null;
12
- let models = {};
6
+ let models = {};
13
7
 
14
8
  try {
15
- const dbPath = path.join(process.cwd(), "SAGOR-FCA");
16
- if (!fs.existsSync(dbPath)) fs.mkdirSync(dbPath, { recursive: true });
9
+ const databasePath = path.join(process.cwd(), "Sagor_Database");
10
+ if (!fs.existsSync(databasePath)) {
11
+ fs.mkdirSync(databasePath, { recursive: true });
12
+ }
17
13
 
18
14
  sequelize = new Sequelize({
19
- dialect: "sqlite",
20
- storage: path.join(dbPath, "sagor.sqlite"),
21
- logging: false,
22
- pool: { max: 5, min: 0, acquire: 30000, idle: 10000 },
23
- retry: { max: 3 },
24
- dialectOptions: { timeout: 5000 },
15
+ dialect: "sqlite",
16
+ storage: path.join(databasePath, "sagor.sqlite"),
17
+ logging: false,
18
+ pool: {
19
+ max: 5,
20
+ min: 0,
21
+ acquire: 30000,
22
+ idle: 10000
23
+ },
24
+ retry: {
25
+ max: 3
26
+ },
27
+ dialectOptions: {
28
+ timeout: 5000
29
+ },
25
30
  isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.READ_COMMITTED
26
31
  });
27
32
 
28
- const files = fs.readdirSync(__dirname).filter(f => f.endsWith(".js") && f !== "index.js");
29
- for (const file of files) {
30
- try {
31
- const model = require(path.join(__dirname, file))(sequelize);
32
- if (model?.name) models[model.name] = model;
33
- } catch (e) {
34
- console.error(`[ SAGOR ] Model loading failed ${file}:`, e?.message || e);
35
- }
36
- }
33
+ // Load models with error handling
34
+ try {
35
+ const modelFiles = fs.readdirSync(__dirname)
36
+ .filter(file => file.endsWith(".js") && file !== "index.js");
37
37
 
38
- // Associate models if method exists
39
- for (const name of Object.keys(models)) {
40
- if (typeof models[name].associate === "function") {
41
- try { models[name].associate(models); } catch (_) {}
38
+ for (const file of modelFiles) {
39
+ try {
40
+ const model = require(path.join(__dirname, file))(sequelize);
41
+ if (model && model.name) {
42
+ models[model.name] = model;
43
+ }
44
+ } catch (modelError) {
45
+ // Log but continue loading other models
46
+ console.error(`Failed to load model ${file}:`, modelError && modelError.message ? modelError.message : String(modelError));
47
+ }
42
48
  }
43
- }
44
49
 
45
- sequelize.sync({ alter: false }).catch(e => {
46
- console.error("[ SAGOR ] Database sync error:", e?.message || e);
47
- });
50
+ // Associate models
51
+ Object.keys(models).forEach(modelName => {
52
+ try {
53
+ if (models[modelName].associate) {
54
+ models[modelName].associate(models);
55
+ }
56
+ } catch (assocError) {
57
+ console.error(`Failed to associate model ${modelName}:`, assocError && assocError.message ? assocError.message : String(assocError));
58
+ }
59
+ });
60
+ } catch (loadError) {
61
+ console.error("Failed to load models:", loadError && loadError.message ? loadError.message : String(loadError));
62
+ }
48
63
 
49
- } catch (e) {
50
- console.error("[ SAGOR ] Database init error:", e?.message || e);
51
- sequelize = null;
52
- models = {};
64
+ models.sequelize = sequelize;
65
+ models.Sequelize = Sequelize;
66
+ models.isReady = true;
67
+ models.syncAll = async () => {
68
+ try {
69
+ if (!sequelize) {
70
+ throw new Error("Sequelize instance not initialized");
71
+ }
72
+ await sequelize.sync({ force: false });
73
+ } catch (error) {
74
+ console.error("Failed to synchronize models:", error && error.message ? error.message : String(error));
75
+ throw error;
76
+ }
77
+ };
78
+ } catch (initError) {
79
+ console.error("Database initialization error:", initError && initError.message ? initError.message : String(initError));
80
+ models.sequelize = null;
81
+ models.Sequelize = Sequelize;
82
+ models.isReady = false;
83
+ models.syncAll = async () => {
84
+ throw new Error("Database not initialized");
85
+ };
53
86
  }
54
87
 
55
- module.exports = { sequelize, models, Thread: models.Thread || null, User: models.User || null };
88
+ module.exports = models;
@@ -2,43 +2,53 @@
2
2
  // SAGOR-FCA v2.0 — Thread Model
3
3
  // © 2025 sagor. All Rights Reserved.
4
4
  // ============================================================
5
- "use strict";
6
-
7
5
  module.exports = function(sequelize) {
8
6
  const { Model, DataTypes } = require("sequelize");
9
7
 
10
8
  class Thread extends Model {}
11
9
 
12
- Thread.init({
13
- num: {
14
- type: DataTypes.INTEGER,
15
- allowNull: false,
16
- autoIncrement: true,
17
- primaryKey: true
18
- },
19
- threadID: {
20
- type: DataTypes.STRING,
21
- allowNull: false,
22
- unique: true
23
- },
24
- messageCount: {
25
- type: DataTypes.INTEGER,
26
- allowNull: false,
27
- defaultValue: 0
28
- },
29
- data: {
30
- type: DataTypes.TEXT,
31
- allowNull: true,
32
- get() {
33
- const v = this.getDataValue("data");
34
- if (typeof v === "string") { try { return JSON.parse(v); } catch { return v; } }
35
- return v;
10
+ Thread.init(
11
+ {
12
+ num: {
13
+ type: DataTypes.INTEGER,
14
+ allowNull: false,
15
+ autoIncrement: true,
16
+ primaryKey: true
36
17
  },
37
- set(v) {
38
- this.setDataValue("data", typeof v === "string" ? v : JSON.stringify(v));
18
+ threadID: {
19
+ type: DataTypes.STRING,
20
+ allowNull: false,
21
+ unique: true
22
+ },
23
+ messageCount: {
24
+ type: DataTypes.INTEGER,
25
+ allowNull: false,
26
+ defaultValue: 0
27
+ },
28
+ data: {
29
+ type: DataTypes.TEXT,
30
+ allowNull: true,
31
+ get() {
32
+ const value = this.getDataValue('data');
33
+ if (typeof value === 'string') {
34
+ try {
35
+ return JSON.parse(value);
36
+ } catch {
37
+ return value;
38
+ }
39
+ }
40
+ return value;
41
+ },
42
+ set(value) {
43
+ this.setDataValue('data', typeof value === 'string' ? value : JSON.stringify(value));
44
+ }
39
45
  }
46
+ },
47
+ {
48
+ sequelize,
49
+ modelName: "Thread",
50
+ timestamps: true
40
51
  }
41
- }, { sequelize, modelName: "Thread", timestamps: true });
42
-
52
+ );
43
53
  return Thread;
44
54
  };
@@ -2,38 +2,49 @@
2
2
  // SAGOR-FCA v2.0 — User Model
3
3
  // © 2025 Sagor. All Rights Reserved.
4
4
  // ============================================================
5
- "use strict";
6
-
7
- module.exports = function(sequelize) {
5
+ module.exports = function (sequelize) {
8
6
  const { Model, DataTypes } = require("sequelize");
9
7
 
10
- class User extends Model {}
8
+ class User extends Model { }
11
9
 
12
- User.init({
13
- num: {
14
- type: DataTypes.INTEGER,
15
- allowNull: false,
16
- autoIncrement: true,
17
- primaryKey: true
18
- },
19
- userID: {
20
- type: DataTypes.STRING,
21
- allowNull: false,
22
- unique: true
23
- },
24
- data: {
25
- type: DataTypes.TEXT,
26
- allowNull: true,
27
- get() {
28
- const v = this.getDataValue("data");
29
- if (typeof v === "string") { try { return JSON.parse(v); } catch { return v; } }
30
- return v;
10
+ User.init(
11
+ {
12
+ num: {
13
+ type: DataTypes.INTEGER,
14
+ allowNull: false,
15
+ autoIncrement: true,
16
+ primaryKey: true
31
17
  },
32
- set(v) {
33
- this.setDataValue("data", typeof v === "string" ? v : JSON.stringify(v));
18
+ userID: {
19
+ type: DataTypes.STRING,
20
+ allowNull: false,
21
+ unique: true
22
+ },
23
+ data: {
24
+ type: DataTypes.TEXT,
25
+ allowNull: true,
26
+ get() {
27
+ const value = this.getDataValue('data');
28
+ if (typeof value === 'string') {
29
+ try {
30
+ return JSON.parse(value);
31
+ } catch {
32
+ return value;
33
+ }
34
+ }
35
+ return value;
36
+ },
37
+ set(value) {
38
+ this.setDataValue('data', typeof value === 'string' ? value : JSON.stringify(value));
39
+ }
34
40
  }
41
+ },
42
+ {
43
+ sequelize,
44
+ modelName: "User",
45
+ timestamps: true
35
46
  }
36
- }, { sequelize, modelName: "User", timestamps: true });
47
+ );
37
48
 
38
49
  return User;
39
50
  };
@@ -1,92 +1,94 @@
1
- // ============================================================
2
- // SAGOR-FCA v2.0 — Thread Data
3
- // © 2025 SAGOR. All Rights Reserved.
4
- // ============================================================
5
1
  "use strict";
6
2
 
7
3
  const models = require("./models");
8
- const { DB_NOT_INIT, validateId, validateData, normalizeAttributes, wrapError } = require("./helpers");
4
+ const {
5
+ DB_NOT_INIT,
6
+ validateId,
7
+ validateData,
8
+ normalizeAttributes,
9
+ wrapError
10
+ } = require("./helpers");
9
11
 
10
- const Thread = models.Thread;
12
+ const Thread = models.Thread;
11
13
  const ID_FIELD = "threadID";
12
14
 
13
- module.exports = function(bot) {
15
+ module.exports = function (bot) {
14
16
  return {
15
17
  async create(threadID, data) {
16
- if (!Thread) return { thread: { threadID: validateId(threadID, ID_FIELD), ...(data || {}) }, created: true };
18
+ if (!Thread) {
19
+ return { thread: { threadID: validateId(threadID, ID_FIELD), ...(data || {}) }, created: true };
20
+ }
17
21
  try {
18
- threadID = validateId(threadID, ID_FIELD);
19
- let thread = await Thread.findOne({ where: { threadID } });
22
+ threadID = validateId(threadID, ID_FIELD);
23
+ let thread = await Thread.findOne({ where: { threadID } });
20
24
  if (thread) return { thread: thread.get(), created: false };
21
25
  thread = await Thread.create({ threadID, ...(data || {}) });
22
26
  return { thread: thread.get(), created: true };
23
- } catch (err) { throw wrapError("Failed to create thread", err); }
27
+ } catch (err) {
28
+ throw wrapError("Failed to create thread", err);
29
+ }
24
30
  },
25
31
 
26
32
  async get(threadID) {
27
33
  if (!Thread) return null;
28
34
  try {
29
- threadID = validateId(threadID, ID_FIELD);
35
+ threadID = validateId(threadID, ID_FIELD);
30
36
  const thread = await Thread.findOne({ where: { threadID } });
31
37
  return thread ? thread.get() : null;
32
- } catch (err) { throw wrapError("Failed to get thread", err); }
38
+ } catch (err) {
39
+ throw wrapError("Failed to get thread", err);
40
+ }
33
41
  },
34
42
 
35
43
  async update(threadID, data) {
36
- if (!Thread) return { thread: { threadID: validateId(threadID, ID_FIELD), ...(data || {}) }, created: false };
44
+ if (!Thread) {
45
+ return { thread: { threadID: validateId(threadID, ID_FIELD), ...(data || {}) }, created: false };
46
+ }
37
47
  try {
38
- threadID = validateId(threadID, ID_FIELD);
48
+ threadID = validateId(threadID, ID_FIELD);
39
49
  validateData(data);
40
50
  const thread = await Thread.findOne({ where: { threadID } });
41
- if (thread) { await thread.update(data); return { thread: thread.get(), created: false }; }
42
- const newT = await Thread.create({ ...data, threadID });
43
- return { thread: newT.get(), created: true };
44
- } catch (err) { throw wrapError("Failed to update thread", err); }
51
+ if (thread) {
52
+ await thread.update(data);
53
+ return { thread: thread.get(), created: false };
54
+ }
55
+ const newThread = await Thread.create({ ...data, threadID });
56
+ return { thread: newThread.get(), created: true };
57
+ } catch (err) {
58
+ throw wrapError("Failed to update thread", err);
59
+ }
45
60
  },
46
61
 
47
62
  async del(threadID) {
48
63
  if (!Thread) throw new Error(DB_NOT_INIT);
49
64
  try {
50
- threadID = validateId(threadID, ID_FIELD);
65
+ threadID = validateId(threadID, ID_FIELD);
51
66
  const result = await Thread.destroy({ where: { threadID } });
52
67
  if (result === 0) throw new Error("No thread found with the specified threadID");
53
68
  return result;
54
- } catch (err) { throw wrapError("Failed to delete thread", err); }
69
+ } catch (err) {
70
+ throw wrapError("Failed to delete thread", err);
71
+ }
55
72
  },
56
73
 
57
74
  async delAll() {
58
75
  if (!Thread) return 0;
59
- try { return await Thread.destroy({ where: {} }); }
60
- catch (err) { throw wrapError("Failed to delete all threads", err); }
61
- },
62
-
63
- async getAll(options = {}) {
64
- if (!Thread) return [];
65
76
  try {
66
- const query = {};
67
- if (options.attributes) query.attributes = normalizeAttributes(options.attributes);
68
- if (options.limit) query.limit = options.limit;
69
- if (options.offset) query.offset = options.offset;
70
- const threads = await Thread.findAll(query);
71
- return threads.map(t => t.get());
72
- } catch (err) { throw wrapError("Failed to get all threads", err); }
77
+ return await Thread.destroy({ where: {} });
78
+ } catch (err) {
79
+ throw wrapError("Failed to delete all threads", err);
80
+ }
73
81
  },
74
82
 
75
- async count() {
76
- if (!Thread) return 0;
77
- try { return await Thread.count(); }
78
- catch (err) { throw wrapError("Failed to count threads", err); }
79
- },
80
-
81
- async incrementMessageCount(threadID, amount = 1) {
82
- if (!Thread) return null;
83
+ async getAll(keys = null) {
84
+ if (!Thread) return [];
83
85
  try {
84
- threadID = validateId(threadID, ID_FIELD);
85
- const thread = await Thread.findOne({ where: { threadID } });
86
- if (!thread) return null;
87
- await thread.increment("messageCount", { by: amount });
88
- return thread.get();
89
- } catch (err) { throw wrapError("Failed to increment message count", err); }
86
+ const attributes = normalizeAttributes(keys);
87
+ const rows = await Thread.findAll({ attributes });
88
+ return rows.map((t) => t.get());
89
+ } catch (err) {
90
+ throw wrapError("Failed to get all threads", err);
91
+ }
90
92
  }
91
93
  };
92
94
  };
@@ -1,88 +1,98 @@
1
- // ============================================================
2
- // SAGOR-FCA v2.0 — User Data
3
- // © 2025 Sagor. All Rights Reserved.
4
- // ============================================================
5
1
  "use strict";
6
2
 
7
3
  const models = require("./models");
8
- const { DB_NOT_INIT, validateId, validateData, normalizeAttributes, normalizePayload, wrapError } = require("./helpers");
4
+ const {
5
+ DB_NOT_INIT,
6
+ validateId,
7
+ validateData,
8
+ normalizeAttributes,
9
+ normalizePayload,
10
+ wrapError
11
+ } = require("./helpers");
9
12
 
10
- const User = models.User;
13
+ const User = models.User;
11
14
  const ID_FIELD = "userID";
12
15
 
13
- function stub(userID, data) {
16
+ function stubUser(userID, data) {
14
17
  return { user: { userID, ...normalizePayload(data || {}, "data") }, created: true };
15
18
  }
16
19
 
17
- module.exports = function(bot) {
20
+ module.exports = function (bot) {
18
21
  return {
19
22
  async create(userID, data) {
20
- if (!User) return stub(validateId(userID, ID_FIELD), data);
23
+ if (!User) return stubUser(validateId(userID, ID_FIELD), data);
21
24
  try {
22
- userID = validateId(userID, ID_FIELD);
25
+ userID = validateId(userID, ID_FIELD);
23
26
  validateData(data);
24
27
  const payload = normalizePayload(data, "data");
25
- let user = await User.findOne({ where: { userID } });
28
+ let user = await User.findOne({ where: { userID } });
26
29
  if (user) return { user: user.get(), created: false };
27
30
  user = await User.create({ userID, ...payload });
28
31
  return { user: user.get(), created: true };
29
- } catch (err) { throw wrapError("Failed to create user", err); }
32
+ } catch (err) {
33
+ throw wrapError("Failed to create user", err);
34
+ }
30
35
  },
31
36
 
32
37
  async get(userID) {
33
38
  if (!User) return null;
34
39
  try {
35
- userID = validateId(userID, ID_FIELD);
40
+ userID = validateId(userID, ID_FIELD);
36
41
  const user = await User.findOne({ where: { userID } });
37
42
  return user ? user.get() : null;
38
- } catch (err) { throw wrapError("Failed to get user", err); }
43
+ } catch (err) {
44
+ throw wrapError("Failed to get user", err);
45
+ }
39
46
  },
40
47
 
41
48
  async update(userID, data) {
42
49
  if (!User) return { user: { userID: validateId(userID, ID_FIELD), ...normalizePayload(data || {}, "data") }, created: false };
43
50
  try {
44
- userID = validateId(userID, ID_FIELD);
51
+ userID = validateId(userID, ID_FIELD);
45
52
  validateData(data);
46
53
  const payload = normalizePayload(data, "data");
47
- const user = await User.findOne({ where: { userID } });
48
- if (user) { await user.update(payload); return { user: user.get(), created: false }; }
49
- const newU = await User.create({ userID, ...payload });
50
- return { user: newU.get(), created: true };
51
- } catch (err) { throw wrapError("Failed to update user", err); }
54
+ const user = await User.findOne({ where: { userID } });
55
+ if (user) {
56
+ await user.update(payload);
57
+ return { user: user.get(), created: false };
58
+ }
59
+ const newUser = await User.create({ userID, ...payload });
60
+ return { user: newUser.get(), created: true };
61
+ } catch (err) {
62
+ throw wrapError("Failed to update user", err);
63
+ }
52
64
  },
53
65
 
54
66
  async del(userID) {
55
67
  if (!User) throw new Error(DB_NOT_INIT);
56
68
  try {
57
- userID = validateId(userID, ID_FIELD);
69
+ userID = validateId(userID, ID_FIELD);
58
70
  const result = await User.destroy({ where: { userID } });
59
71
  if (result === 0) throw new Error("No user found with the specified userID");
60
72
  return result;
61
- } catch (err) { throw wrapError("Failed to delete user", err); }
73
+ } catch (err) {
74
+ throw wrapError("Failed to delete user", err);
75
+ }
62
76
  },
63
77
 
64
78
  async delAll() {
65
79
  if (!User) return 0;
66
- try { return await User.destroy({ where: {} }); }
67
- catch (err) { throw wrapError("Failed to delete all users", err); }
80
+ try {
81
+ return await User.destroy({ where: {} });
82
+ } catch (err) {
83
+ throw wrapError("Failed to delete all users", err);
84
+ }
68
85
  },
69
86
 
70
- async getAll(options = {}) {
87
+ async getAll(keys = null) {
71
88
  if (!User) return [];
72
89
  try {
73
- const query = {};
74
- if (options.attributes) query.attributes = normalizeAttributes(options.attributes);
75
- if (options.limit) query.limit = options.limit;
76
- if (options.offset) query.offset = options.offset;
77
- const users = await User.findAll(query);
78
- return users.map(u => u.get());
79
- } catch (err) { throw wrapError("Failed to get all users", err); }
80
- },
81
-
82
- async count() {
83
- if (!User) return 0;
84
- try { return await User.count(); }
85
- catch (err) { throw wrapError("Failed to count users", err); }
90
+ const attributes = normalizeAttributes(keys);
91
+ const rows = await User.findAll({ attributes });
92
+ return rows.map((u) => u.get());
93
+ } catch (err) {
94
+ throw wrapError("Failed to get all users", err);
95
+ }
86
96
  }
87
97
  };
88
98
  };