levix-bot 2.1.1 → 2.2.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 +3 -1
- package/app.cjs +13 -5
- package/package.json +1 -1
- package/public/dashboard.css +2167 -519
- package/public/dashboard.js +100 -36
- package/scheduler.cjs +84 -17
- package/src/bootstrap/core.js +17 -11
- package/src/bootstrap/session-startup-policy.js +23 -0
- package/src/commands/autoschedule.cjs +19 -42
- package/src/config/ai-identity.cjs +4 -3
- package/src/config/brand.cjs +3 -0
- package/src/db/db.cjs +8 -0
- package/src/db/store.cjs +25 -5
- package/src/routes/dashboard.api.esm.js +60 -3
- package/src/utils/recurrence.cjs +161 -0
- package/src/utils/storage.cjs +1 -0
- package/src/utils/storage.esm.js +1 -0
- package/views/dashboard.ejs +464 -94
- package/views/login.ejs +50 -86
- package/views/qr.ejs +51 -53
- package/views/setup.ejs +67 -110
package/src/db/store.cjs
CHANGED
|
@@ -623,7 +623,7 @@ function countDebts(settled = false) {
|
|
|
623
623
|
// them, and a crash in the middle of a write can't truncate the file.
|
|
624
624
|
//
|
|
625
625
|
// A job is `{ id, type, targetJid, message, cronString?, date?, status,
|
|
626
|
-
// creatorJid
|
|
626
|
+
// creatorJid, lastRunAt?, lastDeliveryStatus?, lastError? }`.
|
|
627
627
|
|
|
628
628
|
function scheduleRow(row) {
|
|
629
629
|
if (!row) return null;
|
|
@@ -637,6 +637,9 @@ function scheduleRow(row) {
|
|
|
637
637
|
status: row.status,
|
|
638
638
|
creatorJid: row.creator_jid ?? null,
|
|
639
639
|
createdAt: row.created_at,
|
|
640
|
+
lastRunAt: row.last_run_at ?? null,
|
|
641
|
+
lastDeliveryStatus: row.last_delivery_status ?? null,
|
|
642
|
+
lastError: row.last_error ?? null,
|
|
640
643
|
};
|
|
641
644
|
}
|
|
642
645
|
|
|
@@ -651,8 +654,9 @@ function getSchedule(id) {
|
|
|
651
654
|
function saveSchedule(job) {
|
|
652
655
|
q(
|
|
653
656
|
`INSERT INTO schedules
|
|
654
|
-
(id, type, target_jid, message, cron_string, date, status, creator_jid,
|
|
655
|
-
|
|
657
|
+
(id, type, target_jid, message, cron_string, date, status, creator_jid,
|
|
658
|
+
created_at, last_run_at, last_delivery_status, last_error)
|
|
659
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
656
660
|
ON CONFLICT(id) DO UPDATE SET
|
|
657
661
|
type = excluded.type,
|
|
658
662
|
target_jid = excluded.target_jid,
|
|
@@ -660,7 +664,10 @@ function saveSchedule(job) {
|
|
|
660
664
|
cron_string = excluded.cron_string,
|
|
661
665
|
date = excluded.date,
|
|
662
666
|
status = excluded.status,
|
|
663
|
-
creator_jid = excluded.creator_jid
|
|
667
|
+
creator_jid = excluded.creator_jid,
|
|
668
|
+
last_run_at = excluded.last_run_at,
|
|
669
|
+
last_delivery_status = excluded.last_delivery_status,
|
|
670
|
+
last_error = excluded.last_error`
|
|
664
671
|
).run(
|
|
665
672
|
String(job.id),
|
|
666
673
|
job.type,
|
|
@@ -670,7 +677,10 @@ function saveSchedule(job) {
|
|
|
670
677
|
job.date ?? null,
|
|
671
678
|
job.status || "pending",
|
|
672
679
|
job.creatorJid ?? null,
|
|
673
|
-
job.createdAt ?? Date.now()
|
|
680
|
+
job.createdAt ?? Date.now(),
|
|
681
|
+
job.lastRunAt ?? null,
|
|
682
|
+
job.lastDeliveryStatus ?? null,
|
|
683
|
+
job.lastError ?? null
|
|
674
684
|
);
|
|
675
685
|
return getSchedule(job.id);
|
|
676
686
|
}
|
|
@@ -679,6 +689,15 @@ function setScheduleStatus(id, status) {
|
|
|
679
689
|
q("UPDATE schedules SET status = ? WHERE id = ?").run(status, String(id));
|
|
680
690
|
}
|
|
681
691
|
|
|
692
|
+
function setScheduleDelivery(id, status, runAt, error = null) {
|
|
693
|
+
q(
|
|
694
|
+
`UPDATE schedules
|
|
695
|
+
SET last_delivery_status = ?, last_run_at = ?, last_error = ?
|
|
696
|
+
WHERE id = ?`
|
|
697
|
+
).run(status, runAt, error, String(id));
|
|
698
|
+
return getSchedule(id);
|
|
699
|
+
}
|
|
700
|
+
|
|
682
701
|
function deleteSchedule(id) {
|
|
683
702
|
const { changes } = q("DELETE FROM schedules WHERE id = ?").run(String(id));
|
|
684
703
|
return changes > 0;
|
|
@@ -821,6 +840,7 @@ module.exports = {
|
|
|
821
840
|
getSchedule,
|
|
822
841
|
saveSchedule,
|
|
823
842
|
setScheduleStatus,
|
|
843
|
+
setScheduleDelivery,
|
|
824
844
|
deleteSchedule,
|
|
825
845
|
countSchedules,
|
|
826
846
|
// AI history
|
|
@@ -49,7 +49,8 @@ const memory = require("../utils/memory.cjs");
|
|
|
49
49
|
const secrets = require("../config/secrets.cjs");
|
|
50
50
|
const { DATA_DIR } = require("../config/paths.cjs");
|
|
51
51
|
const { PERSONA_FILE } = require("../services/aiAgent.cjs");
|
|
52
|
-
const { deleteScheduledJob } = require("../../scheduler.cjs");
|
|
52
|
+
const { deleteScheduledJob, retryScheduledJob } = require("../../scheduler.cjs");
|
|
53
|
+
const { describeScheduledJob } = require("../utils/recurrence.cjs");
|
|
53
54
|
|
|
54
55
|
const router = Router();
|
|
55
56
|
|
|
@@ -732,9 +733,23 @@ router.post("/roles", (req, res) => {
|
|
|
732
733
|
// Scheduled messages
|
|
733
734
|
// ===========================================================================
|
|
734
735
|
|
|
736
|
+
function scheduleView(job) {
|
|
737
|
+
if (!job) return null;
|
|
738
|
+
const timezone = settings.get("bot_timezone");
|
|
739
|
+
return { ...job, when: describeScheduledJob(job, timezone) };
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function scheduleViews() {
|
|
743
|
+
return getSchedules().map(scheduleView);
|
|
744
|
+
}
|
|
745
|
+
|
|
735
746
|
router.get("/schedules", (req, res) => {
|
|
736
747
|
try {
|
|
737
|
-
res.json({
|
|
748
|
+
res.json({
|
|
749
|
+
success: true,
|
|
750
|
+
timezone: settings.get("bot_timezone"),
|
|
751
|
+
schedules: scheduleViews(),
|
|
752
|
+
});
|
|
738
753
|
} catch (error) {
|
|
739
754
|
fail(res, error, "Error listing schedules");
|
|
740
755
|
}
|
|
@@ -745,12 +760,54 @@ router.delete("/schedules/:id", (req, res) => {
|
|
|
745
760
|
// Through the scheduler, not the store: the running timer/cron task has to
|
|
746
761
|
// be stopped too, or a deleted job still fires until the next restart.
|
|
747
762
|
deleteScheduledJob(req.params.id);
|
|
748
|
-
res.json({ success: true, schedules:
|
|
763
|
+
res.json({ success: true, schedules: scheduleViews() });
|
|
749
764
|
} catch (error) {
|
|
750
765
|
fail(res, error, "Error deleting a schedule");
|
|
751
766
|
}
|
|
752
767
|
});
|
|
753
768
|
|
|
769
|
+
router.post(
|
|
770
|
+
"/schedules/:id/retry",
|
|
771
|
+
asyncRoute(async (req, res) => {
|
|
772
|
+
const id = String(req.params.id ?? "");
|
|
773
|
+
if (!id || id.length > 128) return badRequest(res, "Invalid schedule ID");
|
|
774
|
+
|
|
775
|
+
const sock = currentSocket();
|
|
776
|
+
if (!sessionState().connected || !sock) {
|
|
777
|
+
return res.status(409).json({
|
|
778
|
+
success: false,
|
|
779
|
+
error: "Connect WhatsApp before retrying a schedule",
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const result = await retryScheduledJob(sock, id);
|
|
784
|
+
if (result.reason === "not_found") {
|
|
785
|
+
return res.status(404).json({ success: false, error: "Schedule not found" });
|
|
786
|
+
}
|
|
787
|
+
if (result.reason === "not_failed") {
|
|
788
|
+
return res.status(409).json({
|
|
789
|
+
success: false,
|
|
790
|
+
error: "Only a failed delivery can be retried",
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
if (result.reason === "in_flight") {
|
|
794
|
+
return res.status(409).json({
|
|
795
|
+
success: false,
|
|
796
|
+
error: "This schedule is already being delivered",
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
if (!result.ok) {
|
|
800
|
+
return res.status(502).json({
|
|
801
|
+
success: false,
|
|
802
|
+
error: "WhatsApp delivery failed; the schedule remains retryable",
|
|
803
|
+
schedule: scheduleView(result.job),
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
res.json({ success: true, schedule: scheduleView(result.job) });
|
|
808
|
+
})
|
|
809
|
+
);
|
|
810
|
+
|
|
754
811
|
// ===========================================================================
|
|
755
812
|
// Security — the panel's own password
|
|
756
813
|
// ===========================================================================
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
const DAY_ALIASES = new Map([
|
|
2
|
+
["0", 0],
|
|
3
|
+
["7", 0],
|
|
4
|
+
["sun", 0],
|
|
5
|
+
["sunday", 0],
|
|
6
|
+
["الاحد", 0],
|
|
7
|
+
["احد", 0],
|
|
8
|
+
["1", 1],
|
|
9
|
+
["mon", 1],
|
|
10
|
+
["monday", 1],
|
|
11
|
+
["الاثنين", 1],
|
|
12
|
+
["اثنين", 1],
|
|
13
|
+
["الاتنين", 1],
|
|
14
|
+
["اتنين", 1],
|
|
15
|
+
["2", 2],
|
|
16
|
+
["tue", 2],
|
|
17
|
+
["tues", 2],
|
|
18
|
+
["tuesday", 2],
|
|
19
|
+
["الثلاثاء", 2],
|
|
20
|
+
["ثلاثاء", 2],
|
|
21
|
+
["التلات", 2],
|
|
22
|
+
["3", 3],
|
|
23
|
+
["wed", 3],
|
|
24
|
+
["wednesday", 3],
|
|
25
|
+
["الاربعاء", 3],
|
|
26
|
+
["اربعاء", 3],
|
|
27
|
+
["الاربع", 3],
|
|
28
|
+
["4", 4],
|
|
29
|
+
["thu", 4],
|
|
30
|
+
["thur", 4],
|
|
31
|
+
["thurs", 4],
|
|
32
|
+
["thursday", 4],
|
|
33
|
+
["الخميس", 4],
|
|
34
|
+
["خميس", 4],
|
|
35
|
+
["5", 5],
|
|
36
|
+
["fri", 5],
|
|
37
|
+
["friday", 5],
|
|
38
|
+
["الجمعه", 5],
|
|
39
|
+
["جمعه", 5],
|
|
40
|
+
["6", 6],
|
|
41
|
+
["sat", 6],
|
|
42
|
+
["saturday", 6],
|
|
43
|
+
["السبت", 6],
|
|
44
|
+
["سبت", 6],
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
const WEEKDAYS = [
|
|
48
|
+
"Sunday",
|
|
49
|
+
"Monday",
|
|
50
|
+
"Tuesday",
|
|
51
|
+
"Wednesday",
|
|
52
|
+
"Thursday",
|
|
53
|
+
"Friday",
|
|
54
|
+
"Saturday",
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
function normalizeDay(value) {
|
|
58
|
+
return String(value ?? "")
|
|
59
|
+
.trim()
|
|
60
|
+
.toLowerCase()
|
|
61
|
+
.normalize("NFKC")
|
|
62
|
+
.replace(/[\u0640\u064b-\u065f\u0670]/g, "")
|
|
63
|
+
.replace(/[أإآ]/g, "ا")
|
|
64
|
+
.replace(/ة/g, "ه");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseDayOfWeek(value) {
|
|
68
|
+
return DAY_ALIASES.get(normalizeDay(value)) ?? null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parseTime(value) {
|
|
72
|
+
const match = /^(\d{1,2}):(\d{2})$/.exec(String(value ?? "").trim());
|
|
73
|
+
if (!match) return null;
|
|
74
|
+
|
|
75
|
+
const hour = Number(match[1]);
|
|
76
|
+
const minute = Number(match[2]);
|
|
77
|
+
if (hour > 23 || minute > 59) return null;
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
hour,
|
|
81
|
+
minute,
|
|
82
|
+
text: `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseRecurringArgs(args) {
|
|
87
|
+
const type = String(args[0] ?? "").trim().toLowerCase();
|
|
88
|
+
|
|
89
|
+
if (type === "daily") {
|
|
90
|
+
const time = parseTime(args[1]);
|
|
91
|
+
const message = args.slice(2).join(" ").trim();
|
|
92
|
+
if (!time) return { error: "time" };
|
|
93
|
+
if (!message) return { error: "message" };
|
|
94
|
+
return {
|
|
95
|
+
type,
|
|
96
|
+
time: time.text,
|
|
97
|
+
message,
|
|
98
|
+
cronString: `${time.minute} ${time.hour} * * *`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (type === "weekly") {
|
|
103
|
+
const dayOfWeek = parseDayOfWeek(args[1]);
|
|
104
|
+
const time = parseTime(args[2]);
|
|
105
|
+
const message = args.slice(3).join(" ").trim();
|
|
106
|
+
if (dayOfWeek === null) return { error: "day" };
|
|
107
|
+
if (!time) return { error: "time" };
|
|
108
|
+
if (!message) return { error: "message" };
|
|
109
|
+
return {
|
|
110
|
+
type,
|
|
111
|
+
dayOfWeek,
|
|
112
|
+
time: time.text,
|
|
113
|
+
message,
|
|
114
|
+
cronString: `${time.minute} ${time.hour} * * ${dayOfWeek}`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { error: type ? "type" : "usage" };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function describeScheduledJob(job, timezone) {
|
|
122
|
+
if (job.type === "recurring") {
|
|
123
|
+
const parts = String(job.cronString ?? "").trim().split(/\s+/);
|
|
124
|
+
if (parts.length === 5) {
|
|
125
|
+
const [minuteText, hourText, dayOfMonth, month, dayText] = parts;
|
|
126
|
+
const minute = Number(minuteText);
|
|
127
|
+
const hour = Number(hourText);
|
|
128
|
+
const validTime =
|
|
129
|
+
/^\d+$/.test(minuteText) &&
|
|
130
|
+
/^\d+$/.test(hourText) &&
|
|
131
|
+
minute >= 0 &&
|
|
132
|
+
minute <= 59 &&
|
|
133
|
+
hour >= 0 &&
|
|
134
|
+
hour <= 23;
|
|
135
|
+
|
|
136
|
+
if (validTime && dayOfMonth === "*" && month === "*") {
|
|
137
|
+
const time = `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
|
|
138
|
+
if (dayText === "*") return `Daily at ${time} (${timezone})`;
|
|
139
|
+
|
|
140
|
+
const day = parseDayOfWeek(dayText);
|
|
141
|
+
if (day !== null) return `Every ${WEEKDAYS[day]} at ${time} (${timezone})`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return `${job.cronString || "Unknown recurrence"} (${timezone})`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const date = new Date(job.date);
|
|
149
|
+
if (!Number.isFinite(date.getTime())) return "Invalid date";
|
|
150
|
+
return `${new Intl.DateTimeFormat("en-GB", {
|
|
151
|
+
dateStyle: "medium",
|
|
152
|
+
timeStyle: "short",
|
|
153
|
+
timeZone: timezone,
|
|
154
|
+
}).format(date)} (${timezone})`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = {
|
|
158
|
+
parseDayOfWeek,
|
|
159
|
+
parseRecurringArgs,
|
|
160
|
+
describeScheduledJob,
|
|
161
|
+
};
|
package/src/utils/storage.cjs
CHANGED
|
@@ -75,6 +75,7 @@ module.exports = {
|
|
|
75
75
|
getSchedule: store.getSchedule,
|
|
76
76
|
saveSchedule: store.saveSchedule,
|
|
77
77
|
setScheduleStatus: store.setScheduleStatus,
|
|
78
|
+
setScheduleDelivery: store.setScheduleDelivery,
|
|
78
79
|
deleteSchedule: store.deleteSchedule,
|
|
79
80
|
countSchedules: store.countSchedules,
|
|
80
81
|
};
|
package/src/utils/storage.esm.js
CHANGED
|
@@ -89,5 +89,6 @@ export const getSchedules = store.getSchedules;
|
|
|
89
89
|
export const getSchedule = store.getSchedule;
|
|
90
90
|
export const saveSchedule = store.saveSchedule;
|
|
91
91
|
export const setScheduleStatus = store.setScheduleStatus;
|
|
92
|
+
export const setScheduleDelivery = store.setScheduleDelivery;
|
|
92
93
|
export const deleteSchedule = store.deleteSchedule;
|
|
93
94
|
export const countSchedules = store.countSchedules;
|