@tryty/easydc-js 1.1.0 → 1.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 +619 -504
- package/examples/mongo.js +44 -0
- package/index.d.ts +27 -6
- package/mongodb.d.ts +31 -0
- package/package.json +25 -4
- package/src/bot.js +105 -13
- package/src/database.js +24 -1
- package/src/functions/vars.js +8 -4
- package/src/index.js +2 -1
- package/src/plugins/mongodb.js +140 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Bot dengan database MongoDB.
|
|
2
|
+
// Pasang dulu: npm install easydc-js discord.js mongodb
|
|
3
|
+
// Jalankan: TOKEN=token_bot MONGO_URI="mongodb+srv://..." node examples/mongo.js
|
|
4
|
+
const { Bot } = require('easydc-js'); // saat dicoba dari folder ini: require('../src')
|
|
5
|
+
const { mongodb } = require('easydc-js/mongodb'); // saat dicoba dari folder ini: require('../src/plugins/mongodb')
|
|
6
|
+
|
|
7
|
+
const bot = new Bot({ token: process.env.TOKEN, prefix: '!', timezone: 'Asia/Jakarta' });
|
|
8
|
+
|
|
9
|
+
bot.use(mongodb({
|
|
10
|
+
uri: process.env.MONGO_URI,
|
|
11
|
+
dbName: 'botku',
|
|
12
|
+
// cache: false, // aktifkan baris ini jika bot dijalankan di lebih dari satu proses/server
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
bot.variables({ uang: 100 });
|
|
16
|
+
|
|
17
|
+
bot.command({ name: 'uang', code: '💰 $username punya $numberSeparator[$getUserVar[uang];.] koin' });
|
|
18
|
+
|
|
19
|
+
bot.command({
|
|
20
|
+
name: 'kerja',
|
|
21
|
+
code: `
|
|
22
|
+
$cooldown[1m;Istirahat dulu, tunggu %time%.]
|
|
23
|
+
$incUserVar[uang;$random[10;50]]
|
|
24
|
+
Kamu bekerja! Uangmu sekarang $getUserVar[uang]
|
|
25
|
+
`,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
bot.command({
|
|
29
|
+
name: 'kirim',
|
|
30
|
+
code: `
|
|
31
|
+
$argsCheck[2;Format: !kirim @user jumlah]
|
|
32
|
+
$onlyIf[$mentioned[1]!=;Mention user tujuan.]
|
|
33
|
+
$onlyIf[$isNumber[$message[2]]==true&&$message[2]>0;Jumlah harus angka lebih dari 0.]
|
|
34
|
+
$onlyIf[$getUserVar[uang]>=$message[2];Uangmu tidak cukup.]
|
|
35
|
+
$incUserVar[uang;-$message[2]]
|
|
36
|
+
$incUserVar[uang;$message[2];$mentioned[1]]
|
|
37
|
+
✅ Terkirim $message[2] koin ke <@$mentioned[1]>
|
|
38
|
+
`,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
bot.connect().then(() => console.log('Bot online, database terhubung.')).catch((e) => {
|
|
42
|
+
console.error(e.message);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
});
|
package/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface BotOptions {
|
|
|
15
15
|
handleSignals?: boolean;
|
|
16
16
|
/** Nyalakan $eval / $jsEval / bot.evalCommand (hanya untuk ID di `owners`). Bawaan: false */
|
|
17
17
|
eval?: boolean;
|
|
18
|
-
database?: { path?: string; memory?: boolean; saveDelay?: number } |
|
|
18
|
+
database?: { path?: string; memory?: boolean; saveDelay?: number } | DatabaseAdapter;
|
|
19
19
|
messages?: { cooldown?: string; error?: string };
|
|
20
20
|
status?: { text: string; type?: 'Playing' | 'Streaming' | 'Listening' | 'Watching' | 'Competing'; status?: 'online' | 'idle' | 'dnd' | 'invisible' };
|
|
21
21
|
onError?: (error: Error, ctx: Context) => unknown;
|
|
@@ -70,7 +70,19 @@ export class Context {
|
|
|
70
70
|
rawArgs: string;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
/** Adapter database: hanya get/set/delete yang wajib. Nilai balik boleh Promise. */
|
|
74
|
+
export interface DatabaseAdapter {
|
|
75
|
+
get(key: string): unknown | Promise<unknown>;
|
|
76
|
+
set(key: string, value: unknown): unknown | Promise<unknown>;
|
|
77
|
+
delete(key: string): unknown | Promise<unknown>;
|
|
78
|
+
has?(key: string): boolean | Promise<boolean>;
|
|
79
|
+
keys?(prefix?: string): string[] | Promise<string[]>;
|
|
80
|
+
flush?(): unknown | Promise<unknown>;
|
|
81
|
+
connect?(): Promise<void>;
|
|
82
|
+
close?(): unknown | Promise<unknown>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export class Database implements DatabaseAdapter {
|
|
74
86
|
constructor(options?: { path?: string; memory?: boolean; saveDelay?: number });
|
|
75
87
|
get(key: string): unknown;
|
|
76
88
|
set(key: string, value: unknown): void;
|
|
@@ -78,12 +90,18 @@ export class Database {
|
|
|
78
90
|
delete(key: string): boolean;
|
|
79
91
|
keys(prefix?: string): string[];
|
|
80
92
|
flush(): void;
|
|
93
|
+
close(): void;
|
|
81
94
|
}
|
|
82
95
|
|
|
96
|
+
export function copyDatabase(from: DatabaseAdapter, to: DatabaseAdapter, options?: { overwrite?: boolean; prefix?: string }): Promise<{ copied: number; skipped: number }>;
|
|
97
|
+
|
|
98
|
+
export type Plugin = ((bot: Bot, options?: unknown) => void) | { name?: string; install(bot: Bot, options?: unknown): void };
|
|
99
|
+
|
|
83
100
|
export class Bot {
|
|
84
101
|
constructor(options?: BotOptions);
|
|
85
102
|
client: Client | null;
|
|
86
|
-
db:
|
|
103
|
+
db: DatabaseAdapter;
|
|
104
|
+
plugins: Map<string, unknown>;
|
|
87
105
|
command(def: CommandDef): this;
|
|
88
106
|
slashCommand(def: SlashDef): this;
|
|
89
107
|
button(def: ComponentDef): this;
|
|
@@ -117,9 +135,12 @@ export class Bot {
|
|
|
117
135
|
addFunction(fn: CustomFunction): this;
|
|
118
136
|
loadCommands(dir: string): this;
|
|
119
137
|
validate(): true;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
138
|
+
use(plugin: Plugin, options?: unknown): this;
|
|
139
|
+
setDatabase(adapter: DatabaseAdapter): this;
|
|
140
|
+
getVar(scope: 'user' | 'server' | 'channel' | 'global', name: string, id?: string | null): Promise<string>;
|
|
141
|
+
setVar(scope: 'user' | 'server' | 'channel' | 'global', name: string, value: unknown, id?: string | null): Promise<void>;
|
|
142
|
+
resetVar(scope: 'user' | 'server' | 'channel' | 'global', name: string, id?: string | null): Promise<void>;
|
|
143
|
+
incVar(scope: 'user' | 'server' | 'channel' | 'global', name: string, amount: number | string, id?: string | null): Promise<string>;
|
|
123
144
|
connect(token?: string): Promise<this>;
|
|
124
145
|
destroy(): Promise<void>;
|
|
125
146
|
}
|
package/mongodb.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Database } from './index';
|
|
2
|
+
|
|
3
|
+
export interface MongoOptions {
|
|
4
|
+
/** Alamat MongoDB, mis. process.env.MONGO_URI. Wajib kecuali `client` diisi. */
|
|
5
|
+
uri?: string;
|
|
6
|
+
dbName?: string;
|
|
7
|
+
/** Bawaan: "easydc" */
|
|
8
|
+
collection?: string;
|
|
9
|
+
/** Bawaan true. Matikan bila lebih dari satu proses bot memakai database yang sama. */
|
|
10
|
+
cache?: boolean;
|
|
11
|
+
cacheLimit?: number;
|
|
12
|
+
/** MongoClient milik sendiri (tidak ditutup otomatis). */
|
|
13
|
+
client?: unknown;
|
|
14
|
+
driver?: unknown;
|
|
15
|
+
clientOptions?: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class MongoDatabase {
|
|
19
|
+
constructor(options: MongoOptions);
|
|
20
|
+
connect(): Promise<void>;
|
|
21
|
+
get(key: string): Promise<string | undefined>;
|
|
22
|
+
has(key: string): Promise<boolean>;
|
|
23
|
+
set(key: string, value: unknown): Promise<void>;
|
|
24
|
+
delete(key: string): Promise<boolean>;
|
|
25
|
+
keys(prefix?: string): Promise<string[]>;
|
|
26
|
+
flush(): Promise<void>;
|
|
27
|
+
close(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Plugin: bot.use(mongodb({ uri, dbName })) */
|
|
31
|
+
export function mongodb(options: MongoOptions): { name: string; install(bot: unknown): void };
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tryty/easydc-js",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Super easy Discord bot on top of discord.js: write commands with $function[arg] syntax.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
7
|
"files": [
|
|
8
8
|
"src",
|
|
9
9
|
"examples",
|
|
10
10
|
"index.d.ts",
|
|
11
|
+
"mongodb.d.ts",
|
|
11
12
|
"README.md",
|
|
12
13
|
"LICENSE"
|
|
13
14
|
],
|
|
@@ -16,11 +17,12 @@
|
|
|
16
17
|
"docs": "node scripts/gen-docs.js"
|
|
17
18
|
},
|
|
18
19
|
"keywords": [
|
|
20
|
+
"bot",
|
|
19
21
|
"discord",
|
|
20
22
|
"discord.js",
|
|
21
|
-
"bot",
|
|
22
|
-
"aoi.js",
|
|
23
23
|
"easy",
|
|
24
|
+
"mongodb",
|
|
25
|
+
"plugin",
|
|
24
26
|
"wrapper"
|
|
25
27
|
],
|
|
26
28
|
"author": "",
|
|
@@ -30,5 +32,24 @@
|
|
|
30
32
|
},
|
|
31
33
|
"dependencies": {
|
|
32
34
|
"discord.js": "^14.16.0"
|
|
35
|
+
},
|
|
36
|
+
"exports": {
|
|
37
|
+
".": {
|
|
38
|
+
"types": "./index.d.ts",
|
|
39
|
+
"default": "./src/index.js"
|
|
40
|
+
},
|
|
41
|
+
"./mongodb": {
|
|
42
|
+
"types": "./mongodb.d.ts",
|
|
43
|
+
"default": "./src/plugins/mongodb.js"
|
|
44
|
+
},
|
|
45
|
+
"./package.json": "./package.json"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"mongodb": ">=5.0.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"mongodb": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
33
54
|
}
|
|
34
55
|
}
|
package/src/bot.js
CHANGED
|
@@ -7,6 +7,7 @@ const { Interpreter } = require('./interpreter');
|
|
|
7
7
|
const { Context } = require('./context');
|
|
8
8
|
const { Database } = require('./database');
|
|
9
9
|
const { createRegistry } = require('./functions');
|
|
10
|
+
const { formatNumber } = require('./util');
|
|
10
11
|
const { f } = require('./functions/helpers');
|
|
11
12
|
|
|
12
13
|
const DEFAULT_INTENTS = [
|
|
@@ -38,6 +39,16 @@ const EVENT_TYPES = [
|
|
|
38
39
|
];
|
|
39
40
|
const VAR_SCOPES = ['user', 'server', 'channel', 'global'];
|
|
40
41
|
|
|
42
|
+
const ADAPTER_METHODS = ['get', 'set', 'delete'];
|
|
43
|
+
const looksLikeAdapter = (d) => !!d && typeof d === 'object' && ADAPTER_METHODS.some((m) => typeof d[m] === 'function');
|
|
44
|
+
|
|
45
|
+
/** Pastikan adapter database punya method wajib: get(key), set(key, value), delete(key). */
|
|
46
|
+
function checkAdapter(adapter) {
|
|
47
|
+
const missing = ADAPTER_METHODS.filter((m) => typeof adapter?.[m] !== 'function');
|
|
48
|
+
if (missing.length) throw new BotError(`Adapter database tidak lengkap, method yang kurang: ${missing.join(', ')}`);
|
|
49
|
+
return adapter;
|
|
50
|
+
}
|
|
51
|
+
|
|
41
52
|
const wildcardToRegExp = (pattern) => new RegExp(
|
|
42
53
|
`^${pattern.split('*').map((s) => s.replace(/[.+?^${}()|[\]\\]/g, '\\$&')).join('.*')}$`,
|
|
43
54
|
);
|
|
@@ -80,7 +91,9 @@ class Bot {
|
|
|
80
91
|
this.client = null;
|
|
81
92
|
this.registry = createRegistry();
|
|
82
93
|
this.interpreter = new Interpreter();
|
|
83
|
-
this.db = opts.database
|
|
94
|
+
this.db = looksLikeAdapter(opts.database) ? checkAdapter(opts.database) : new Database(opts.database);
|
|
95
|
+
this.plugins = new Map();
|
|
96
|
+
this._locks = new Map();
|
|
84
97
|
this.cooldowns = new Map();
|
|
85
98
|
|
|
86
99
|
this.commands = new Map(); // nama/alias -> handler
|
|
@@ -280,6 +293,33 @@ class Bot {
|
|
|
280
293
|
/** Jalan untuk setiap pesan yang BUKAN command. */
|
|
281
294
|
onMessage(arg) { return this._eventShortcut('message', arg); }
|
|
282
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Pasang plugin. Plugin = function(bot, opsi) atau object { name, install(bot, opsi) }.
|
|
298
|
+
* install harus sinkron (koneksi jaringan dilakukan adapter lewat connect()).
|
|
299
|
+
* bot.use(mongodb({ uri: process.env.MONGO_URI, dbName: 'botku' }))
|
|
300
|
+
*/
|
|
301
|
+
use(plugin, pluginOptions) {
|
|
302
|
+
const p = typeof plugin === 'function' ? { name: plugin.name || 'anonim', install: plugin } : plugin;
|
|
303
|
+
if (!p || typeof p.install !== 'function') {
|
|
304
|
+
throw new BotError('use: plugin harus berupa function atau object { name, install(bot) }');
|
|
305
|
+
}
|
|
306
|
+
const name = String(p.name ?? 'anonim');
|
|
307
|
+
if (name !== 'anonim' && this.plugins.has(name)) throw new BotError(`Plugin "${name}" sudah terpasang`);
|
|
308
|
+
const result = p.install(this, pluginOptions);
|
|
309
|
+
if (result && typeof result.then === 'function') {
|
|
310
|
+
throw new BotError(`Plugin "${name}": install() tidak boleh async. Lakukan koneksi di method connect() milik adapter`);
|
|
311
|
+
}
|
|
312
|
+
this.plugins.set(name, p);
|
|
313
|
+
return this;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Ganti database. Adapter wajib punya get/set/delete; opsional: has, keys, flush, connect, close. */
|
|
317
|
+
setDatabase(adapter) {
|
|
318
|
+
if (this._started) throw new BotError('setDatabase harus dipanggil sebelum connect()');
|
|
319
|
+
this.db = checkAdapter(adapter);
|
|
320
|
+
return this;
|
|
321
|
+
}
|
|
322
|
+
|
|
283
323
|
/** Deklarasi variabel + nilai awal: bot.variables({ uang: 0, bio: '-' }) */
|
|
284
324
|
variables(vars) {
|
|
285
325
|
if (!vars || typeof vars !== 'object') throw new BotError('variables: berikan object { nama: nilaiAwal }');
|
|
@@ -395,18 +435,49 @@ class Bot {
|
|
|
395
435
|
return scope === 'global' ? `global:${name}` : `${scope}:${id}:${name}`;
|
|
396
436
|
}
|
|
397
437
|
|
|
398
|
-
|
|
438
|
+
// Semua akses variabel bersifat asinkron karena adapter database (mis. MongoDB) berjalan lewat jaringan.
|
|
439
|
+
async getVar(scope, name, id) {
|
|
399
440
|
const key = this._varKey(scope, name, id);
|
|
400
|
-
const stored = this.db.get(key);
|
|
401
|
-
return stored === undefined ? this.declaredVars.get(name) : stored;
|
|
441
|
+
const stored = await this.db.get(key);
|
|
442
|
+
return stored === undefined || stored === null ? this.declaredVars.get(name) : stored;
|
|
402
443
|
}
|
|
403
444
|
|
|
404
|
-
setVar(scope, name, value, id) {
|
|
405
|
-
this.db.set(this._varKey(scope, name, id), String(value));
|
|
445
|
+
async setVar(scope, name, value, id) {
|
|
446
|
+
await this.db.set(this._varKey(scope, name, id), String(value));
|
|
406
447
|
}
|
|
407
448
|
|
|
408
|
-
resetVar(scope, name, id) {
|
|
409
|
-
this.db.delete(this._varKey(scope, name, id));
|
|
449
|
+
async resetVar(scope, name, id) {
|
|
450
|
+
await this.db.delete(this._varKey(scope, name, id));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Tambah (atau kurangi, bila negatif) variabel angka. Dijalankan berurutan per variabel di dalam satu proses,
|
|
455
|
+
* sehingga dua command bersamaan tidak saling menimpa. Mengembalikan nilai baru.
|
|
456
|
+
*/
|
|
457
|
+
async incVar(scope, name, amount, id) {
|
|
458
|
+
const key = this._varKey(scope, name, id);
|
|
459
|
+
const add = Number(String(amount).trim());
|
|
460
|
+
if (String(amount).trim() === '' || !Number.isFinite(add)) throw new BotError(`Jumlah "${amount}" bukan angka`);
|
|
461
|
+
const prev = this._locks.get(key) ?? Promise.resolve();
|
|
462
|
+
let release;
|
|
463
|
+
const gate = new Promise((resolve) => { release = resolve; });
|
|
464
|
+
const chain = prev.then(() => gate);
|
|
465
|
+
this._locks.set(key, chain);
|
|
466
|
+
await prev;
|
|
467
|
+
try {
|
|
468
|
+
const stored = await this.db.get(key);
|
|
469
|
+
const current = stored === undefined || stored === null ? this.declaredVars.get(name) : stored;
|
|
470
|
+
const base = Number(String(current).trim());
|
|
471
|
+
if (String(current).trim() === '' || !Number.isFinite(base)) {
|
|
472
|
+
throw new BotError(`Variabel "${name}" bernilai "${current}", bukan angka`);
|
|
473
|
+
}
|
|
474
|
+
const next = formatNumber(base + add);
|
|
475
|
+
await this.db.set(key, next);
|
|
476
|
+
return next;
|
|
477
|
+
} finally {
|
|
478
|
+
release();
|
|
479
|
+
if (this._locks.get(key) === chain) this._locks.delete(key);
|
|
480
|
+
}
|
|
410
481
|
}
|
|
411
482
|
|
|
412
483
|
// ===== Menjalankan =========================================================
|
|
@@ -748,6 +819,7 @@ class Bot {
|
|
|
748
819
|
throw new BotError('Token bot belum diisi. Contoh: new Bot({ token: process.env.TOKEN })');
|
|
749
820
|
}
|
|
750
821
|
this.validate();
|
|
822
|
+
this._started = true;
|
|
751
823
|
const discord = this._discord();
|
|
752
824
|
const client = this.options.client ?? new discord.Client({
|
|
753
825
|
intents: this._resolveIntents(discord),
|
|
@@ -757,19 +829,39 @@ class Bot {
|
|
|
757
829
|
this._bind(client, discord);
|
|
758
830
|
if (this.options.handleSignals && !this._signalsBound) {
|
|
759
831
|
this._signalsBound = true;
|
|
760
|
-
const shutdown = () => {
|
|
832
|
+
const shutdown = async () => {
|
|
833
|
+
const timeout = new Promise((resolve) => { setTimeout(resolve, 5000); });
|
|
834
|
+
try { await Promise.race([this.destroy(), timeout]); } catch { /* abaikan */ }
|
|
835
|
+
process.exit(0);
|
|
836
|
+
};
|
|
761
837
|
process.once('SIGINT', shutdown);
|
|
762
838
|
process.once('SIGTERM', shutdown);
|
|
763
|
-
process.once('exit', () => {
|
|
839
|
+
process.once('exit', () => {
|
|
840
|
+
try {
|
|
841
|
+
const r = this.db.flush?.();
|
|
842
|
+
if (r && typeof r.catch === 'function') r.catch(() => {});
|
|
843
|
+
} catch { /* abaikan */ }
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
try {
|
|
847
|
+
await this.db.connect?.();
|
|
848
|
+
} catch (e) {
|
|
849
|
+
throw new BotError(`Gagal terhubung ke database: ${e.message}`, { cause: e });
|
|
850
|
+
}
|
|
851
|
+
try {
|
|
852
|
+
await client.login(token);
|
|
853
|
+
} catch (e) {
|
|
854
|
+
try { await this.db.close?.(); } catch { /* abaikan */ }
|
|
855
|
+
throw e;
|
|
764
856
|
}
|
|
765
|
-
await client.login(token);
|
|
766
857
|
return this;
|
|
767
858
|
}
|
|
768
859
|
|
|
769
|
-
/** Matikan bot dan
|
|
860
|
+
/** Matikan bot, simpan database, dan tutup koneksinya. */
|
|
770
861
|
async destroy() {
|
|
771
|
-
this.db.flush();
|
|
862
|
+
await this.db.flush?.();
|
|
772
863
|
if (this.client && typeof this.client.destroy === 'function') await this.client.destroy();
|
|
864
|
+
await this.db.close?.();
|
|
773
865
|
}
|
|
774
866
|
}
|
|
775
867
|
|
package/src/database.js
CHANGED
|
@@ -60,6 +60,9 @@ class Database {
|
|
|
60
60
|
if (this._timer.unref) this._timer.unref();
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/** Dipanggil saat bot dimatikan: simpan data. */
|
|
64
|
+
close() { this.flush(); }
|
|
65
|
+
|
|
63
66
|
/** Tulis ke disk sekarang juga (sinkron). */
|
|
64
67
|
flush() {
|
|
65
68
|
if (!this.file) return;
|
|
@@ -70,4 +73,24 @@ class Database {
|
|
|
70
73
|
}
|
|
71
74
|
}
|
|
72
75
|
|
|
73
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Salin semua data dari satu database ke database lain (mis. file JSON -> MongoDB).
|
|
78
|
+
* Data yang sudah ada di tujuan tidak ditimpa, kecuali overwrite: true.
|
|
79
|
+
* Kedua adapter harus sudah connect() bila perlu, dan `from` wajib punya method keys().
|
|
80
|
+
* Mengembalikan { copied, skipped }.
|
|
81
|
+
*/
|
|
82
|
+
async function copyDatabase(from, to, { overwrite = false, prefix = '' } = {}) {
|
|
83
|
+
if (typeof from?.keys !== 'function') throw new Error('copyDatabase: database asal harus punya method keys()');
|
|
84
|
+
let copied = 0;
|
|
85
|
+
let skipped = 0;
|
|
86
|
+
for (const key of await from.keys(prefix)) {
|
|
87
|
+
if (!overwrite && (await to.get(key)) !== undefined) { skipped++; continue; }
|
|
88
|
+
const value = await from.get(key);
|
|
89
|
+
if (value === undefined) continue;
|
|
90
|
+
await to.set(key, value);
|
|
91
|
+
copied++;
|
|
92
|
+
}
|
|
93
|
+
return { copied, skipped };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { Database, copyDatabase };
|
package/src/functions/vars.js
CHANGED
|
@@ -17,21 +17,25 @@ for (const [Scope, scope, defaultId, label] of SCOPES) {
|
|
|
17
17
|
};
|
|
18
18
|
out.push(
|
|
19
19
|
f(`set${Scope}Var`, { b: 'required', min: 2, max: 3, s: `[nama;nilai;${label}ID?]`, d: `Simpan variabel ${label} (harus dideklarasikan dengan bot.variables)` },
|
|
20
|
-
(ctx, [name, value, id]) => { ctx.bot.setVar(scope, name, value, idFor(ctx, id)); return ''; }),
|
|
20
|
+
async (ctx, [name, value, id]) => { await ctx.bot.setVar(scope, name, value, idFor(ctx, id)); return ''; }),
|
|
21
21
|
f(`get${Scope}Var`, { b: 'required', min: 1, max: 2, s: `[nama;${label}ID?]`, d: `Baca variabel ${label}` },
|
|
22
22
|
(ctx, [name, id]) => ctx.bot.getVar(scope, name, idFor(ctx, id))),
|
|
23
23
|
f(`reset${Scope}Var`, { b: 'required', min: 1, max: 2, s: `[nama;${label}ID?]`, d: `Kembalikan variabel ${label} ke nilai awal` },
|
|
24
|
-
(ctx, [name, id]) => { ctx.bot.resetVar(scope, name, idFor(ctx, id)); return ''; }),
|
|
24
|
+
async (ctx, [name, id]) => { await ctx.bot.resetVar(scope, name, idFor(ctx, id)); return ''; }),
|
|
25
|
+
f(`inc${Scope}Var`, { b: 'required', min: 2, max: 3, s: `[nama;jumlah;${label}ID?]`, d: `Tambah variabel ${label} berupa angka (jumlah negatif = kurangi). Aman dari tabrakan antar command. Tidak menampilkan apa pun` },
|
|
26
|
+
async (ctx, [name, amount, id]) => { await ctx.bot.incVar(scope, name, amount, idFor(ctx, id)); return ''; }),
|
|
25
27
|
);
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
out.push(
|
|
29
31
|
f('setGlobalVar', { b: 'required', min: 2, max: 2, s: '[nama;nilai]', d: 'Simpan variabel global' },
|
|
30
|
-
(ctx, [name, value]) => { ctx.bot.setVar('global', name, value, null); return ''; }),
|
|
32
|
+
async (ctx, [name, value]) => { await ctx.bot.setVar('global', name, value, null); return ''; }),
|
|
31
33
|
f('getGlobalVar', { b: 'required', min: 1, max: 1, s: '[nama]', d: 'Baca variabel global' },
|
|
32
34
|
(ctx, [name]) => ctx.bot.getVar('global', name, null)),
|
|
33
35
|
f('resetGlobalVar', { b: 'required', min: 1, max: 1, s: '[nama]', d: 'Kembalikan variabel global ke nilai awal' },
|
|
34
|
-
(ctx, [name]) => { ctx.bot.resetVar('global', name, null); return ''; }),
|
|
36
|
+
async (ctx, [name]) => { await ctx.bot.resetVar('global', name, null); return ''; }),
|
|
37
|
+
f('incGlobalVar', { b: 'required', min: 2, max: 2, s: '[nama;jumlah]', d: 'Tambah variabel global berupa angka (jumlah negatif = kurangi)' },
|
|
38
|
+
async (ctx, [name, amount]) => { await ctx.bot.incVar('global', name, amount, null); return ''; }),
|
|
35
39
|
);
|
|
36
40
|
|
|
37
41
|
module.exports = out;
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const { Bot } = require('./bot');
|
|
3
|
-
const { Database } = require('./database');
|
|
3
|
+
const { Database, copyDatabase } = require('./database');
|
|
4
4
|
const { Context } = require('./context');
|
|
5
5
|
const { BotError, ParseError, ValidationError } = require('./errors');
|
|
6
6
|
const { parse } = require('./parser');
|
|
@@ -9,6 +9,7 @@ const { createRegistry, CATEGORIES } = require('./functions');
|
|
|
9
9
|
module.exports = {
|
|
10
10
|
Bot,
|
|
11
11
|
Database,
|
|
12
|
+
copyDatabase,
|
|
12
13
|
Context,
|
|
13
14
|
BotError,
|
|
14
15
|
ParseError,
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { BotError } = require('../errors');
|
|
3
|
+
|
|
4
|
+
const MISSING = Symbol('missing');
|
|
5
|
+
const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Adapter database MongoDB. Setiap variabel = satu dokumen { _id: kunci, v: nilai, t: waktuUbah }.
|
|
9
|
+
*
|
|
10
|
+
* Opsi:
|
|
11
|
+
* - uri alamat MongoDB (wajib kecuali memberi `client`), mis. process.env.MONGO_URI
|
|
12
|
+
* - dbName nama database (bila kosong dipakai nama dari URI, atau "test")
|
|
13
|
+
* - collection nama collection, bawaan "easydc"
|
|
14
|
+
* - cache true (bawaan): simpan data yang sudah dibaca di RAM. Matikan (false) bila
|
|
15
|
+
* LEBIH DARI SATU proses bot memakai database yang sama.
|
|
16
|
+
* - cacheLimit maksimal jumlah kunci di cache (bawaan 10000)
|
|
17
|
+
* - client MongoClient milik sendiri (tidak ditutup otomatis)
|
|
18
|
+
* - driver modul mongodb sendiri (untuk pengujian)
|
|
19
|
+
* - clientOptions opsi tambahan untuk new MongoClient(uri, opsi)
|
|
20
|
+
*/
|
|
21
|
+
class MongoDatabase {
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
if (!options.uri && !options.client) {
|
|
24
|
+
throw new BotError('mongodb: opsi "uri" wajib diisi. Contoh: mongodb({ uri: process.env.MONGO_URI, dbName: "botku" })');
|
|
25
|
+
}
|
|
26
|
+
this.options = options;
|
|
27
|
+
this.client = options.client ?? null;
|
|
28
|
+
this._ownsClient = !options.client;
|
|
29
|
+
this.collectionName = options.collection ?? 'easydc';
|
|
30
|
+
if (typeof this.collectionName !== 'string' || !/^[^$\0]{1,120}$/.test(this.collectionName) || this.collectionName.startsWith('system.')) {
|
|
31
|
+
throw new BotError(`mongodb: nama collection "${this.collectionName}" tidak valid`);
|
|
32
|
+
}
|
|
33
|
+
this.useCache = options.cache !== false;
|
|
34
|
+
this.cacheLimit = options.cacheLimit ?? 10000;
|
|
35
|
+
this.cache = new Map();
|
|
36
|
+
this._gen = new Map();
|
|
37
|
+
this.col = null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_loadDriver() {
|
|
41
|
+
if (this.options.driver) return this.options.driver;
|
|
42
|
+
try {
|
|
43
|
+
// eslint-disable-next-line global-require
|
|
44
|
+
return require('mongodb');
|
|
45
|
+
} catch (e) {
|
|
46
|
+
throw new BotError('Package "mongodb" tidak ditemukan. Jalankan: npm install mongodb', { cause: e });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async connect() {
|
|
51
|
+
if (this.col) return;
|
|
52
|
+
if (!this.client) {
|
|
53
|
+
const { MongoClient } = this._loadDriver();
|
|
54
|
+
this.client = new MongoClient(this.options.uri, { serverSelectionTimeoutMS: 10000, ...this.options.clientOptions });
|
|
55
|
+
}
|
|
56
|
+
await this.client.connect();
|
|
57
|
+
this.col = this.client.db(this.options.dbName).collection(this.collectionName);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
_ready() {
|
|
61
|
+
if (!this.col) throw new BotError('Database MongoDB belum terhubung. Pastikan bot.connect() sudah dipanggil');
|
|
62
|
+
return this.col;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_touch(key) { this._gen.set(key, (this._gen.get(key) ?? 0) + 1); }
|
|
66
|
+
|
|
67
|
+
_remember(key, value) {
|
|
68
|
+
if (!this.useCache) return;
|
|
69
|
+
this.cache.delete(key); // pindahkan ke posisi terbaru
|
|
70
|
+
this.cache.set(key, value);
|
|
71
|
+
if (this.cache.size > this.cacheLimit) this.cache.delete(this.cache.keys().next().value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async get(key) {
|
|
75
|
+
if (this.useCache && this.cache.has(key)) {
|
|
76
|
+
const hit = this.cache.get(key);
|
|
77
|
+
return hit === MISSING ? undefined : hit;
|
|
78
|
+
}
|
|
79
|
+
const col = this._ready();
|
|
80
|
+
const gen = this._gen.get(key) ?? 0;
|
|
81
|
+
const doc = await col.findOne({ _id: key });
|
|
82
|
+
// Jangan isi cache bila kunci ini diubah selagi kita membaca (supaya tidak menyimpan data basi)
|
|
83
|
+
if ((this._gen.get(key) ?? 0) === gen) this._remember(key, doc ? doc.v : MISSING);
|
|
84
|
+
return doc ? doc.v : undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async has(key) { return (await this.get(key)) !== undefined; }
|
|
88
|
+
|
|
89
|
+
async set(key, value) {
|
|
90
|
+
const col = this._ready();
|
|
91
|
+
this._touch(key);
|
|
92
|
+
await col.updateOne({ _id: key }, { $set: { v: value, t: new Date() } }, { upsert: true });
|
|
93
|
+
this._touch(key);
|
|
94
|
+
this._remember(key, value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async delete(key) {
|
|
98
|
+
const col = this._ready();
|
|
99
|
+
this._touch(key);
|
|
100
|
+
const res = await col.deleteOne({ _id: key });
|
|
101
|
+
this._touch(key);
|
|
102
|
+
this._remember(key, MISSING);
|
|
103
|
+
return (res?.deletedCount ?? 0) > 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Daftar kunci berawalan `prefix` (langsung dari database, bukan dari cache). */
|
|
107
|
+
async keys(prefix = '') {
|
|
108
|
+
const col = this._ready();
|
|
109
|
+
const filter = prefix ? { _id: { $regex: `^${escapeRegex(prefix)}` } } : {};
|
|
110
|
+
const docs = await col.find(filter, { projection: { _id: 1 } }).toArray();
|
|
111
|
+
return docs.map((d) => d._id);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Penulisan ke MongoDB langsung (tanpa antrean), jadi tidak ada yang perlu di-flush. */
|
|
115
|
+
async flush() {}
|
|
116
|
+
|
|
117
|
+
async close() {
|
|
118
|
+
this.cache.clear();
|
|
119
|
+
this._gen.clear();
|
|
120
|
+
const had = this.col;
|
|
121
|
+
this.col = null;
|
|
122
|
+
if (had && this._ownsClient && this.client) await this.client.close();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Plugin MongoDB untuk Bot.
|
|
128
|
+
* const { mongodb } = require('easydc-js/mongodb');
|
|
129
|
+
* bot.use(mongodb({ uri: process.env.MONGO_URI, dbName: 'botku' }));
|
|
130
|
+
*/
|
|
131
|
+
function mongodb(options) {
|
|
132
|
+
return {
|
|
133
|
+
name: 'mongodb',
|
|
134
|
+
install(bot) {
|
|
135
|
+
bot.setDatabase(new MongoDatabase(options));
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = { mongodb, MongoDatabase };
|