levix-bot 2.2.0 → 2.2.1

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 CHANGED
@@ -18,9 +18,11 @@ go to **Connection**, press **Start session** and scan the QR — see
18
18
  [SETUP.md](SETUP.md) for the longer version, Docker, and running it as a
19
19
  service.
20
20
 
21
- Starting Levix does not connect it to WhatsApp. The panel, the database and the
22
- commands come up on their own, and the WhatsApp session is started from the
23
- Connection screen when you want it.
21
+ On a brand-new install, starting Levix does not begin WhatsApp pairing by
22
+ itself: the panel, database and commands come up first, and the Connection
23
+ screen starts the first session when you ask. After that first successful link,
24
+ the saved WhatsApp session resumes automatically after a process, Docker or
25
+ systemd restart unless you explicitly stop or unlink it.
24
26
 
25
27
  ```
26
28
  levix start Levix with the web panel
package/SETUP.md CHANGED
@@ -58,9 +58,14 @@ The terminal prints something like:
58
58
  bot before you do.)
59
59
  3. You land on the control panel. Go to **Connection**, press **Start session**,
60
60
  and scan the QR with WhatsApp → Settings → Linked devices → Link a device.
61
- Levix does not connect to WhatsApp until you ask it to.
61
+ A brand-new install does not begin pairing until you press Start.
62
62
  4. Send `!ping` in any chat. The bot answers.
63
63
 
64
+ After that first successful pairing, the saved WhatsApp session resumes
65
+ automatically after a Levix, Docker or systemd restart. You only need the
66
+ Connection screen again if you deliberately stop/unlink the session or WhatsApp
67
+ requires a new link.
68
+
64
69
  Levix only opens a browser when there is one to open: over SSH, under systemd,
65
70
  in Docker or in CI it just prints the address and says why. `--no-open` turns
66
71
  it off anywhere.
@@ -85,7 +90,8 @@ Then open <http://localhost:3001>. The setup code is in the logs:
85
90
  docker compose logs levix | grep -F '[Setup] Setup code:' | tail -1
86
91
  ```
87
92
 
88
- Everything the bot owns lives in the `levix-data` volume.
93
+ Everything the bot owns lives in the `levix-data` volume. Once WhatsApp has
94
+ been paired, restarting the container reuses that saved session automatically.
89
95
 
90
96
  ---
91
97
 
@@ -181,11 +187,11 @@ journalctl -u levix --no-pager | grep -F '[Setup] Setup code:' | tail -1
181
187
 
182
188
  Read the script first if you like — it is short, and it explains each step.
183
189
 
184
- That always installs the newest stable Levix. To stay on one version — the same
185
- script, pinned — use its own URL:
190
+ That always installs the newest stable Levix. To stay on this release — the
191
+ same script, pinned — use its own URL:
186
192
 
187
193
  ```bash
188
- curl -fsSL https://levix.leviro.net/install/v2.0.0.sh | bash
194
+ curl -fsSL https://levix.leviro.net/install/v2.2.1.sh | bash
189
195
  ```
190
196
 
191
197
  **Behind a proxy** — if this server reaches the internet through a proxy, the
package/app.cjs CHANGED
@@ -19,6 +19,7 @@ const { assetPath } = require("./src/config/paths.cjs");
19
19
  const brand = require("./src/config/brand.cjs");
20
20
  const settings = require("./src/config/settings.cjs");
21
21
  const secrets = require("./src/config/secrets.cjs");
22
+ const { PanelSessionStore } = require("./src/panel/session-store.cjs");
22
23
  const { getQrCode } = require("./src/utils/storage.cjs");
