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/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 }` — the shape !schedule and !autoschedule have always built.
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, created_at)
655
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
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({ success: true, schedules: getSchedules() });
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: getSchedules() });
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
+ };
@@ -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
  };
@@ -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;