@newt-dev/compiler 0.1.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/dist/src/ast.d.ts +220 -0
- package/dist/src/ast.js +1 -0
- package/dist/src/codegen.d.ts +6 -0
- package/dist/src/codegen.js +266 -0
- package/dist/src/errors.d.ts +31 -0
- package/dist/src/errors.js +120 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.js +33 -0
- package/dist/src/lexer.d.ts +9 -0
- package/dist/src/lexer.js +285 -0
- package/dist/src/parser.d.ts +3 -0
- package/dist/src/parser.js +459 -0
- package/dist/src/validator.d.ts +3 -0
- package/dist/src/validator.js +176 -0
- package/dist/test/integration.test.d.ts +1 -0
- package/dist/test/integration.test.js +37 -0
- package/package.json +37 -0
- package/src/ast.ts +307 -0
- package/src/codegen.ts +286 -0
- package/src/errors.ts +164 -0
- package/src/index.ts +48 -0
- package/src/lexer.ts +349 -0
- package/src/parser.ts +534 -0
- package/src/validator.ts +191 -0
- package/test/integration.test.ts +42 -0
- package/tsconfig.json +18 -0
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@newt-dev/compiler",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Compiler for the Newt Discord bot DSL.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/src/index.js",
|
|
7
|
+
"types": "dist/src/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/src/index.d.ts",
|
|
11
|
+
"import": "./dist/src/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc -p tsconfig.json",
|
|
16
|
+
"check": "tsc -p tsconfig.json --noEmit",
|
|
17
|
+
"test": "node --test dist/test/*.test.js"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"discord",
|
|
21
|
+
"bot",
|
|
22
|
+
"dsl",
|
|
23
|
+
"compiler",
|
|
24
|
+
"newt"
|
|
25
|
+
],
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/newt-dev-sudo/newt.git",
|
|
29
|
+
"directory": "packages/compiler"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/newt-dev-sudo/newt#readme",
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^20.12.0",
|
|
34
|
+
"typescript": "^5.5.0"
|
|
35
|
+
},
|
|
36
|
+
"license": "MIT"
|
|
37
|
+
}
|
package/src/ast.ts
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
export interface SourceLocation {
|
|
2
|
+
line: number;
|
|
3
|
+
column: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface BaseNode {
|
|
7
|
+
type: string;
|
|
8
|
+
loc: SourceLocation;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface Program extends BaseNode {
|
|
12
|
+
type: "Program";
|
|
13
|
+
body: TopLevelNode[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type TopLevelNode = BotDecl | Handler | TimerDecl;
|
|
17
|
+
|
|
18
|
+
export type BotDeclKind = "name" | "prefix" | "token";
|
|
19
|
+
|
|
20
|
+
export interface BotDecl extends BaseNode {
|
|
21
|
+
type: "BotDecl";
|
|
22
|
+
kind: BotDeclKind;
|
|
23
|
+
value: StringLiteral;
|
|
24
|
+
fromEnv: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type Handler =
|
|
28
|
+
| ReadyHandler
|
|
29
|
+
| CommandHandler
|
|
30
|
+
| MessageContainsHandler
|
|
31
|
+
| JoinHandler
|
|
32
|
+
| LeaveHandler
|
|
33
|
+
| ReactionAddHandler;
|
|
34
|
+
|
|
35
|
+
export interface ReadyHandler extends BaseNode {
|
|
36
|
+
type: "ReadyHandler";
|
|
37
|
+
body: Statement[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface CommandHandler extends BaseNode {
|
|
41
|
+
type: "CommandHandler";
|
|
42
|
+
command: string;
|
|
43
|
+
body: Statement[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface MessageContainsHandler extends BaseNode {
|
|
47
|
+
type: "MessageContainsHandler";
|
|
48
|
+
needle: StringLiteral;
|
|
49
|
+
body: Statement[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface JoinHandler extends BaseNode {
|
|
53
|
+
type: "JoinHandler";
|
|
54
|
+
body: Statement[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface LeaveHandler extends BaseNode {
|
|
58
|
+
type: "LeaveHandler";
|
|
59
|
+
body: Statement[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ReactionAddHandler extends BaseNode {
|
|
63
|
+
type: "ReactionAddHandler";
|
|
64
|
+
emoji: StringLiteral;
|
|
65
|
+
body: Statement[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type TimerDecl = EveryTimerDecl | DailyTimerDecl;
|
|
69
|
+
|
|
70
|
+
export interface EveryTimerDecl extends BaseNode {
|
|
71
|
+
type: "EveryTimerDecl";
|
|
72
|
+
amount: NumberLiteral;
|
|
73
|
+
unit: TimeUnit;
|
|
74
|
+
body: Statement[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface DailyTimerDecl extends BaseNode {
|
|
78
|
+
type: "DailyTimerDecl";
|
|
79
|
+
time: StringLiteral;
|
|
80
|
+
body: Statement[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type TimeUnit = "second" | "seconds" | "minute" | "minutes" | "hour" | "hours" | "day" | "days";
|
|
84
|
+
|
|
85
|
+
export type Statement =
|
|
86
|
+
| ReplyStatement
|
|
87
|
+
| SayStatement
|
|
88
|
+
| SayEmbedStatement
|
|
89
|
+
| LetDecl
|
|
90
|
+
| StoreStatement
|
|
91
|
+
| IfStatement
|
|
92
|
+
| ForEachStatement
|
|
93
|
+
| RequireRoleStatement
|
|
94
|
+
| GiveRoleStatement
|
|
95
|
+
| RemoveRoleStatement
|
|
96
|
+
| MuteStatement
|
|
97
|
+
| KickStatement
|
|
98
|
+
| BanStatement
|
|
99
|
+
| PinStatement
|
|
100
|
+
| DeleteKeyStatement
|
|
101
|
+
| WaitStatement
|
|
102
|
+
| TryCatchStatement
|
|
103
|
+
| ExpressionStatement;
|
|
104
|
+
|
|
105
|
+
export interface ReplyStatement extends BaseNode {
|
|
106
|
+
type: "ReplyStatement";
|
|
107
|
+
message: Expression;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface SayStatement extends BaseNode {
|
|
111
|
+
type: "SayStatement";
|
|
112
|
+
message: Expression;
|
|
113
|
+
channel?: StringLiteral;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface SayEmbedStatement extends BaseNode {
|
|
117
|
+
type: "SayEmbedStatement";
|
|
118
|
+
embed: EmbedBlock;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface EmbedBlock extends BaseNode {
|
|
122
|
+
type: "EmbedBlock";
|
|
123
|
+
title?: StringLiteral;
|
|
124
|
+
description?: StringLiteral;
|
|
125
|
+
color?: ColorLiteral;
|
|
126
|
+
fields: EmbedField[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface EmbedField extends BaseNode {
|
|
130
|
+
type: "EmbedField";
|
|
131
|
+
name: StringLiteral;
|
|
132
|
+
value: StringLiteral;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface LetDecl extends BaseNode {
|
|
136
|
+
type: "LetDecl";
|
|
137
|
+
name: string;
|
|
138
|
+
value: Expression;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface StoreStatement extends BaseNode {
|
|
142
|
+
type: "StoreStatement";
|
|
143
|
+
namespace: Expression;
|
|
144
|
+
key: string;
|
|
145
|
+
value: Expression;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface IfStatement extends BaseNode {
|
|
149
|
+
type: "IfStatement";
|
|
150
|
+
condition: Expression;
|
|
151
|
+
consequent: Statement[];
|
|
152
|
+
alternate: Statement[];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface ForEachStatement extends BaseNode {
|
|
156
|
+
type: "ForEachStatement";
|
|
157
|
+
itemName: string;
|
|
158
|
+
iterable: Expression;
|
|
159
|
+
body: Statement[];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface RequireRoleStatement extends BaseNode {
|
|
163
|
+
type: "RequireRoleStatement";
|
|
164
|
+
role: StringLiteral;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface GiveRoleStatement extends BaseNode {
|
|
168
|
+
type: "GiveRoleStatement";
|
|
169
|
+
subject: Expression;
|
|
170
|
+
role: StringLiteral;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface RemoveRoleStatement extends BaseNode {
|
|
174
|
+
type: "RemoveRoleStatement";
|
|
175
|
+
subject: Expression;
|
|
176
|
+
role: StringLiteral;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface MuteStatement extends BaseNode {
|
|
180
|
+
type: "MuteStatement";
|
|
181
|
+
subject: Expression;
|
|
182
|
+
duration?: DurationLiteral;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface KickStatement extends BaseNode {
|
|
186
|
+
type: "KickStatement";
|
|
187
|
+
subject: Expression;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface BanStatement extends BaseNode {
|
|
191
|
+
type: "BanStatement";
|
|
192
|
+
subject: Expression;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface PinStatement extends BaseNode {
|
|
196
|
+
type: "PinStatement";
|
|
197
|
+
target: Expression;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface DeleteKeyStatement extends BaseNode {
|
|
201
|
+
type: "DeleteKeyStatement";
|
|
202
|
+
namespace: Expression;
|
|
203
|
+
key: string;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface WaitStatement extends BaseNode {
|
|
207
|
+
type: "WaitStatement";
|
|
208
|
+
duration: DurationLiteral;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface TryCatchStatement extends BaseNode {
|
|
212
|
+
type: "TryCatchStatement";
|
|
213
|
+
body: Statement[];
|
|
214
|
+
errorHandler: Statement[];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface ExpressionStatement extends BaseNode {
|
|
218
|
+
type: "ExpressionStatement";
|
|
219
|
+
expression: Expression;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export type Expression =
|
|
223
|
+
| StringLiteral
|
|
224
|
+
| NumberLiteral
|
|
225
|
+
| BooleanLiteral
|
|
226
|
+
| ColorLiteral
|
|
227
|
+
| IdentifierExpr
|
|
228
|
+
| MemberExpr
|
|
229
|
+
| ArgsIndexExpr
|
|
230
|
+
| LoadExpr
|
|
231
|
+
| FetchExpr
|
|
232
|
+
| BinaryExpr
|
|
233
|
+
| UnaryExpr
|
|
234
|
+
| CallExpr;
|
|
235
|
+
|
|
236
|
+
export interface StringLiteral extends BaseNode {
|
|
237
|
+
type: "StringLiteral";
|
|
238
|
+
value: string;
|
|
239
|
+
interpolated: boolean;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface NumberLiteral extends BaseNode {
|
|
243
|
+
type: "NumberLiteral";
|
|
244
|
+
value: number;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export interface BooleanLiteral extends BaseNode {
|
|
248
|
+
type: "BooleanLiteral";
|
|
249
|
+
value: boolean;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export interface ColorLiteral extends BaseNode {
|
|
253
|
+
type: "ColorLiteral";
|
|
254
|
+
value: string;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export interface IdentifierExpr extends BaseNode {
|
|
258
|
+
type: "IdentifierExpr";
|
|
259
|
+
name: string;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export interface MemberExpr extends BaseNode {
|
|
263
|
+
type: "MemberExpr";
|
|
264
|
+
path: string[];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export interface ArgsIndexExpr extends BaseNode {
|
|
268
|
+
type: "ArgsIndexExpr";
|
|
269
|
+
index: number;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export interface LoadExpr extends BaseNode {
|
|
273
|
+
type: "LoadExpr";
|
|
274
|
+
namespace: Expression;
|
|
275
|
+
key: string;
|
|
276
|
+
fallback?: Expression;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export interface FetchExpr extends BaseNode {
|
|
280
|
+
type: "FetchExpr";
|
|
281
|
+
url: Expression;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export interface BinaryExpr extends BaseNode {
|
|
285
|
+
type: "BinaryExpr";
|
|
286
|
+
operator: string;
|
|
287
|
+
left: Expression;
|
|
288
|
+
right: Expression;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export interface UnaryExpr extends BaseNode {
|
|
292
|
+
type: "UnaryExpr";
|
|
293
|
+
operator: string;
|
|
294
|
+
argument: Expression;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export interface CallExpr extends BaseNode {
|
|
298
|
+
type: "CallExpr";
|
|
299
|
+
callee: string;
|
|
300
|
+
args: Expression[];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export interface DurationLiteral extends BaseNode {
|
|
304
|
+
type: "DurationLiteral";
|
|
305
|
+
amount: NumberLiteral;
|
|
306
|
+
unit: TimeUnit;
|
|
307
|
+
}
|
package/src/codegen.ts
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BotDecl,
|
|
3
|
+
Expression,
|
|
4
|
+
Handler,
|
|
5
|
+
Program,
|
|
6
|
+
Statement,
|
|
7
|
+
TimerDecl
|
|
8
|
+
} from "./ast.js";
|
|
9
|
+
|
|
10
|
+
export interface GeneratedProject {
|
|
11
|
+
botJs: string;
|
|
12
|
+
packageJson: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function generate(program: Program): GeneratedProject {
|
|
16
|
+
const botName = getBotValue(program, "name") ?? "NewtBot";
|
|
17
|
+
const prefix = getBotValue(program, "prefix") ?? "!";
|
|
18
|
+
const tokenDecl = program.body.find((node): node is BotDecl => node.type === "BotDecl" && node.kind === "token");
|
|
19
|
+
const tokenExpr = tokenDecl?.fromEnv ? `process.env.${tokenDecl.value.value}` : JSON.stringify(tokenDecl?.value.value ?? "");
|
|
20
|
+
const handlers = program.body
|
|
21
|
+
.filter((node): node is Handler | TimerDecl => node.type.endsWith("Handler") || node.type.endsWith("TimerDecl"))
|
|
22
|
+
.map((node) => emitTopLevel(node, prefix))
|
|
23
|
+
.join("\n\n");
|
|
24
|
+
|
|
25
|
+
const botJs = `import { Client, EmbedBuilder, GatewayIntentBits, Partials } from "discord.js";
|
|
26
|
+
import Database from "better-sqlite3";
|
|
27
|
+
import axios from "axios";
|
|
28
|
+
|
|
29
|
+
const client = new Client({
|
|
30
|
+
intents: [
|
|
31
|
+
GatewayIntentBits.Guilds,
|
|
32
|
+
GatewayIntentBits.GuildMessages,
|
|
33
|
+
GatewayIntentBits.GuildMembers,
|
|
34
|
+
GatewayIntentBits.MessageContent,
|
|
35
|
+
GatewayIntentBits.GuildMessageReactions
|
|
36
|
+
],
|
|
37
|
+
partials: [Partials.Message, Partials.Channel, Partials.Reaction]
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const db = new Database("newt-store.sqlite");
|
|
41
|
+
db.exec("CREATE TABLE IF NOT EXISTS store (namespace TEXT NOT NULL, key TEXT NOT NULL, value TEXT, PRIMARY KEY(namespace, key))");
|
|
42
|
+
const botName = ${JSON.stringify(botName)};
|
|
43
|
+
const prefix = ${JSON.stringify(prefix)};
|
|
44
|
+
|
|
45
|
+
function saveValue(namespace, key, value) {
|
|
46
|
+
db.prepare("INSERT OR REPLACE INTO store(namespace, key, value) VALUES (?, ?, ?)").run(String(namespace), String(key), JSON.stringify(value));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function loadValue(namespace, key, fallback = undefined) {
|
|
50
|
+
const row = db.prepare("SELECT value FROM store WHERE namespace = ? AND key = ?").get(String(namespace), String(key));
|
|
51
|
+
return row ? JSON.parse(row.value) : fallback;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function findChannel(guild, name) {
|
|
55
|
+
return guild?.channels?.cache?.find((channel) => channel.name === name);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function findRole(guild, name) {
|
|
59
|
+
return guild?.roles?.cache?.find((role) => role.name === name);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
${handlers}
|
|
63
|
+
|
|
64
|
+
client.login(${tokenExpr});
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
botJs,
|
|
69
|
+
packageJson: JSON.stringify({
|
|
70
|
+
type: "module",
|
|
71
|
+
scripts: { start: "node bot.js" },
|
|
72
|
+
dependencies: {
|
|
73
|
+
"axios": "^1.7.0",
|
|
74
|
+
"better-sqlite3": "^11.0.0",
|
|
75
|
+
"discord.js": "^14.15.0"
|
|
76
|
+
}
|
|
77
|
+
}, null, 2)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getBotValue(program: Program, kind: BotDecl["kind"]): string | undefined {
|
|
82
|
+
return program.body.find((node): node is BotDecl => node.type === "BotDecl" && node.kind === kind)?.value.value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function emitTopLevel(node: Handler | TimerDecl, prefix: string): string {
|
|
86
|
+
switch (node.type) {
|
|
87
|
+
case "ReadyHandler":
|
|
88
|
+
return `client.once("ready", async () => {
|
|
89
|
+
for (const guild of client.guilds.cache.values()) {
|
|
90
|
+
const server = guild;
|
|
91
|
+
${emitStatements(node.body, " ", "guild")}
|
|
92
|
+
}
|
|
93
|
+
});`;
|
|
94
|
+
case "CommandHandler":
|
|
95
|
+
return `client.on("messageCreate", async (message) => {
|
|
96
|
+
if (message.author.bot) return;
|
|
97
|
+
if (!message.content.startsWith(prefix + ${JSON.stringify(node.command)})) return;
|
|
98
|
+
const args = message.content.slice((prefix + ${JSON.stringify(node.command)}).length).trim().split(/\\s+/).filter(Boolean);
|
|
99
|
+
const user = message.author;
|
|
100
|
+
const channel = message.channel;
|
|
101
|
+
const server = message.guild;
|
|
102
|
+
const target = message.mentions.members.first();
|
|
103
|
+
${emitStatements(node.body, " ", "message")}
|
|
104
|
+
});`;
|
|
105
|
+
case "MessageContainsHandler":
|
|
106
|
+
return `client.on("messageCreate", async (message) => {
|
|
107
|
+
if (message.author.bot) return;
|
|
108
|
+
if (!message.content.includes(${emitExpression(node.needle)})) return;
|
|
109
|
+
const user = message.author;
|
|
110
|
+
const channel = message.channel;
|
|
111
|
+
const server = message.guild;
|
|
112
|
+
${emitStatements(node.body, " ", "message")}
|
|
113
|
+
});`;
|
|
114
|
+
case "JoinHandler":
|
|
115
|
+
return `client.on("guildMemberAdd", async (member) => {
|
|
116
|
+
const user = member.user;
|
|
117
|
+
const server = member.guild;
|
|
118
|
+
const channel = findChannel(server, "general");
|
|
119
|
+
${emitStatements(node.body, " ", "member")}
|
|
120
|
+
});`;
|
|
121
|
+
case "LeaveHandler":
|
|
122
|
+
return `client.on("guildMemberRemove", async (member) => {
|
|
123
|
+
const user = member.user;
|
|
124
|
+
const server = member.guild;
|
|
125
|
+
${emitStatements(node.body, " ", "member")}
|
|
126
|
+
});`;
|
|
127
|
+
case "ReactionAddHandler":
|
|
128
|
+
return `client.on("messageReactionAdd", async (reaction, user) => {
|
|
129
|
+
if (reaction.emoji.name !== ${emitExpression(node.emoji)}) return;
|
|
130
|
+
const message = reaction.message;
|
|
131
|
+
const channel = message.channel;
|
|
132
|
+
const server = message.guild;
|
|
133
|
+
${emitStatements(node.body, " ", "message")}
|
|
134
|
+
});`;
|
|
135
|
+
case "EveryTimerDecl":
|
|
136
|
+
return `setInterval(async () => {\n${emitStatements(node.body, " ", "message")}\n}, ${durationMs(node.amount.value, node.unit)});`;
|
|
137
|
+
case "DailyTimerDecl":
|
|
138
|
+
return `setInterval(async () => {
|
|
139
|
+
const now = new Date();
|
|
140
|
+
const hhmm = now.toTimeString().slice(0, 5);
|
|
141
|
+
if (hhmm !== ${emitExpression(node.time)}) return;
|
|
142
|
+
${emitStatements(node.body, " ", "message")}
|
|
143
|
+
}, 60000);`;
|
|
144
|
+
default:
|
|
145
|
+
return "";
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function emitStatements(statements: Statement[], indent: string, triggerName: string): string {
|
|
150
|
+
return statements.map((statement) => emitStatement(statement, indent, triggerName)).join("\n");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function emitStatement(statement: Statement, indent: string, triggerName: string): string {
|
|
154
|
+
switch (statement.type) {
|
|
155
|
+
case "ReplyStatement":
|
|
156
|
+
return `${indent}await ${triggerName}.reply(${emitExpression(statement.message)});`;
|
|
157
|
+
case "SayStatement": {
|
|
158
|
+
if (statement.channel) {
|
|
159
|
+
return `${indent}await findChannel(${triggerName === "guild" ? "server" : `server ?? ${triggerName}.guild`}, ${emitExpression(statement.channel)})?.send(${emitExpression(statement.message)});`;
|
|
160
|
+
}
|
|
161
|
+
return `${indent}await (${triggerName}.channel ?? channel)?.send(${emitExpression(statement.message)});`;
|
|
162
|
+
}
|
|
163
|
+
case "SayEmbedStatement":
|
|
164
|
+
if (triggerName === "member") {
|
|
165
|
+
return `${indent}await findChannel(server, "general")?.send({ embeds: [${emitEmbed(statement.embed)}] });`;
|
|
166
|
+
}
|
|
167
|
+
return `${indent}await (${triggerName}.channel ?? channel)?.send({ embeds: [${emitEmbed(statement.embed)}] });`;
|
|
168
|
+
case "LetDecl":
|
|
169
|
+
return `${indent}const ${statement.name} = ${emitExpression(statement.value)};`;
|
|
170
|
+
case "StoreStatement":
|
|
171
|
+
return `${indent}saveValue(${emitExpression(statement.namespace)}, ${JSON.stringify(statement.key)}, ${emitExpression(statement.value)});`;
|
|
172
|
+
case "IfStatement":
|
|
173
|
+
return `${indent}if (${emitExpression(statement.condition)}) {\n${emitStatements(statement.consequent, `${indent} `, triggerName)}\n${indent}}${statement.alternate.length ? ` else {\n${emitStatements(statement.alternate, `${indent} `, triggerName)}\n${indent}}` : ""}`;
|
|
174
|
+
case "ForEachStatement":
|
|
175
|
+
return `${indent}for (const ${statement.itemName} of (server?.members?.cache?.values?.() ?? [])) {\n${emitStatements(statement.body, `${indent} `, triggerName)}\n${indent}}`;
|
|
176
|
+
case "RequireRoleStatement":
|
|
177
|
+
return `${indent}if (!${triggerName}.member?.roles?.cache?.some((role) => role.name === ${emitExpression(statement.role)})) return;`;
|
|
178
|
+
case "GiveRoleStatement":
|
|
179
|
+
if (triggerName === "member") {
|
|
180
|
+
return `${indent}await ${triggerName}.roles?.add(findRole(server, ${emitExpression(statement.role)}));`;
|
|
181
|
+
}
|
|
182
|
+
return `${indent}await (${emitExpression(statement.subject)}?.roles ?? ${triggerName}.member?.roles)?.add(findRole(server ?? ${triggerName}.guild, ${emitExpression(statement.role)}));`;
|
|
183
|
+
case "RemoveRoleStatement":
|
|
184
|
+
return `${indent}await (${emitExpression(statement.subject)}?.roles ?? ${triggerName}.member?.roles)?.remove(findRole(server ?? ${triggerName}.guild, ${emitExpression(statement.role)}));`;
|
|
185
|
+
case "MuteStatement":
|
|
186
|
+
return `${indent}await ${emitExpression(statement.subject)}?.timeout?.(${statement.duration ? durationMs(statement.duration.amount.value, statement.duration.unit) : 600000});`;
|
|
187
|
+
case "KickStatement":
|
|
188
|
+
return `${indent}await ${emitExpression(statement.subject)}?.kick?.();`;
|
|
189
|
+
case "BanStatement":
|
|
190
|
+
return `${indent}await ${emitExpression(statement.subject)}?.ban?.();`;
|
|
191
|
+
case "TryCatchStatement":
|
|
192
|
+
return `${indent}try {\n${emitStatements(statement.body, `${indent} `, triggerName)}\n${indent}} catch (error) {\n${emitStatements(statement.errorHandler, `${indent} `, triggerName)}\n${indent}}`;
|
|
193
|
+
case "WaitStatement":
|
|
194
|
+
return `${indent}await new Promise((resolve) => setTimeout(resolve, ${durationMs(statement.duration.amount.value, statement.duration.unit)}));`;
|
|
195
|
+
case "ExpressionStatement":
|
|
196
|
+
return `${indent}${emitExpression(statement.expression)};`;
|
|
197
|
+
default:
|
|
198
|
+
return `${indent}// TODO: ${statement.type}`;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function emitEmbed(embed: { title?: Expression; description?: Expression; color?: { value: string }; fields: { name: Expression; value: Expression }[] }): string {
|
|
203
|
+
const lines = ["new EmbedBuilder()"];
|
|
204
|
+
if (embed.title) lines.push(`.setTitle(${emitExpression(embed.title)})`);
|
|
205
|
+
if (embed.description) lines.push(`.setDescription(${emitExpression(embed.description)})`);
|
|
206
|
+
if (embed.color) lines.push(`.setColor(${JSON.stringify(embed.color.value)})`);
|
|
207
|
+
for (const field of embed.fields) {
|
|
208
|
+
lines.push(`.addFields({ name: ${emitExpression(field.name)}, value: ${emitExpression(field.value)} })`);
|
|
209
|
+
}
|
|
210
|
+
return lines.join("");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function emitExpression(expression: Expression): string {
|
|
214
|
+
switch (expression.type) {
|
|
215
|
+
case "StringLiteral":
|
|
216
|
+
if (expression.interpolated) {
|
|
217
|
+
// Replace {expr} placeholders with properly emitted expressions
|
|
218
|
+
let result = expression.value.replace(/`/g, "\\`");
|
|
219
|
+
// Find all {expr} patterns and replace them
|
|
220
|
+
result = result.replace(/\{([^}]+)\}/g, (match, exprContent) => {
|
|
221
|
+
// Map user.name to user.username for Discord.js v14 compatibility
|
|
222
|
+
if (exprContent === "user.name") {
|
|
223
|
+
return "${user.username}";
|
|
224
|
+
}
|
|
225
|
+
if (exprContent === "member.name") {
|
|
226
|
+
return "${member.user.username}";
|
|
227
|
+
}
|
|
228
|
+
return "${" + exprContent + "}";
|
|
229
|
+
});
|
|
230
|
+
return "`" + result + "`";
|
|
231
|
+
}
|
|
232
|
+
return JSON.stringify(expression.value);
|
|
233
|
+
case "NumberLiteral":
|
|
234
|
+
return String(expression.value);
|
|
235
|
+
case "BooleanLiteral":
|
|
236
|
+
return String(expression.value);
|
|
237
|
+
case "ColorLiteral":
|
|
238
|
+
return JSON.stringify(expression.value);
|
|
239
|
+
case "IdentifierExpr":
|
|
240
|
+
return expression.name;
|
|
241
|
+
case "MemberExpr":
|
|
242
|
+
// Map user.name to user.username for Discord.js v14 compatibility
|
|
243
|
+
if (expression.path.join(".") === "user.name") {
|
|
244
|
+
return "user.username";
|
|
245
|
+
}
|
|
246
|
+
// Map member.name to member.user.username for Discord.js v14 compatibility
|
|
247
|
+
if (expression.path.join(".") === "member.name") {
|
|
248
|
+
return "member.user.username";
|
|
249
|
+
}
|
|
250
|
+
return expression.path.join(".");
|
|
251
|
+
case "ArgsIndexExpr":
|
|
252
|
+
return `args[${expression.index}]`;
|
|
253
|
+
case "LoadExpr":
|
|
254
|
+
return `loadValue(${emitExpression(expression.namespace)}, ${JSON.stringify(expression.key)}${expression.fallback ? `, ${emitExpression(expression.fallback)}` : ""})`;
|
|
255
|
+
case "FetchExpr":
|
|
256
|
+
return `(await axios.get(${emitExpression(expression.url)})).data`;
|
|
257
|
+
case "BinaryExpr":
|
|
258
|
+
if (expression.operator === "or") {
|
|
259
|
+
return `(${emitExpression(expression.left)} ?? ${emitExpression(expression.right)})`;
|
|
260
|
+
}
|
|
261
|
+
if (expression.operator === "has") {
|
|
262
|
+
return `(message.member?.roles?.cache?.some((role) => role.name === ${emitExpression(expression.right)}) ?? false)`;
|
|
263
|
+
}
|
|
264
|
+
return `(${emitExpression(expression.left)} ${expression.operator} ${emitExpression(expression.right)})`;
|
|
265
|
+
case "UnaryExpr":
|
|
266
|
+
return `(${expression.operator}${emitExpression(expression.argument)})`;
|
|
267
|
+
case "CallExpr":
|
|
268
|
+
return `${expression.callee}(${expression.args.map(emitExpression).join(", ")})`;
|
|
269
|
+
default:
|
|
270
|
+
return "undefined";
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function durationMs(amount: number, unit: string): number {
|
|
275
|
+
const multipliers: Record<string, number> = {
|
|
276
|
+
second: 1000,
|
|
277
|
+
seconds: 1000,
|
|
278
|
+
minute: 60000,
|
|
279
|
+
minutes: 60000,
|
|
280
|
+
hour: 3600000,
|
|
281
|
+
hours: 3600000,
|
|
282
|
+
day: 86400000,
|
|
283
|
+
days: 86400000
|
|
284
|
+
};
|
|
285
|
+
return amount * (multipliers[unit] ?? 1000);
|
|
286
|
+
}
|