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.
- package/module/loginHelper.js +2 -2
- package/package.json +1 -1
- package/src/api/messaging/sendVoiceMessage.js +173 -0
- package/src/api/threads/getThreadPictures.js +56 -27
- package/src/database/helpers.js +31 -14
- package/src/database/models/index.js +71 -38
- package/src/database/models/thread.js +40 -30
- package/src/database/models/user.js +37 -26
- package/src/database/threadData.js +50 -48
- package/src/database/userData.js +48 -38
package/module/loginHelper.js
CHANGED
|
@@ -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(
|
|
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
|
|
1123
|
+
logger("FCA FIXED BY SAGOR 🍒", "info");
|
|
1124
1124
|
const emitter = new EventEmitter();
|
|
1125
1125
|
const ctxMain = {
|
|
1126
1126
|
userID,
|
package/package.json
CHANGED
|
@@ -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
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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(
|
|
23
|
-
if (
|
|
40
|
+
.then(function(resData) {
|
|
41
|
+
if (resData.error) {
|
|
42
|
+
throw resData;
|
|
43
|
+
}
|
|
24
44
|
return Promise.all(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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(
|
|
32
|
-
|
|
33
|
-
|
|
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
|
})
|
package/src/database/helpers.js
CHANGED
|
@@ -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)
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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(
|
|
31
|
-
|
|
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 = {
|
|
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
|
|
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
|
|
16
|
-
if (!fs.existsSync(
|
|
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:
|
|
20
|
-
storage:
|
|
21
|
-
logging:
|
|
22
|
-
pool:
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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 =
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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 {
|
|
4
|
+
const {
|
|
5
|
+
DB_NOT_INIT,
|
|
6
|
+
validateId,
|
|
7
|
+
validateData,
|
|
8
|
+
normalizeAttributes,
|
|
9
|
+
wrapError
|
|
10
|
+
} = require("./helpers");
|
|
9
11
|
|
|
10
|
-
const 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)
|
|
18
|
+
if (!Thread) {
|
|
19
|
+
return { thread: { threadID: validateId(threadID, ID_FIELD), ...(data || {}) }, created: true };
|
|
20
|
+
}
|
|
17
21
|
try {
|
|
18
|
-
threadID
|
|
19
|
-
let thread
|
|
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) {
|
|
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
|
|
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) {
|
|
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)
|
|
44
|
+
if (!Thread) {
|
|
45
|
+
return { thread: { threadID: validateId(threadID, ID_FIELD), ...(data || {}) }, created: false };
|
|
46
|
+
}
|
|
37
47
|
try {
|
|
38
|
-
threadID
|
|
48
|
+
threadID = validateId(threadID, ID_FIELD);
|
|
39
49
|
validateData(data);
|
|
40
50
|
const thread = await Thread.findOne({ where: { threadID } });
|
|
41
|
-
if (thread) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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) {
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
76
|
-
if (!Thread) return
|
|
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
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
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
|
};
|
package/src/database/userData.js
CHANGED
|
@@ -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 {
|
|
4
|
+
const {
|
|
5
|
+
DB_NOT_INIT,
|
|
6
|
+
validateId,
|
|
7
|
+
validateData,
|
|
8
|
+
normalizeAttributes,
|
|
9
|
+
normalizePayload,
|
|
10
|
+
wrapError
|
|
11
|
+
} = require("./helpers");
|
|
9
12
|
|
|
10
|
-
const User
|
|
13
|
+
const User = models.User;
|
|
11
14
|
const ID_FIELD = "userID";
|
|
12
15
|
|
|
13
|
-
function
|
|
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
|
|
23
|
+
if (!User) return stubUser(validateId(userID, ID_FIELD), data);
|
|
21
24
|
try {
|
|
22
|
-
userID
|
|
25
|
+
userID = validateId(userID, ID_FIELD);
|
|
23
26
|
validateData(data);
|
|
24
27
|
const payload = normalizePayload(data, "data");
|
|
25
|
-
let user
|
|
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) {
|
|
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
|
|
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) {
|
|
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
|
|
51
|
+
userID = validateId(userID, ID_FIELD);
|
|
45
52
|
validateData(data);
|
|
46
53
|
const payload = normalizePayload(data, "data");
|
|
47
|
-
const user
|
|
48
|
-
if (user) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
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) {
|
|
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 {
|
|
67
|
-
|
|
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(
|
|
87
|
+
async getAll(keys = null) {
|
|
71
88
|
if (!User) return [];
|
|
72
89
|
try {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
};
|