23
24
  const {
24
25
  clientAddress,
@@ -151,9 +152,14 @@ app.locals.brand = brand;
151
152
  app.use(express.static(assetPath("public")));
152
153
  app.use(express.urlencoded({ extended: true, limit: "100kb" }));
153
154
 
155
+ // Levix is intentionally one process and one operator, so a small expiring
156
+ // in-memory store is enough. Unlike express-session's development MemoryStore,
157
+ // it is explicitly bounded and prunes expired entries instead of growing for
158
+ // the lifetime of the process. Sessions still disappear on restart by design.
154
159
  const sessionMiddleware = session({
155
160
  name: SESSION_COOKIE_NAME,
156
161
  secret: secrets.getSessionSecret(),
162
+ store: new PanelSessionStore({ ttlMs: SESSION_MAX_AGE_MS }),
157
163
  resave: false,
158
164
  saveUninitialized: false,
159
165
  cookie: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "levix-bot",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "A self-hosted personal WhatsApp bot with an AI agent, group moderation and a web control panel.",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -57,7 +57,7 @@
57
57
  "type": "git",
58
58
  "url": "https://github.com/Abdodiab2005/levix.git"
59
59
  },
60
- "homepage": "https://github.com/Abdodiab2005/levix",
60
+ "homepage": "https://levix.leviro.net",
61
61
  "bugs": {
62
62
  "url": "https://github.com/Abdodiab2005/levix/issues"
63
63
  },
@@ -0,0 +1,100 @@
1
+ const session = require("express-session");
2
+
3
+ function clone(value) {
4
+ return value == null ? value : JSON.parse(JSON.stringify(value));
5
+ }
6
+
7
+ class PanelSessionStore extends session.Store {
8
+ constructor({ ttlMs = 12 * 60 * 60 * 1000, maxSessions = 128 } = {}) {
9
+ super();
10
+ this.ttlMs = ttlMs;
11
+ this.maxSessions = maxSessions;
12
+ this.sessions = new Map();
13
+ }
14
+
15
+ _expiresAt(sess) {
16
+ const expires = Date.parse(sess?.cookie?.expires || "");
17
+ if (Number.isFinite(expires)) return expires;
18
+
19
+ const maxAge = Number(sess?.cookie?.maxAge);
20
+ if (Number.isFinite(maxAge) && maxAge > 0) return Date.now() + maxAge;
21
+
22
+ return Date.now() + this.ttlMs;
23
+ }
24
+
25
+ _prune() {
26
+ const now = Date.now();
27
+ for (const [sid, entry] of this.sessions) {
28
+ if (entry.expiresAt <= now) this.sessions.delete(sid);
29
+ }
30
+ }
31
+
32
+ _makeRoom(sid) {
33
+ if (this.sessions.has(sid) || this.sessions.size < this.maxSessions) return;
34
+
35
+ let oldestSid = null;
36
+ let oldestExpiry = Infinity;
37
+ for (const [candidateSid, entry] of this.sessions) {
38
+ if (entry.expiresAt < oldestExpiry) {
39
+ oldestSid = candidateSid;
40
+ oldestExpiry = entry.expiresAt;
41
+ }
42
+ }
43
+ if (oldestSid) this.sessions.delete(oldestSid);
44
+ }
45
+
46
+ get(sid, callback) {
47
+ try {
48
+ this._prune();
49
+ const entry = this.sessions.get(sid);
50
+ callback(null, entry ? clone(entry.session) : null);
51
+ } catch (error) {
52
+ callback(error);
53
+ }
54
+ }
55
+
56
+ set(sid, sess, callback = () => {}) {
57
+ try {
58
+ this._prune();
59
+ this._makeRoom(sid);
60
+ this.sessions.set(sid, {
61
+ session: clone(sess),
62
+ expiresAt: this._expiresAt(sess),
63
+ });
64
+ callback(null);
65
+ } catch (error) {
66
+ callback(error);
67
+ }
68
+ }
69
+
70
+ touch(sid, sess, callback = () => {}) {
71
+ try {
72
+ this._prune();
73
+ const entry = this.sessions.get(sid);
74
+ if (entry) {
75
+ entry.expiresAt = this._expiresAt(sess);
76
+ if (sess?.cookie) entry.session.cookie = clone(sess.cookie);
77
+ }
78
+ callback(null);
79
+ } catch (error) {
80
+ callback(error);
81
+ }
82
+ }
83
+
84
+ destroy(sid, callback = () => {}) {
85
+ this.sessions.delete(sid);
86
+ callback(null);
87
+ }
88
+
89
+ clear(callback = () => {}) {
90
+ this.sessions.clear();
91
+ callback(null);
92
+ }
93
+
94
+ length(callback) {
95
+ this._prune();
96
+ callback(null, this.sessions.size);
97
+ }
98
+ }
99
+
100
+ module.exports = { PanelSessionStore };