serverless-ircd 0.7.0 → 0.8.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/.github/workflows/ci.yml +3 -3
- package/.gitmodules +1 -1
- package/CHANGELOG.md +273 -29
- package/README.md +155 -55
- package/apps/aws-stack/package.json +1 -1
- package/apps/aws-stack/src/aws-stack.ts +186 -18
- package/apps/aws-stack/tests/stack.test.ts +400 -56
- package/apps/cf-tcp-container/package.json +1 -1
- package/apps/cf-worker/package.json +1 -1
- package/apps/cf-worker/src/worker.ts +4 -4
- package/apps/cf-worker/tests/fixtures/web-dist/webclient/index.html +18 -0
- package/apps/cf-worker/tests/smoke.test.ts +5 -5
- package/apps/cf-worker/wrangler.test.toml +6 -6
- package/apps/cf-worker/wrangler.toml +7 -5
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +8 -0
- package/apps/local-cli/src/main.ts +16 -0
- package/apps/local-cli/src/server.ts +1 -0
- package/apps/local-cli/tests/config-loader.test.ts +14 -0
- package/apps/web/landing/index.html +14 -16
- package/apps/web/package.json +2 -2
- package/apps/web/scripts/build.mjs +23 -16
- package/apps/web/src/build-env.ts +1 -1
- package/apps/web/src/config-schema.ts +6 -6
- package/apps/web/tests/build-smoke.test.ts +14 -14
- package/apps/web/tests/config-schema.test.ts +1 -1
- package/docs/AWS-Deployment.md +21 -8
- package/docs/Cloudflare-Deployment-Guide.md +22 -6
- package/docs/PlanExtensions.md +113 -3
- package/docs/Release-Process.md +23 -13
- package/docs/Services.md +546 -0
- package/docs/WebClientGuide.md +32 -31
- package/package.json +2 -2
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/aws-runtime.ts +5 -0
- package/packages/aws-adapter/src/cdk-table-defs.ts +6 -0
- package/packages/aws-adapter/src/config-loader.ts +11 -0
- package/packages/aws-adapter/src/connection-counter.ts +89 -0
- package/packages/aws-adapter/src/dynamo-services-store.ts +649 -0
- package/packages/aws-adapter/src/handlers/connect.ts +55 -51
- package/packages/aws-adapter/src/handlers/default.ts +36 -4
- package/packages/aws-adapter/src/handlers/index.ts +15 -0
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +15 -0
- package/packages/aws-adapter/src/handlers/sweeper.ts +5 -1
- package/packages/aws-adapter/src/index.ts +4 -0
- package/packages/aws-adapter/src/stats.ts +6 -1
- package/packages/aws-adapter/src/tables.ts +34 -4
- package/packages/aws-adapter/tests/aws-harness.ts +3 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +8 -0
- package/packages/aws-adapter/tests/connect.test.ts +158 -32
- package/packages/aws-adapter/tests/connection-counter.test.ts +127 -0
- package/packages/aws-adapter/tests/dynamo-services-store-dynamo.test.ts +183 -0
- package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +568 -0
- package/packages/aws-adapter/tests/handlers.test.ts +105 -3
- package/packages/aws-adapter/tests/tables.test.ts +6 -1
- package/packages/cf-adapter/package.json +1 -1
- package/packages/cf-adapter/src/config-loader.ts +11 -0
- package/packages/cf-adapter/src/connection-do.ts +112 -2
- package/packages/cf-adapter/src/d1-services-store.ts +703 -0
- package/packages/cf-adapter/src/env.ts +8 -0
- package/packages/cf-adapter/src/index.ts +5 -0
- package/packages/cf-adapter/tests/config-loader.test.ts +19 -0
- package/packages/cf-adapter/tests/connection-do-nickserv-d1.test.ts +128 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +150 -2
- package/packages/cf-adapter/tests/d1-services-store.test.ts +582 -0
- package/packages/cf-adapter/tests/serialize.test.ts +1 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/scripts/generate-build-info.mjs +26 -5
- package/packages/irc-core/src/commands/account-auth.ts +172 -0
- package/packages/irc-core/src/commands/chanserv.ts +882 -0
- package/packages/irc-core/src/commands/hostserv.ts +487 -0
- package/packages/irc-core/src/commands/index.ts +12 -0
- package/packages/irc-core/src/commands/join.ts +164 -8
- package/packages/irc-core/src/commands/markread.ts +202 -0
- package/packages/irc-core/src/commands/memoserv.ts +319 -0
- package/packages/irc-core/src/commands/mode.ts +96 -4
- package/packages/irc-core/src/commands/nickserv.ts +390 -0
- package/packages/irc-core/src/commands/oper.ts +18 -1
- package/packages/irc-core/src/commands/operserv.ts +346 -0
- package/packages/irc-core/src/commands/pre-away.ts +3 -1
- package/packages/irc-core/src/commands/privmsg.ts +42 -0
- package/packages/irc-core/src/commands/read-marker.ts +8 -8
- package/packages/irc-core/src/commands/registration.ts +61 -6
- package/packages/irc-core/src/commands/sasl.ts +18 -49
- package/packages/irc-core/src/commands/tagmsg.ts +41 -6
- package/packages/irc-core/src/commands/topic.ts +37 -0
- package/packages/irc-core/src/config.ts +36 -5
- package/packages/irc-core/src/effects.ts +56 -1
- package/packages/irc-core/src/ports.ts +1653 -84
- package/packages/irc-core/src/protocol/numerics.ts +8 -0
- package/packages/irc-core/src/state/channel.ts +21 -1
- package/packages/irc-core/src/state/connection.ts +25 -1
- package/packages/irc-core/src/types.ts +48 -12
- package/packages/irc-core/tests/commands/chanserv.test.ts +1668 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +6 -0
- package/packages/irc-core/tests/commands/hostserv.test.ts +935 -0
- package/packages/irc-core/tests/commands/join.test.ts +393 -1
- package/packages/irc-core/tests/commands/markread.test.ts +361 -0
- package/packages/irc-core/tests/commands/memoserv.test.ts +654 -0
- package/packages/irc-core/tests/commands/mode.test.ts +381 -2
- package/packages/irc-core/tests/commands/nickserv.test.ts +807 -0
- package/packages/irc-core/tests/commands/oper.test.ts +13 -0
- package/packages/irc-core/tests/commands/operserv.test.ts +656 -0
- package/packages/irc-core/tests/commands/privmsg.test.ts +147 -0
- package/packages/irc-core/tests/commands/read-marker.test.ts +28 -28
- package/packages/irc-core/tests/commands/registration.test.ts +788 -14
- package/packages/irc-core/tests/commands/sasl.test.ts +185 -12
- package/packages/irc-core/tests/commands/server-info.test.ts +9 -5
- package/packages/irc-core/tests/commands/tagmsg.test.ts +73 -33
- package/packages/irc-core/tests/commands/topic.test.ts +94 -2
- package/packages/irc-core/tests/commands/unified-account.test.ts +416 -0
- package/packages/irc-core/tests/config.test.ts +49 -5
- package/packages/irc-core/tests/effects.test.ts +19 -0
- package/packages/irc-core/tests/message-store.test.ts +63 -0
- package/packages/irc-core/tests/persistent-services-store.test.ts +582 -0
- package/packages/irc-core/tests/services-store.test.ts +1289 -0
- package/packages/irc-core/tests/state/channel.test.ts +3 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +71 -16
- package/packages/irc-server/src/dispatch.ts +94 -7
- package/packages/irc-server/src/routing.ts +19 -0
- package/packages/irc-server/tests/actor.test.ts +623 -12
- package/packages/irc-server/tests/dispatch.test.ts +270 -2
- package/packages/irc-server/tests/routing.test.ts +6 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/in-memory-harness.ts +29 -3
- package/packages/irc-test-support/src/index.ts +1 -0
- package/packages/irc-test-support/tests/in-memory-harness.test.ts +32 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/load-test/package.json +1 -1
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +2 -2
- package/apps/cf-worker/tests/fixtures/web-dist/app/index.html +0 -18
- package/packages/irc-core/tests/read-marker-store.test.ts +0 -108
|
@@ -2,7 +2,13 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
import { channelModeReducer, userModeReducer } from '../../src/commands/mode';
|
|
3
3
|
import { Effect } from '../../src/effects';
|
|
4
4
|
import type { Effect as EffectType, RawLine } from '../../src/effects';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
EmptyMotdProvider,
|
|
7
|
+
FakeClock,
|
|
8
|
+
InMemoryServicesStore,
|
|
9
|
+
SequentialIdFactory,
|
|
10
|
+
type ServicesStore,
|
|
11
|
+
} from '../../src/ports';
|
|
6
12
|
import { type ChannelState, createChannel } from '../../src/state/channel';
|
|
7
13
|
import { type ConnectionState, createConnection } from '../../src/state/connection';
|
|
8
14
|
import { type Ctx, type ServerConfig, buildCtx } from '../../src/types';
|
|
@@ -19,13 +25,18 @@ const serverConfig: ServerConfig = {
|
|
|
19
25
|
quitMessage: 'Client Quit',
|
|
20
26
|
};
|
|
21
27
|
|
|
22
|
-
function makeCtx(
|
|
28
|
+
function makeCtx(
|
|
29
|
+
conn: ConnectionState,
|
|
30
|
+
clock = new FakeClock(1_000),
|
|
31
|
+
services?: ServicesStore,
|
|
32
|
+
): Ctx {
|
|
23
33
|
return buildCtx({
|
|
24
34
|
serverConfig,
|
|
25
35
|
clock,
|
|
26
36
|
ids: new SequentialIdFactory(),
|
|
27
37
|
motd: EmptyMotdProvider,
|
|
28
38
|
connection: conn,
|
|
39
|
+
...(services !== undefined ? { services } : {}),
|
|
29
40
|
});
|
|
30
41
|
}
|
|
31
42
|
|
|
@@ -1175,3 +1186,371 @@ describe('userModeReducer — read-only mode `S` (TLS connected)', () => {
|
|
|
1175
1186
|
expect(texts).toContainEqual(L(':irc.example.com 501 alice :Unknown MODE flag'));
|
|
1176
1187
|
});
|
|
1177
1188
|
});
|
|
1189
|
+
|
|
1190
|
+
// ============================================================================
|
|
1191
|
+
// userModeReducer — read-only mode `r` (registered / identified via services)
|
|
1192
|
+
// ============================================================================
|
|
1193
|
+
|
|
1194
|
+
describe('userModeReducer — read-only mode `r` (registered)', () => {
|
|
1195
|
+
it('emits 221 with +r when the connection has the registered user mode set', () => {
|
|
1196
|
+
const conn = makeConn();
|
|
1197
|
+
conn.userModes.registered = true;
|
|
1198
|
+
const ctx = makeCtx(conn);
|
|
1199
|
+
|
|
1200
|
+
const out = userModeReducer(conn, { command: 'MODE', params: ['alice'], tags: {} }, ctx);
|
|
1201
|
+
|
|
1202
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
1203
|
+
Effect.send('c1', [L(':irc.example.com 221 alice +r')]),
|
|
1204
|
+
]);
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
it('emits 221 with +ir when invisible and registered are both set', () => {
|
|
1208
|
+
const conn = makeConn();
|
|
1209
|
+
conn.userModes.invisible = true;
|
|
1210
|
+
conn.userModes.registered = true;
|
|
1211
|
+
const ctx = makeCtx(conn);
|
|
1212
|
+
|
|
1213
|
+
const out = userModeReducer(conn, { command: 'MODE', params: ['alice'], tags: {} }, ctx);
|
|
1214
|
+
|
|
1215
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
1216
|
+
Effect.send('c1', [L(':irc.example.com 221 alice +ir')]),
|
|
1217
|
+
]);
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
it('emits 221 with +rS when both registered and tls are set', () => {
|
|
1221
|
+
const conn = makeConn();
|
|
1222
|
+
conn.userModes.registered = true;
|
|
1223
|
+
conn.userModes.tls = true;
|
|
1224
|
+
const ctx = makeCtx(conn);
|
|
1225
|
+
|
|
1226
|
+
const out = userModeReducer(conn, { command: 'MODE', params: ['alice'], tags: {} }, ctx);
|
|
1227
|
+
|
|
1228
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
1229
|
+
Effect.send('c1', [L(':irc.example.com 221 alice +rS')]),
|
|
1230
|
+
]);
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
it('rejects MODE nick +r with 501 ERR_UMODEUNKNOWNFLAG (read-only, services-only)', () => {
|
|
1234
|
+
const conn = makeConn();
|
|
1235
|
+
const ctx = makeCtx(conn);
|
|
1236
|
+
|
|
1237
|
+
const out = userModeReducer(conn, { command: 'MODE', params: ['alice', '+r'], tags: {} }, ctx);
|
|
1238
|
+
|
|
1239
|
+
expect(conn.userModes.registered).toBe(false);
|
|
1240
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
1241
|
+
Effect.send('c1', [L(':irc.example.com 501 alice :Unknown MODE flag')]),
|
|
1242
|
+
]);
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
it('rejects MODE nick -r with 501 even when registered is set', () => {
|
|
1246
|
+
const conn = makeConn();
|
|
1247
|
+
conn.userModes.registered = true;
|
|
1248
|
+
const ctx = makeCtx(conn);
|
|
1249
|
+
|
|
1250
|
+
const out = userModeReducer(conn, { command: 'MODE', params: ['alice', '-r'], tags: {} }, ctx);
|
|
1251
|
+
|
|
1252
|
+
expect(conn.userModes.registered).toBe(true);
|
|
1253
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
1254
|
+
Effect.send('c1', [L(':irc.example.com 501 alice :Unknown MODE flag')]),
|
|
1255
|
+
]);
|
|
1256
|
+
});
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
// ============================================================================
|
|
1260
|
+
// channelModeReducer — services-only modes r/R/M (reject via MODE)
|
|
1261
|
+
// ============================================================================
|
|
1262
|
+
|
|
1263
|
+
describe('channelModeReducer — services-only channel modes (+r/+R/+M)', () => {
|
|
1264
|
+
it('rejects MODE #chan +r with 472 (ChanServ-only)', () => {
|
|
1265
|
+
const chan = makeChan('#foo');
|
|
1266
|
+
addMember(chan, 'c1', 'alice', true);
|
|
1267
|
+
const conn = makeConn();
|
|
1268
|
+
const ctx = makeCtx(conn);
|
|
1269
|
+
|
|
1270
|
+
const out = channelModeReducer(
|
|
1271
|
+
chan,
|
|
1272
|
+
{ command: 'MODE', params: ['#foo', '+r'], tags: {} },
|
|
1273
|
+
ctx,
|
|
1274
|
+
);
|
|
1275
|
+
|
|
1276
|
+
expect(chan.modes.registered).toBe(false);
|
|
1277
|
+
const texts = out.effects.flatMap((e) => (e.tag === 'Send' ? e.lines.map((l) => l.text) : []));
|
|
1278
|
+
expect(texts).toContain(':irc.example.com 472 alice r :is unknown mode char to me');
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
it('rejects MODE #chan -r with 472 (ChanServ-only)', () => {
|
|
1282
|
+
const chan = makeChan('#foo');
|
|
1283
|
+
chan.modes.registered = true;
|
|
1284
|
+
addMember(chan, 'c1', 'alice', true);
|
|
1285
|
+
const conn = makeConn();
|
|
1286
|
+
const ctx = makeCtx(conn);
|
|
1287
|
+
|
|
1288
|
+
const out = channelModeReducer(
|
|
1289
|
+
chan,
|
|
1290
|
+
{ command: 'MODE', params: ['#foo', '-r'], tags: {} },
|
|
1291
|
+
ctx,
|
|
1292
|
+
);
|
|
1293
|
+
|
|
1294
|
+
expect(chan.modes.registered).toBe(true);
|
|
1295
|
+
const texts = out.effects.flatMap((e) => (e.tag === 'Send' ? e.lines.map((l) => l.text) : []));
|
|
1296
|
+
expect(texts).toContain(':irc.example.com 472 alice r :is unknown mode char to me');
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
it('rejects MODE #chan +R with 472 (ChanServ-only)', () => {
|
|
1300
|
+
const chan = makeChan('#foo');
|
|
1301
|
+
addMember(chan, 'c1', 'alice', true);
|
|
1302
|
+
const conn = makeConn();
|
|
1303
|
+
const ctx = makeCtx(conn);
|
|
1304
|
+
|
|
1305
|
+
channelModeReducer(chan, { command: 'MODE', params: ['#foo', '+R'], tags: {} }, ctx);
|
|
1306
|
+
|
|
1307
|
+
expect(chan.modes.blockUnidentified).toBe(false);
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
it('rejects MODE #chan +M with 472 (ChanServ-only)', () => {
|
|
1311
|
+
const chan = makeChan('#foo');
|
|
1312
|
+
addMember(chan, 'c1', 'alice', true);
|
|
1313
|
+
const conn = makeConn();
|
|
1314
|
+
const ctx = makeCtx(conn);
|
|
1315
|
+
|
|
1316
|
+
channelModeReducer(chan, { command: 'MODE', params: ['#foo', '+M'], tags: {} }, ctx);
|
|
1317
|
+
|
|
1318
|
+
expect(chan.modes.moderatedIdent).toBe(false);
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
it('surfaces +r/+R/+M in the 324 read reply when ChanServ set them', () => {
|
|
1322
|
+
const chan = makeChan('#foo');
|
|
1323
|
+
chan.modes.registered = true;
|
|
1324
|
+
chan.modes.blockUnidentified = true;
|
|
1325
|
+
chan.modes.moderatedIdent = true;
|
|
1326
|
+
addMember(chan, 'c1', 'alice');
|
|
1327
|
+
const conn = makeConn();
|
|
1328
|
+
const ctx = makeCtx(conn);
|
|
1329
|
+
|
|
1330
|
+
const out = channelModeReducer(chan, { command: 'MODE', params: ['#foo'], tags: {} }, ctx);
|
|
1331
|
+
|
|
1332
|
+
const texts = out.effects.flatMap((e) => (e.tag === 'Send' ? e.lines.map((l) => l.text) : []));
|
|
1333
|
+
// Modes appear in canonical order: i t n m s p r R M
|
|
1334
|
+
expect(texts).toContain(':irc.example.com 324 alice #foo +rRM');
|
|
1335
|
+
});
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
// ============================================================================
|
|
1339
|
+
// channelModeReducer — ChanServ MLOCK reapplication
|
|
1340
|
+
// ============================================================================
|
|
1341
|
+
|
|
1342
|
+
describe('channelModeReducer — MLOCK reapplication', () => {
|
|
1343
|
+
it('reapplies a locked boolean mode an op tried to unset (+n)', () => {
|
|
1344
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1345
|
+
services.registerNick('alice', 'pw', 'a@e');
|
|
1346
|
+
services.registerChannel('#foo', 'alice');
|
|
1347
|
+
services.setChannelMlock('#foo', '+n');
|
|
1348
|
+
const chan = makeChan('#foo');
|
|
1349
|
+
chan.modes.noExternal = true;
|
|
1350
|
+
addMember(chan, 'c1', 'alice', true);
|
|
1351
|
+
const conn = makeConn();
|
|
1352
|
+
const ctx = makeCtx(conn, new FakeClock(1_000), services);
|
|
1353
|
+
|
|
1354
|
+
const out = channelModeReducer(
|
|
1355
|
+
chan,
|
|
1356
|
+
{ command: 'MODE', params: ['#foo', '-n'], tags: {} },
|
|
1357
|
+
ctx,
|
|
1358
|
+
);
|
|
1359
|
+
|
|
1360
|
+
// The op's -n applies, but ChanServ reapplies +n afterwards.
|
|
1361
|
+
expect(chan.modes.noExternal).toBe(true);
|
|
1362
|
+
// The op's -n broadcast goes out, then ChanServ's +n reapply broadcast.
|
|
1363
|
+
const broadcasts = out.effects.filter((e) => e.tag === 'Broadcast');
|
|
1364
|
+
expect(broadcasts.length).toBe(2);
|
|
1365
|
+
const applyDeltas = out.effects.filter((e) => e.tag === 'ApplyChannelDelta');
|
|
1366
|
+
// The second ApplyChannelDelta carries the +n reapply.
|
|
1367
|
+
const reapply = applyDeltas[1];
|
|
1368
|
+
if (reapply?.tag === 'ApplyChannelDelta') {
|
|
1369
|
+
expect(reapply.delta.modeChanges).toContainEqual({ mode: 'noExternal', set: true });
|
|
1370
|
+
} else {
|
|
1371
|
+
expect.fail('expected a second ApplyChannelDelta for the MLOCK reapply');
|
|
1372
|
+
}
|
|
1373
|
+
});
|
|
1374
|
+
|
|
1375
|
+
it('does not reapply modes that are not locked', () => {
|
|
1376
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1377
|
+
services.registerNick('alice', 'pw', 'a@e');
|
|
1378
|
+
services.registerChannel('#foo', 'alice');
|
|
1379
|
+
services.setChannelMlock('#foo', '+n');
|
|
1380
|
+
const chan = makeChan('#foo');
|
|
1381
|
+
chan.modes.noExternal = true;
|
|
1382
|
+
chan.modes.topicLock = true;
|
|
1383
|
+
addMember(chan, 'c1', 'alice', true);
|
|
1384
|
+
const conn = makeConn();
|
|
1385
|
+
const ctx = makeCtx(conn, new FakeClock(1_000), services);
|
|
1386
|
+
|
|
1387
|
+
const out = channelModeReducer(
|
|
1388
|
+
chan,
|
|
1389
|
+
{ command: 'MODE', params: ['#foo', '-t'], tags: {} },
|
|
1390
|
+
ctx,
|
|
1391
|
+
);
|
|
1392
|
+
|
|
1393
|
+
// -t is not in MLOCK, so no reapply.
|
|
1394
|
+
expect(chan.modes.topicLock).toBe(false);
|
|
1395
|
+
expect(out.effects.filter((e) => e.tag === 'Broadcast')).toHaveLength(1);
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
it('does not reapply when no services store is bound', () => {
|
|
1399
|
+
const chan = makeChan('#foo');
|
|
1400
|
+
chan.modes.noExternal = true;
|
|
1401
|
+
addMember(chan, 'c1', 'alice', true);
|
|
1402
|
+
const conn = makeConn();
|
|
1403
|
+
const ctx = makeCtx(conn);
|
|
1404
|
+
|
|
1405
|
+
const out = channelModeReducer(
|
|
1406
|
+
chan,
|
|
1407
|
+
{ command: 'MODE', params: ['#foo', '-n'], tags: {} },
|
|
1408
|
+
ctx,
|
|
1409
|
+
);
|
|
1410
|
+
|
|
1411
|
+
expect(chan.modes.noExternal).toBe(false);
|
|
1412
|
+
expect(out.effects.filter((e) => e.tag === 'Broadcast')).toHaveLength(1);
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
it('does not reapply when the channel is not registered', () => {
|
|
1416
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1417
|
+
const chan = makeChan('#foo');
|
|
1418
|
+
chan.modes.noExternal = true;
|
|
1419
|
+
addMember(chan, 'c1', 'alice', true);
|
|
1420
|
+
const conn = makeConn();
|
|
1421
|
+
const ctx = makeCtx(conn, new FakeClock(1_000), services);
|
|
1422
|
+
|
|
1423
|
+
const out = channelModeReducer(
|
|
1424
|
+
chan,
|
|
1425
|
+
{ command: 'MODE', params: ['#foo', '-n'], tags: {} },
|
|
1426
|
+
ctx,
|
|
1427
|
+
);
|
|
1428
|
+
|
|
1429
|
+
expect(chan.modes.noExternal).toBe(false);
|
|
1430
|
+
expect(out.effects.filter((e) => e.tag === 'Broadcast')).toHaveLength(1);
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
it('does not reapply when MLOCK is empty', () => {
|
|
1434
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1435
|
+
services.registerNick('alice', 'pw', 'a@e');
|
|
1436
|
+
services.registerChannel('#foo', 'alice');
|
|
1437
|
+
// MLOCK is empty by default.
|
|
1438
|
+
const chan = makeChan('#foo');
|
|
1439
|
+
chan.modes.noExternal = true;
|
|
1440
|
+
addMember(chan, 'c1', 'alice', true);
|
|
1441
|
+
const conn = makeConn();
|
|
1442
|
+
const ctx = makeCtx(conn, new FakeClock(1_000), services);
|
|
1443
|
+
|
|
1444
|
+
const out = channelModeReducer(
|
|
1445
|
+
chan,
|
|
1446
|
+
{ command: 'MODE', params: ['#foo', '-n'], tags: {} },
|
|
1447
|
+
ctx,
|
|
1448
|
+
);
|
|
1449
|
+
|
|
1450
|
+
expect(chan.modes.noExternal).toBe(false);
|
|
1451
|
+
expect(out.effects.filter((e) => e.tag === 'Broadcast')).toHaveLength(1);
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1454
|
+
it('reapplies multiple locked modes unset in a single MODE', () => {
|
|
1455
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1456
|
+
services.registerNick('alice', 'pw', 'a@e');
|
|
1457
|
+
services.registerChannel('#foo', 'alice');
|
|
1458
|
+
services.setChannelMlock('#foo', '+nt');
|
|
1459
|
+
const chan = makeChan('#foo');
|
|
1460
|
+
chan.modes.noExternal = true;
|
|
1461
|
+
chan.modes.topicLock = true;
|
|
1462
|
+
addMember(chan, 'c1', 'alice', true);
|
|
1463
|
+
const conn = makeConn();
|
|
1464
|
+
const ctx = makeCtx(conn, new FakeClock(1_000), services);
|
|
1465
|
+
|
|
1466
|
+
const out = channelModeReducer(
|
|
1467
|
+
chan,
|
|
1468
|
+
{ command: 'MODE', params: ['#foo', '-tn'], tags: {} },
|
|
1469
|
+
ctx,
|
|
1470
|
+
);
|
|
1471
|
+
|
|
1472
|
+
expect(chan.modes.noExternal).toBe(true);
|
|
1473
|
+
expect(chan.modes.topicLock).toBe(true);
|
|
1474
|
+
// Op broadcast + ChanServ reapply broadcast.
|
|
1475
|
+
expect(out.effects.filter((e) => e.tag === 'Broadcast')).toHaveLength(2);
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
it('MLOCK reapplies services-only modes (+r) that an op somehow tried to unset', () => {
|
|
1479
|
+
// Even though MODE rejects +r/-r at the parser, MLOCK should defend the
|
|
1480
|
+
// registered mode if it ever appears in a MODE delta (defense in depth).
|
|
1481
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1482
|
+
services.registerNick('alice', 'pw', 'a@e');
|
|
1483
|
+
services.registerChannel('#foo', 'alice');
|
|
1484
|
+
services.setChannelMlock('#foo', '+r');
|
|
1485
|
+
const chan = makeChan('#foo');
|
|
1486
|
+
chan.modes.registered = true;
|
|
1487
|
+
addMember(chan, 'c1', 'alice', true);
|
|
1488
|
+
const conn = makeConn();
|
|
1489
|
+
const ctx = makeCtx(conn, new FakeClock(1_000), services);
|
|
1490
|
+
|
|
1491
|
+
// The op's MODE -r will reject at the parser; no broadcast goes out.
|
|
1492
|
+
// The MLOCK reapply path is exercised against any other applied ops in
|
|
1493
|
+
// the same modestring — here combined with -t (which IS unset).
|
|
1494
|
+
chan.modes.topicLock = true;
|
|
1495
|
+
const out = channelModeReducer(
|
|
1496
|
+
chan,
|
|
1497
|
+
{ command: 'MODE', params: ['#foo', '-t'], tags: {} },
|
|
1498
|
+
ctx,
|
|
1499
|
+
);
|
|
1500
|
+
|
|
1501
|
+
// -t applies, +r stays (no -r was applied), no MLOCK breach on r → no
|
|
1502
|
+
// reapply of r. Verified by the absence of a second broadcast.
|
|
1503
|
+
expect(chan.modes.topicLock).toBe(false);
|
|
1504
|
+
expect(chan.modes.registered).toBe(true);
|
|
1505
|
+
expect(out.effects.filter((e) => e.tag === 'Broadcast')).toHaveLength(1);
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
it('does not reapply when MLOCK has no positive letters (e.g. `-n` only)', () => {
|
|
1509
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1510
|
+
services.registerNick('alice', 'pw', 'a@e');
|
|
1511
|
+
services.registerChannel('#foo', 'alice');
|
|
1512
|
+
// MLOCK with only a negative section.
|
|
1513
|
+
services.setChannelMlock('#foo', '-n');
|
|
1514
|
+
const chan = makeChan('#foo');
|
|
1515
|
+
chan.modes.noExternal = true;
|
|
1516
|
+
addMember(chan, 'c1', 'alice', true);
|
|
1517
|
+
const conn = makeConn();
|
|
1518
|
+
const ctx = makeCtx(conn, new FakeClock(1_000), services);
|
|
1519
|
+
|
|
1520
|
+
const out = channelModeReducer(
|
|
1521
|
+
chan,
|
|
1522
|
+
{ command: 'MODE', params: ['#foo', '-n'], tags: {} },
|
|
1523
|
+
ctx,
|
|
1524
|
+
);
|
|
1525
|
+
|
|
1526
|
+
expect(chan.modes.noExternal).toBe(false);
|
|
1527
|
+
expect(out.effects.filter((e) => e.tag === 'Broadcast')).toHaveLength(1);
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
it('skips MLOCK letters that are not boolean modes (e.g. k/l/o/v/b)', () => {
|
|
1531
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1532
|
+
services.registerNick('alice', 'pw', 'a@e');
|
|
1533
|
+
services.registerChannel('#foo', 'alice');
|
|
1534
|
+
// MLOCK carries o/v/b/k/l letters; the reapply loop must skip them
|
|
1535
|
+
// without throwing. We pair with a +n lock so we can still observe one
|
|
1536
|
+
// reapply, which doubles as proof the loop ran.
|
|
1537
|
+
services.setChannelMlock('#foo', '+novbkl');
|
|
1538
|
+
const chan = makeChan('#foo');
|
|
1539
|
+
chan.modes.noExternal = true;
|
|
1540
|
+
addMember(chan, 'c1', 'alice', true);
|
|
1541
|
+
const conn = makeConn();
|
|
1542
|
+
const ctx = makeCtx(conn, new FakeClock(1_000), services);
|
|
1543
|
+
|
|
1544
|
+
const out = channelModeReducer(
|
|
1545
|
+
chan,
|
|
1546
|
+
{ command: 'MODE', params: ['#foo', '-n'], tags: {} },
|
|
1547
|
+
ctx,
|
|
1548
|
+
);
|
|
1549
|
+
|
|
1550
|
+
// The op unset +n; ChanServ reapplies it. The other letters are
|
|
1551
|
+
// skipped silently (they are not boolean modes the reapply path
|
|
1552
|
+
// toggles).
|
|
1553
|
+
expect(chan.modes.noExternal).toBe(true);
|
|
1554
|
+
expect(out.effects.filter((e) => e.tag === 'Broadcast')).toHaveLength(2);
|
|
1555
|
+
});
|
|
1556
|
+
});
|