levix-bot 3.3.0-rc.2 → 4.0.0
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/README.md +13 -9
- package/app.cjs +49 -46
- package/package.json +9 -1
- package/public/dashboard/assets/index-BBBXWzM0.js +391 -0
- package/public/dashboard/assets/index-pYaivHON.css +1 -0
- package/public/dashboard/index.html +19 -0
- package/public/dashboard.css +391 -66
- package/public/dashboard.js +593 -42
- package/scheduler.cjs +9 -14
- package/src/auth/use-database-auth-state.js +2 -4
- package/src/bootstrap/core.js +2 -5
- package/src/bootstrap/panel.js +10 -5
- package/src/cli.js +3 -5
- package/src/commands/autoschedule.cjs +1 -4
- package/src/commands/blacklist.cjs +22 -20
- package/src/commands/block.cjs +2 -4
- package/src/commands/calc.cjs +9 -16
- package/src/commands/digit.cjs +97 -0
- package/src/commands/gemini.cjs +147 -161
- package/src/commands/group/add.cjs +7 -16
- package/src/commands/group/antiSpam.cjs +5 -11
- package/src/commands/group/antilink.cjs +34 -28
- package/src/commands/group/approveall.cjs +3 -11
- package/src/commands/group/clearwarns.cjs +2 -5
- package/src/commands/group/demote.cjs +3 -8
- package/src/commands/group/kick.cjs +12 -15
- package/src/commands/group/media.cjs +17 -24
- package/src/commands/group/members.cjs +1 -2
- package/src/commands/group/promote.cjs +4 -12
- package/src/commands/group/removeall.cjs +2 -5
- package/src/commands/group/setpp.cjs +1 -1
- package/src/commands/group/setrules.cjs +2 -8
- package/src/commands/group/setwarn.cjs +4 -2
- package/src/commands/group/tagadmins.cjs +2 -5
- package/src/commands/group/warn.cjs +26 -24
- package/src/commands/group/warns.cjs +1 -2
- package/src/commands/group/welcome.cjs +13 -17
- package/src/commands/group.cjs +7 -10
- package/src/commands/help.cjs +6 -12
- package/src/commands/lang.cjs +104 -0
- package/src/commands/listschedules.cjs +2 -6
- package/src/commands/loop.cjs +6 -13
- package/src/commands/memory.cjs +19 -32
- package/src/commands/mod.cjs +97 -35
- package/src/commands/perm.cjs +18 -56
- package/src/commands/ping.cjs +1 -5
- package/src/commands/poll.cjs +1 -1
- package/src/commands/rand.cjs +2 -5
- package/src/commands/schedule.cjs +4 -8
- package/src/commands/score.cjs +8 -2
- package/src/commands/setprefix.cjs +2 -2
- package/src/commands/shortlink.cjs +7 -15
- package/src/commands/shutdown.cjs +1 -1
- package/src/commands/sticker.cjs +222 -0
- package/src/commands/stt.cjs +136 -68
- package/src/commands/todo.cjs +9 -15
- package/src/commands/tts.cjs +10 -16
- package/src/commands/unblock.cjs +2 -6
- package/src/commands/weather.cjs +5 -12
- package/src/config/ai-persona.md +5 -3
- package/src/config/available-models.json +1156 -0
- package/src/config/baileys.config.js +2 -1
- package/src/config/brand.esm.js +1 -10
- package/src/config/constants.js +1 -1
- package/src/config/defaults.cjs +66 -66
- package/src/config/lock.cjs +2 -2
- package/src/config/modelCapabilities.cjs +172 -0
- package/src/config/runtime-config.cjs +8 -13
- package/src/config/secrets.cjs +18 -10
- package/src/config/settings.cjs +94 -10
- package/src/config/vault.cjs +107 -0
- package/src/core/connection.js +13 -12
- package/src/core/events.js +41 -23
- package/src/core/proxy.js +7 -7
- package/src/core/session.js +91 -48
- package/src/core/socket.js +12 -15
- package/src/db/db.cjs +14 -6
- package/src/db/store.cjs +221 -87
- package/src/domain/caddy.js +1 -1
- package/src/domain/command.js +40 -19
- package/src/domain/detect.js +1 -1
- package/src/domain/system.js +2 -5
- package/src/handlers/command.handler.js +22 -43
- package/src/handlers/group.handler.js +13 -13
- package/src/handlers/message.handler.js +11 -21
- package/src/index.js +8 -4
- package/src/middleware/antispam.middleware.js +46 -23
- package/src/middleware/blacklist.middleware.js +14 -9
- package/src/middleware/forward-tracking.middleware.js +3 -2
- package/src/middleware/permissions.middleware.js +7 -10
- package/src/panel/login-throttle.cjs +43 -0
- package/src/panel/session-auth.cjs +17 -0
- package/src/routes/dashboard.api.esm.js +394 -72
- package/src/services/aiAgent.cjs +136 -49
- package/src/services/aiProviders.cjs +193 -47
- package/src/services/aiRouter.cjs +137 -0
- package/src/services/aiTools.cjs +305 -128
- package/src/services/llmDiscovery.cjs +481 -0
- package/src/services/modelRegistry.cjs +412 -0
- package/src/utils/datetime.cjs +2 -2
- package/src/utils/lid-helper.esm.js +7 -7
- package/src/utils/logger.cjs +1 -1
- package/src/utils/markdownParser.cjs +159 -0
- package/src/utils/memory.cjs +43 -13
- package/src/utils/messageContent.cjs +40 -0
- package/src/utils/normalizeJid.esm.js +7 -7
- package/src/utils/numerals.cjs +75 -0
- package/src/utils/permissions.esm.js +10 -15
- package/src/utils/providerUrl.cjs +281 -0
- package/src/utils/recurrence.cjs +7 -11
- package/src/utils/sendBotMessage.esm.js +4 -14
- package/src/utils/statusMessage.cjs +5 -13
- package/src/utils/storage.cjs +10 -0
- package/src/utils/storage.esm.js +11 -0
- package/src/utils/textDecode.cjs +4 -9
- package/src/utils/thumbnail.cjs +8 -14
- package/src/utils/typing.esm.js +12 -12
- package/views/dashboard.ejs +134 -106
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Browsers } from "@whiskeysockets/baileys";
|
|
2
|
-
import pino from "pino";
|
|
3
2
|
import NodeCache from "node-cache";
|
|
3
|
+
import pino from "pino";
|
|
4
|
+
|
|
4
5
|
// rc10 supports several pluggable caches; we keep them in-process so message
|
|
5
6
|
// retries / device discovery don't keep slamming the network.
|
|
6
7
|
const msgRetryCounterCache = new NodeCache({
|
package/src/config/brand.esm.js
CHANGED
|
@@ -5,13 +5,4 @@ const require = createRequire(import.meta.url);
|
|
|
5
5
|
const brand = require("./brand.cjs");
|
|
6
6
|
|
|
7
7
|
export default brand;
|
|
8
|
-
export const {
|
|
9
|
-
name,
|
|
10
|
-
nameAr,
|
|
11
|
-
tagline,
|
|
12
|
-
taglineAr,
|
|
13
|
-
developer,
|
|
14
|
-
developerAr,
|
|
15
|
-
studio,
|
|
16
|
-
repo,
|
|
17
|
-
} = brand;
|
|
8
|
+
export const { name, nameAr, tagline, taglineAr, developer, developerAr, studio, repo } = brand;
|
package/src/config/constants.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
export const RETRY_DELAY_MS = 5000;
|
|
16
16
|
export const MAX_RETRIES = 5;
|
|
17
17
|
export const RETRY_SCHEDULE_MS = Object.freeze(
|
|
18
|
-
Array.from({ length: MAX_RETRIES }, (_, index) => RETRY_DELAY_MS * (index + 1))
|
|
18
|
+
Array.from({ length: MAX_RETRIES }, (_, index) => RETRY_DELAY_MS * (index + 1)),
|
|
19
19
|
);
|
|
20
20
|
|
|
21
21
|
// Group metadata cache
|
package/src/config/defaults.cjs
CHANGED
|
@@ -18,72 +18,72 @@
|
|
|
18
18
|
|
|
19
19
|
const PREFIX = "!";
|
|
20
20
|
|
|
21
|
-
const COMMAND_PERMISSIONS = Object.freeze(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
);
|
|
21
|
+
const COMMAND_PERMISSIONS = Object.freeze({
|
|
22
|
+
ping: "MEMBERS",
|
|
23
|
+
gemini: "MEMBERS",
|
|
24
|
+
qr: "MEMBERS",
|
|
25
|
+
shortlink: "MEMBERS",
|
|
26
|
+
schedule: "ADMINS_OWNER",
|
|
27
|
+
weather: "MEMBERS",
|
|
28
|
+
prayer: "MEMBERS",
|
|
29
|
+
tagadmins: "MEMBERS",
|
|
30
|
+
help: "MEMBERS",
|
|
31
|
+
score: "MEMBERS",
|
|
32
|
+
stt: "MEMBERS",
|
|
33
|
+
tts: "MEMBERS",
|
|
34
|
+
todo: "MEMBERS",
|
|
35
|
+
notes: "MEMBERS",
|
|
36
|
+
debt: "MEMBERS",
|
|
37
|
+
calc: "MEMBERS",
|
|
38
|
+
loop: "MEMBERS",
|
|
39
|
+
rand: "MEMBERS",
|
|
40
|
+
poll: "MEMBERS",
|
|
41
|
+
memory: "MEMBERS",
|
|
42
|
+
mod: "ADMINS_OWNER",
|
|
43
|
+
blacklist: "ADMINS_OWNER",
|
|
44
|
+
listschedules: "ADMINS_OWNER",
|
|
45
|
+
deleteschedule: "ADMINS_OWNER",
|
|
46
|
+
autoschedule: "OWNER_ONLY",
|
|
47
|
+
perm: "OWNER_ONLY",
|
|
48
|
+
setprefix: "OWNER_ONLY",
|
|
49
|
+
restart: "OWNER_ONLY",
|
|
50
|
+
shutdown: "OWNER_ONLY",
|
|
51
|
+
status: "OWNER_ONLY",
|
|
52
|
+
lang: "MEMBERS",
|
|
53
|
+
digit: "MEMBERS",
|
|
54
|
+
block: "OWNER_ONLY",
|
|
55
|
+
unblock: "OWNER_ONLY",
|
|
56
|
+
group: {
|
|
57
|
+
default_permission: "ADMINS_OWNER",
|
|
58
|
+
sub_commands: {
|
|
59
|
+
approveall: "ADMINS_OWNER",
|
|
60
|
+
setname: "ADMINS_OWNER",
|
|
61
|
+
setpp: "ADMINS_OWNER",
|
|
62
|
+
kick: "ADMINS_OWNER",
|
|
63
|
+
add: "ADMINS_OWNER",
|
|
64
|
+
promote: "ADMINS_OWNER",
|
|
65
|
+
demote: "ADMINS_OWNER",
|
|
66
|
+
removeall: "OWNER_ONLY",
|
|
67
|
+
setrules: "ADMINS_ONLY",
|
|
68
|
+
antilink: "ADMINS_ONLY",
|
|
69
|
+
media: "ADMINS_ONLY",
|
|
70
|
+
setwarn: "ADMINS_ONLY",
|
|
71
|
+
warn: "ADMINS_ONLY",
|
|
72
|
+
warns: "ADMINS_OWNER",
|
|
73
|
+
clearwarns: "ADMINS_ONLY",
|
|
74
|
+
rules: "MEMBERS",
|
|
75
|
+
notes: "MEMBERS",
|
|
76
|
+
members: "MEMBERS",
|
|
77
|
+
tagadmins: "MEMBERS",
|
|
78
|
+
welcome: "ADMINS_OWNER",
|
|
79
|
+
blacklist: "ADMINS_OWNER",
|
|
80
|
+
save: "ADMINS_OWNER",
|
|
81
|
+
deletenote: "ADMINS_OWNER",
|
|
82
|
+
remove: "ADMINS_ONLY",
|
|
83
|
+
all: "MEMBERS",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
87
|
|
|
88
88
|
module.exports = {
|
|
89
89
|
prefix: PREFIX,
|
package/src/config/lock.cjs
CHANGED
|
@@ -86,7 +86,7 @@ function acquireSingleInstanceLock() {
|
|
|
86
86
|
` ${LOCK_FILE}\n\n` +
|
|
87
87
|
` Only one Levix can run per data directory — a WhatsApp pairing is a\n` +
|
|
88
88
|
` single session. Stop the other one, or start this one with --data\n` +
|
|
89
|
-
` pointing somewhere else
|
|
89
|
+
` pointing somewhere else.`,
|
|
90
90
|
);
|
|
91
91
|
error.code = "ELEVIXLOCKED";
|
|
92
92
|
throw error;
|
|
@@ -94,7 +94,7 @@ function acquireSingleInstanceLock() {
|
|
|
94
94
|
|
|
95
95
|
// The holder is gone (a crash, a kill -9, a reboot). Take it over.
|
|
96
96
|
logger.warn(
|
|
97
|
-
`[lock] Removing a stale lock from pid ${owner ?? "unknown"} — that process is gone
|
|
97
|
+
`[lock] Removing a stale lock from pid ${owner ?? "unknown"} — that process is gone`,
|
|
98
98
|
);
|
|
99
99
|
fs.rmSync(LOCK_FILE, { force: true });
|
|
100
100
|
claim();
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Official capability metadata for known model IDs, dated 2026-09-18.
|
|
2
|
+
//
|
|
3
|
+
// This is fallback metadata only. It is never proof that a user's API key can
|
|
4
|
+
// access the model. Live availability always comes from the provider API.
|
|
5
|
+
//
|
|
6
|
+
// Unknown / newly discovered IDs must NOT be inferred from name substrings.
|
|
7
|
+
|
|
8
|
+
const FULL_MULTIMODAL = Object.freeze({
|
|
9
|
+
textInput: true,
|
|
10
|
+
textOutput: true,
|
|
11
|
+
vision: true,
|
|
12
|
+
audioInput: true,
|
|
13
|
+
stt: true,
|
|
14
|
+
videoInput: true,
|
|
15
|
+
pdfInput: true,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const VISION_TEXT = Object.freeze({
|
|
19
|
+
textInput: true,
|
|
20
|
+
textOutput: true,
|
|
21
|
+
vision: true,
|
|
22
|
+
audioInput: false,
|
|
23
|
+
stt: false,
|
|
24
|
+
videoInput: false,
|
|
25
|
+
pdfInput: true,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const TEXT_VISION_ONLY = Object.freeze({
|
|
29
|
+
textInput: true,
|
|
30
|
+
textOutput: true,
|
|
31
|
+
vision: true,
|
|
32
|
+
audioInput: false,
|
|
33
|
+
stt: false,
|
|
34
|
+
videoInput: false,
|
|
35
|
+
pdfInput: false,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Exact IDs from the 2026-09-18 seed catalog + official docs. No prefix matching.
|
|
39
|
+
const GEMINI_OFFICIAL = Object.freeze({
|
|
40
|
+
"gemini-2.5-flash": FULL_MULTIMODAL,
|
|
41
|
+
"gemini-2.5-pro": FULL_MULTIMODAL,
|
|
42
|
+
"gemini-2.5-flash-lite": FULL_MULTIMODAL,
|
|
43
|
+
"gemini-flash-latest": FULL_MULTIMODAL,
|
|
44
|
+
"gemini-flash-lite-latest": FULL_MULTIMODAL,
|
|
45
|
+
"gemini-pro-latest": FULL_MULTIMODAL,
|
|
46
|
+
"gemini-3-flash-preview": FULL_MULTIMODAL,
|
|
47
|
+
"gemini-3.1-pro-preview": FULL_MULTIMODAL,
|
|
48
|
+
"gemini-3.1-pro-preview-customtools": FULL_MULTIMODAL,
|
|
49
|
+
"gemini-3.1-flash-lite-preview": FULL_MULTIMODAL,
|
|
50
|
+
"gemini-3.1-flash-lite": FULL_MULTIMODAL,
|
|
51
|
+
"gemini-3.5-flash": FULL_MULTIMODAL,
|
|
52
|
+
"gemini-3.5-flash-lite": FULL_MULTIMODAL,
|
|
53
|
+
"gemini-3.6-flash": FULL_MULTIMODAL,
|
|
54
|
+
"gemini-3.7-flash": FULL_MULTIMODAL,
|
|
55
|
+
"gemini-3.8-flash": FULL_MULTIMODAL,
|
|
56
|
+
"gemini-omni-flash-preview": FULL_MULTIMODAL,
|
|
57
|
+
"gemini-omni-1.1-flash": FULL_MULTIMODAL,
|
|
58
|
+
"gemma-4-26b-a4b-it": TEXT_VISION_ONLY,
|
|
59
|
+
"gemma-4-31b-it": TEXT_VISION_ONLY,
|
|
60
|
+
"gemini-1.5-flash": FULL_MULTIMODAL,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const GEMINI_RECOMMENDED = new Set([
|
|
64
|
+
"gemini-3.8-flash",
|
|
65
|
+
"gemini-3.7-flash",
|
|
66
|
+
"gemini-3.5-flash",
|
|
67
|
+
"gemini-3.1-pro-preview",
|
|
68
|
+
"gemini-3.5-flash-lite",
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
function officialCapabilities(provider, modelId) {
|
|
72
|
+
const id = String(modelId || "")
|
|
73
|
+
.trim()
|
|
74
|
+
.replace(/^models\//, "");
|
|
75
|
+
if (!id) return null;
|
|
76
|
+
if (provider === "gemini") return GEMINI_OFFICIAL[id] || null;
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isGeminiRecommended(modelId) {
|
|
81
|
+
const id = String(modelId || "")
|
|
82
|
+
.trim()
|
|
83
|
+
.replace(/^models\//, "");
|
|
84
|
+
return GEMINI_RECOMMENDED.has(id);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Conversational-LLM filter for the main model selector.
|
|
89
|
+
* Non-chat families (embeddings, image-gen, TTS, video, live/realtime) are out.
|
|
90
|
+
*/
|
|
91
|
+
function isConversationalLlm(provider, modelId, meta = {}) {
|
|
92
|
+
const id = String(modelId || "")
|
|
93
|
+
.toLowerCase()
|
|
94
|
+
.replace(/^models\//, "");
|
|
95
|
+
if (!id) return false;
|
|
96
|
+
|
|
97
|
+
const methods = Array.isArray(meta.methods)
|
|
98
|
+
? meta.methods.map((m) => String(m))
|
|
99
|
+
: Array.isArray(meta.supportedGenerationMethods)
|
|
100
|
+
? meta.supportedGenerationMethods.map((m) => String(m))
|
|
101
|
+
: Array.isArray(meta.supportedActions)
|
|
102
|
+
? meta.supportedActions.map((m) => String(m))
|
|
103
|
+
: [];
|
|
104
|
+
|
|
105
|
+
if (provider === "openai") return isOpenAiChatModel(id);
|
|
106
|
+
if (provider === "anthropic") {
|
|
107
|
+
if (meta.type && String(meta.type).toLowerCase() !== "model") return false;
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (provider === "gemini") return isGeminiChatModel(id, methods);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isOpenAiChatModel(id) {
|
|
115
|
+
if (id.includes("embedding") || id.includes("embed-")) return false;
|
|
116
|
+
if (id.includes("moderation")) return false;
|
|
117
|
+
if (id.startsWith("dall-e") || id.includes("dall-e")) return false;
|
|
118
|
+
if (id.startsWith("gpt-image") || id.startsWith("chatgpt-image")) return false;
|
|
119
|
+
if (id.startsWith("tts-") || id.includes("-tts")) return false;
|
|
120
|
+
if (id.startsWith("whisper-")) return false;
|
|
121
|
+
if (id.startsWith("sora")) return false;
|
|
122
|
+
if (id.includes("davinci") || id.includes("babbage") || id.includes("curie")) return false;
|
|
123
|
+
if (id.startsWith("ada-") || id.includes("text-similarity")) return false;
|
|
124
|
+
if (id.includes("canary")) return false;
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isGeminiChatModel(id, methods) {
|
|
129
|
+
if (id.includes("embedding") || id.includes("vector")) return false;
|
|
130
|
+
if (id === "aqa" || id.startsWith("aqa-")) return false;
|
|
131
|
+
if (id.startsWith("imagen") || id.includes("-image") || id.includes("nano-banana")) return false;
|
|
132
|
+
if (id.includes("-tts") || id.endsWith("tts")) return false;
|
|
133
|
+
if (id.startsWith("veo") || id.includes("lyria")) return false;
|
|
134
|
+
if (id.includes("transcribe")) return false;
|
|
135
|
+
if (id.includes("native-audio") || id.includes("realtime")) return false;
|
|
136
|
+
if (id.includes("-live") || /(^|-)live($|-)/.test(id)) return false;
|
|
137
|
+
if (
|
|
138
|
+
id.includes("robotics") ||
|
|
139
|
+
id.includes("computer-use") ||
|
|
140
|
+
id.includes("antigravity") ||
|
|
141
|
+
id.includes("deep-research")
|
|
142
|
+
) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (methods.includes("embedContent") && !methods.includes("generateContent")) return false;
|
|
147
|
+
if (methods.includes("bidiGenerateContent") && !methods.includes("generateContent")) return false;
|
|
148
|
+
if (
|
|
149
|
+
methods.includes("bidiGenerateMusic") ||
|
|
150
|
+
methods.includes("predictLongRunning") ||
|
|
151
|
+
methods.includes("generateAnswer")
|
|
152
|
+
) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
if (
|
|
156
|
+
methods.length &&
|
|
157
|
+
!methods.includes("generateContent") &&
|
|
158
|
+
!methods.includes("generateContentStream")
|
|
159
|
+
) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = {
|
|
166
|
+
FULL_MULTIMODAL,
|
|
167
|
+
VISION_TEXT,
|
|
168
|
+
TEXT_VISION_ONLY,
|
|
169
|
+
officialCapabilities,
|
|
170
|
+
isGeminiRecommended,
|
|
171
|
+
isConversationalLlm,
|
|
172
|
+
};
|
|
@@ -17,12 +17,7 @@
|
|
|
17
17
|
const defaults = require("./defaults.cjs");
|
|
18
18
|
const logger = require("../utils/logger.cjs");
|
|
19
19
|
|
|
20
|
-
const PERMISSION_LEVELS = Object.freeze([
|
|
21
|
-
"MEMBERS",
|
|
22
|
-
"ADMINS_ONLY",
|
|
23
|
-
"ADMINS_OWNER",
|
|
24
|
-
"OWNER_ONLY",
|
|
25
|
-
]);
|
|
20
|
+
const PERMISSION_LEVELS = Object.freeze(["MEMBERS", "ADMINS_ONLY", "ADMINS_OWNER", "OWNER_ONLY"]);
|
|
26
21
|
|
|
27
22
|
const KEY_PERMISSIONS = "command_permissions";
|
|
28
23
|
const KEY_ALIASES = "command_aliases";
|
|
@@ -84,11 +79,7 @@ function defaultPermission(commandKey) {
|
|
|
84
79
|
if (sub) {
|
|
85
80
|
const groupEntry = table[head];
|
|
86
81
|
if (isPlainObject(groupEntry)) {
|
|
87
|
-
return
|
|
88
|
-
groupEntry.sub_commands?.[sub] ||
|
|
89
|
-
groupEntry.default_permission ||
|
|
90
|
-
"MEMBERS"
|
|
91
|
-
);
|
|
82
|
+
return groupEntry.sub_commands?.[sub] || groupEntry.default_permission || "MEMBERS";
|
|
92
83
|
}
|
|
93
84
|
return "MEMBERS";
|
|
94
85
|
}
|
|
@@ -166,7 +157,9 @@ function setAliases(commandName, aliases) {
|
|
|
166
157
|
if (!Array.isArray(aliases)) throw new Error("Aliases must be a list");
|
|
167
158
|
const cleaned = [];
|
|
168
159
|
for (const raw of aliases) {
|
|
169
|
-
const alias = String(raw ?? "")
|
|
160
|
+
const alias = String(raw ?? "")
|
|
161
|
+
.trim()
|
|
162
|
+
.toLowerCase();
|
|
170
163
|
if (!alias) continue;
|
|
171
164
|
if (/\s/.test(alias)) throw new Error(`Alias "${alias}" can't contain spaces`);
|
|
172
165
|
if (alias.length > 20) throw new Error(`Alias "${alias}" is too long`);
|
|
@@ -177,7 +170,9 @@ function setAliases(commandName, aliases) {
|
|
|
177
170
|
}
|
|
178
171
|
|
|
179
172
|
writeSetting(KEY_ALIASES, overrides);
|
|
180
|
-
logger.info(
|
|
173
|
+
logger.info(
|
|
174
|
+
`[config] aliases ${commandName} -> ${JSON.stringify(overrides[commandName] ?? "(default)")}`,
|
|
175
|
+
);
|
|
181
176
|
return getAliases(commandName);
|
|
182
177
|
}
|
|
183
178
|
|
package/src/config/secrets.cjs
CHANGED
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
// secret:session the express-session signing key. 48 random bytes, made on
|
|
10
10
|
// first start and reused forever after. Nobody ever sees it.
|
|
11
11
|
// auth:password the dashboard password as an scrypt hash + salt. The
|
|
12
|
-
// password itself is never written anywhere
|
|
12
|
+
// password itself is never written anywhere (not encrypted
|
|
13
|
+
// for recovery — it is a one-way hash).
|
|
14
|
+
//
|
|
15
|
+
// WhatsApp session blobs and third-party API keys are sealed separately by
|
|
16
|
+
// src/config/vault.cjs (AES-256-GCM, key in levix.key next to the database).
|
|
13
17
|
//
|
|
14
18
|
// Both live in `bot_settings`, i.e. inside the same SQLite file as the rest of
|
|
15
19
|
// the bot's state — so a backup of that one file is a backup of the login too.
|
|
@@ -57,6 +61,13 @@ function hasDashboardPassword() {
|
|
|
57
61
|
return !!(record && record.hash && record.salt);
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
/** Epoch stored on panel sessions so a password change or wipe revokes them. */
|
|
65
|
+
function getPasswordUpdatedAt() {
|
|
66
|
+
const record = store.getBotSetting(PASSWORD_KEY, null);
|
|
67
|
+
if (!record?.hash || !record?.salt) return 0;
|
|
68
|
+
return Number(record.updatedAt) || 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
60
71
|
/**
|
|
61
72
|
* @throws {Error} when the password is too short — the dashboard holds a full
|
|
62
73
|
* WhatsApp account, so this is not a place for a 3-character password.
|
|
@@ -64,13 +75,13 @@ function hasDashboardPassword() {
|
|
|
64
75
|
function setDashboardPassword(plain) {
|
|
65
76
|
const password = String(plain ?? "");
|
|
66
77
|
if (password.length < MIN_PASSWORD_LENGTH) {
|
|
67
|
-
throw new Error(
|
|
68
|
-
`Password must be at least ${MIN_PASSWORD_LENGTH} characters`
|
|
69
|
-
);
|
|
78
|
+
throw new Error(`Password must be at least ${MIN_PASSWORD_LENGTH} characters`);
|
|
70
79
|
}
|
|
71
80
|
|
|
81
|
+
const previousEpoch = getPasswordUpdatedAt();
|
|
72
82
|
const { salt, hash } = hashPassword(password);
|
|
73
|
-
|
|
83
|
+
const updatedAt = Math.max(Date.now(), previousEpoch + 1);
|
|
84
|
+
store.saveBotSetting(PASSWORD_KEY, { alg: "scrypt", salt, hash, updatedAt });
|
|
74
85
|
logger.info("[secrets] Dashboard password updated");
|
|
75
86
|
}
|
|
76
87
|
|
|
@@ -81,11 +92,7 @@ function verifyDashboardPassword(candidate) {
|
|
|
81
92
|
if (typeof candidate !== "string") return false;
|
|
82
93
|
|
|
83
94
|
const expected = Buffer.from(record.hash, "hex");
|
|
84
|
-
const actual = crypto.scryptSync(
|
|
85
|
-
candidate,
|
|
86
|
-
Buffer.from(record.salt, "hex"),
|
|
87
|
-
expected.length
|
|
88
|
-
);
|
|
95
|
+
const actual = crypto.scryptSync(candidate, Buffer.from(record.salt, "hex"), expected.length);
|
|
89
96
|
return crypto.timingSafeEqual(expected, actual);
|
|
90
97
|
}
|
|
91
98
|
|
|
@@ -140,6 +147,7 @@ module.exports = {
|
|
|
140
147
|
MIN_PASSWORD_LENGTH,
|
|
141
148
|
getSessionSecret,
|
|
142
149
|
hasDashboardPassword,
|
|
150
|
+
getPasswordUpdatedAt,
|
|
143
151
|
setDashboardPassword,
|
|
144
152
|
verifyDashboardPassword,
|
|
145
153
|
getSetupCode,
|
package/src/config/settings.cjs
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
// dashboard only ever learns whether one is configured.
|
|
17
17
|
|
|
18
18
|
const logger = require("../utils/logger.cjs");
|
|
19
|
+
const { assertProviderBaseUrl } = require("../utils/providerUrl.cjs");
|
|
19
20
|
|
|
20
21
|
const SETTINGS = [
|
|
21
22
|
// --- General -----------------------------------------------------------
|
|
@@ -27,6 +28,20 @@ const SETTINGS = [
|
|
|
27
28
|
label: "Timezone",
|
|
28
29
|
hint: "IANA name, e.g. Africa/Cairo. Used by the AI, !prayer and the scheduler.",
|
|
29
30
|
},
|
|
31
|
+
{
|
|
32
|
+
key: "bot_language",
|
|
33
|
+
type: "string",
|
|
34
|
+
default: "auto",
|
|
35
|
+
choices: ["auto", "ar", "en"],
|
|
36
|
+
choiceLabels: {
|
|
37
|
+
auto: "Auto-detect (follow user message)",
|
|
38
|
+
ar: "Modern Standard Arabic (العربية الفصحى)",
|
|
39
|
+
en: "English",
|
|
40
|
+
},
|
|
41
|
+
group: "general",
|
|
42
|
+
label: "Bot response language",
|
|
43
|
+
hint: "Preferred language for AI replies and status updates. Can also be switched with !lang.",
|
|
44
|
+
},
|
|
30
45
|
{
|
|
31
46
|
key: "bot_min_delay_ms",
|
|
32
47
|
type: "int",
|
|
@@ -160,6 +175,52 @@ const SETTINGS = [
|
|
|
160
175
|
label: "Gemini Google Search",
|
|
161
176
|
hint: "Lets Gemini use Google's own search grounding when a question needs current information. Gemini only — the openai and anthropic providers never see it.",
|
|
162
177
|
},
|
|
178
|
+
{
|
|
179
|
+
key: "ai_vision_enabled",
|
|
180
|
+
type: "bool",
|
|
181
|
+
default: false,
|
|
182
|
+
group: "ai",
|
|
183
|
+
label: "Vision & image analysis",
|
|
184
|
+
hint: "Allow the active model to inspect and analyze incoming images. Enabled natively on Gemini, and available on OpenAI-compatible/Anthropic when using vision-capable models (e.g. llama-3.2-11b-vision, gpt-4o, claude-3).",
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
key: "ai_stt_enabled",
|
|
188
|
+
type: "bool",
|
|
189
|
+
default: true,
|
|
190
|
+
group: "ai",
|
|
191
|
+
label: "Audio & Speech-to-text (STT)",
|
|
192
|
+
hint: "Enable audio processing and speech-to-text transcription with compatible models.",
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
key: "ai_auto_detect_capabilities",
|
|
196
|
+
type: "bool",
|
|
197
|
+
default: true,
|
|
198
|
+
group: "ai",
|
|
199
|
+
label: "Auto-detect model capabilities",
|
|
200
|
+
hint: "When the selected model does not support Vision or Speech-to-text, those toggles are turned off and locked automatically. Capabilities come from the provider API and the local registry — never from guessing at the model name.",
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
key: "ai_stt_provider",
|
|
204
|
+
type: "string",
|
|
205
|
+
default: "auto",
|
|
206
|
+
choices: ["auto", "gemini", "openai"],
|
|
207
|
+
choiceLabels: {
|
|
208
|
+
auto: "Auto (follow active provider)",
|
|
209
|
+
gemini: "Google Gemini",
|
|
210
|
+
openai: "OpenAI-compatible (Whisper)",
|
|
211
|
+
},
|
|
212
|
+
group: "ai",
|
|
213
|
+
label: "Speech-to-text provider",
|
|
214
|
+
hint: "Which engine powers !stt audio transcription. 'Auto' follows your active chat provider.",
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
key: "openai_stt_model",
|
|
218
|
+
type: "string",
|
|
219
|
+
default: "whisper-large-v3",
|
|
220
|
+
group: "ai",
|
|
221
|
+
label: "OpenAI-compatible Whisper model",
|
|
222
|
+
hint: "Model used for speech transcription when OpenAI-compatible is selected for STT (whisper-large-v3, whisper-1).",
|
|
223
|
+
},
|
|
163
224
|
{
|
|
164
225
|
key: "openai_api_key",
|
|
165
226
|
type: "secret",
|
|
@@ -232,6 +293,14 @@ const SETTINGS = [
|
|
|
232
293
|
label: "OpenWeatherMap API key",
|
|
233
294
|
hint: "Needed by !weather.",
|
|
234
295
|
},
|
|
296
|
+
{
|
|
297
|
+
key: "youtube_api_key",
|
|
298
|
+
type: "secret",
|
|
299
|
+
default: "",
|
|
300
|
+
group: "integrations",
|
|
301
|
+
label: "YouTube Data API key",
|
|
302
|
+
hint: "For YouTube video and search tools.",
|
|
303
|
+
},
|
|
235
304
|
|
|
236
305
|
// --- Media -------------------------------------------------------------
|
|
237
306
|
{
|
|
@@ -387,6 +456,9 @@ const SETTINGS = [
|
|
|
387
456
|
];
|
|
388
457
|
|
|
389
458
|
const BY_KEY = new Map(SETTINGS.map((definition) => [definition.key, definition]));
|
|
459
|
+
const SECRET_SETTING_KEYS = SETTINGS.filter((definition) => definition.type === "secret").map(
|
|
460
|
+
(definition) => definition.key,
|
|
461
|
+
);
|
|
390
462
|
|
|
391
463
|
const GROUP_LABELS = {
|
|
392
464
|
general: "General",
|
|
@@ -402,11 +474,13 @@ const GROUP_LABELS = {
|
|
|
402
474
|
// comes too early falls back to the default instead of throwing.
|
|
403
475
|
function storedValue(key) {
|
|
404
476
|
try {
|
|
405
|
-
const value = require("../utils/storage.cjs").getBotSetting(
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
)
|
|
409
|
-
|
|
477
|
+
const value = require("../utils/storage.cjs").getBotSetting(`setting:${key}`, undefined);
|
|
478
|
+
if (value === undefined || value === null) return undefined;
|
|
479
|
+
const definition = BY_KEY.get(key);
|
|
480
|
+
if (definition?.type === "secret" && typeof value === "string") {
|
|
481
|
+
return require("./vault.cjs").open(value);
|
|
482
|
+
}
|
|
483
|
+
return value;
|
|
410
484
|
} catch {
|
|
411
485
|
return undefined;
|
|
412
486
|
}
|
|
@@ -480,6 +554,9 @@ function validate(definition, value) {
|
|
|
480
554
|
throw new Error(`"${text}" is not a valid timezone`);
|
|
481
555
|
}
|
|
482
556
|
}
|
|
557
|
+
if (["gemini_base_url", "openai_base_url", "anthropic_base_url"].includes(definition.key)) {
|
|
558
|
+
return assertProviderBaseUrl(text, { allowLoopback: true });
|
|
559
|
+
}
|
|
483
560
|
return text;
|
|
484
561
|
}
|
|
485
562
|
|
|
@@ -501,11 +578,11 @@ function set(key, value) {
|
|
|
501
578
|
}
|
|
502
579
|
|
|
503
580
|
const clean = validate(definition, value);
|
|
504
|
-
|
|
581
|
+
const toStore =
|
|
582
|
+
definition.type === "secret" ? require("./vault.cjs").seal(String(clean)) : clean;
|
|
583
|
+
storage.saveBotSetting(`setting:${key}`, toStore);
|
|
505
584
|
// Never log the value of a secret.
|
|
506
|
-
logger.info(
|
|
507
|
-
`[settings] ${key} -> ${definition.type === "secret" ? "(updated)" : clean}`
|
|
508
|
-
);
|
|
585
|
+
logger.info(`[settings] ${key} -> ${definition.type === "secret" ? "(updated)" : clean}`);
|
|
509
586
|
return get(key);
|
|
510
587
|
}
|
|
511
588
|
|
|
@@ -537,4 +614,11 @@ function describe() {
|
|
|
537
614
|
});
|
|
538
615
|
}
|
|
539
616
|
|
|
540
|
-
module.exports = {
|
|
617
|
+
module.exports = {
|
|
618
|
+
get,
|
|
619
|
+
set,
|
|
620
|
+
describe,
|
|
621
|
+
sourceOf,
|
|
622
|
+
SETTING_KEYS: [...BY_KEY.keys()],
|
|
623
|
+
SECRET_SETTING_KEYS: [...SECRET_SETTING_KEYS],
|
|
624
|
+
};
|