@takaro/modules 0.0.1 → 0.0.5

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.
Files changed (99) hide show
  1. package/dist/BuiltinModule.js +0 -4
  2. package/dist/BuiltinModule.js.map +1 -1
  3. package/dist/community-modules.json +21 -0
  4. package/dist/dto/gameEvents.d.ts +3 -2
  5. package/dist/dto/gameEvents.js +1 -0
  6. package/dist/dto/gameEvents.js.map +1 -1
  7. package/dist/dto/index.d.ts +6 -0
  8. package/dist/dto/takaroEvents.d.ts +46 -0
  9. package/dist/dto/takaroEvents.js +129 -1
  10. package/dist/dto/takaroEvents.js.map +1 -1
  11. package/dist/main.js +1 -1
  12. package/dist/main.js.map +1 -1
  13. package/dist/modules/chatBridge/hooks/GameToDiscord.js +1 -1
  14. package/dist/modules/chatBridge/hooks/GameToDiscord.js.map +1 -1
  15. package/dist/modules/chatBridge/hooks/PlayerConnected.js +2 -2
  16. package/dist/modules/chatBridge/hooks/PlayerConnected.js.map +1 -1
  17. package/dist/modules/chatBridge/hooks/PlayerDisconnected.js +1 -1
  18. package/dist/modules/chatBridge/hooks/PlayerDisconnected.js.map +1 -1
  19. package/dist/modules/economyUtils/commands/transfer.js +1 -1
  20. package/dist/modules/economyUtils/commands/transfer.js.map +1 -1
  21. package/dist/modules/geoBlock/hooks/IPDetected.js +3 -5
  22. package/dist/modules/geoBlock/hooks/IPDetected.js.map +1 -1
  23. package/dist/modules/geoBlock/index.js +1 -1
  24. package/dist/modules/geoBlock/index.js.map +1 -1
  25. package/dist/modules/gimme/commands/gimme.js +1 -0
  26. package/dist/modules/gimme/commands/gimme.js.map +1 -1
  27. package/dist/modules/lottery/commands/nextDraw.js +1 -1
  28. package/dist/modules/lottery/commands/nextDraw.js.map +1 -1
  29. package/dist/modules/playerOnboarding/index.js.map +1 -1
  30. package/dist/modules/teleports/commands/deletetp.js +1 -1
  31. package/dist/modules/teleports/commands/deletetp.js.map +1 -1
  32. package/dist/modules/teleports/commands/setprivate.js +4 -6
  33. package/dist/modules/teleports/commands/setprivate.js.map +1 -1
  34. package/dist/modules/teleports/commands/setpublic.js +8 -15
  35. package/dist/modules/teleports/commands/setpublic.js.map +1 -1
  36. package/dist/modules/teleports/commands/settp.js +2 -2
  37. package/dist/modules/teleports/commands/settp.js.map +1 -1
  38. package/dist/modules/teleports/commands/teleport.js +3 -8
  39. package/dist/modules/teleports/commands/teleport.js.map +1 -1
  40. package/dist/modules/teleports/commands/teleportwaypoint.js +9 -1
  41. package/dist/modules/teleports/commands/teleportwaypoint.js.map +1 -1
  42. package/dist/modules/teleports/commands/tplist.js +13 -12
  43. package/dist/modules/teleports/commands/tplist.js.map +1 -1
  44. package/dist/modules/teleports/functions/utils.d.ts +2 -2
  45. package/dist/modules/teleports/functions/utils.js +21 -3
  46. package/dist/modules/teleports/functions/utils.js.map +1 -1
  47. package/dist/modules.json +99 -79
  48. package/package.json +4 -8
  49. package/scripts/buildBuiltinJson.ts +20 -0
  50. package/src/BuiltinModule.ts +1 -1
  51. package/src/__tests__/aliases.integration.test.ts +6 -7
  52. package/src/__tests__/bugRepros.integration.test.ts +72 -0
  53. package/src/__tests__/commandArgs.integration.test.ts +24 -25
  54. package/src/__tests__/economyUtils.integration.test.ts +66 -60
  55. package/src/__tests__/geoblock.integration.test.ts +22 -65
  56. package/src/__tests__/gimme.integration.test.ts +35 -36
  57. package/src/__tests__/help.integration.test.ts +47 -35
  58. package/src/__tests__/lottery.integration.test.ts +32 -28
  59. package/src/__tests__/modulePermission.integration.test.ts +24 -18
  60. package/src/__tests__/onboarding.integration.test.ts +13 -14
  61. package/src/__tests__/ping.integration.test.ts +5 -6
  62. package/src/__tests__/roleExpiry.integration.test.ts +9 -10
  63. package/src/__tests__/serverMessages.integration.test.ts +14 -12
  64. package/src/__tests__/systemConfigCost.integration.test.ts +14 -15
  65. package/src/__tests__/teleports/listtp.integration.test.ts +98 -22
  66. package/src/__tests__/teleports/publicteleports.integration.test.ts +55 -48
  67. package/src/__tests__/teleports/teleport.integration.test.ts +17 -16
  68. package/src/__tests__/teleports/tpManagement.integration.test.ts +26 -27
  69. package/src/__tests__/teleports/waypoints.integration.test.ts +113 -82
  70. package/src/community-modules/README.md +5 -0
  71. package/src/community-modules/modules/vote.json +19 -0
  72. package/src/dto/gameEvents.ts +2 -2
  73. package/src/dto/takaroEvents.ts +79 -0
  74. package/src/main.ts +2 -3
  75. package/src/modules/chatBridge/hooks/GameToDiscord.js +1 -1
  76. package/src/modules/chatBridge/hooks/PlayerConnected.js +2 -2
  77. package/src/modules/chatBridge/hooks/PlayerDisconnected.js +1 -1
  78. package/src/modules/chatBridge/index.ts +1 -1
  79. package/src/modules/economyUtils/commands/transfer.js +4 -4
  80. package/src/modules/economyUtils/index.ts +1 -1
  81. package/src/modules/geoBlock/hooks/IPDetected.js +3 -4
  82. package/src/modules/geoBlock/index.ts +2 -2
  83. package/src/modules/gimme/commands/gimme.js +2 -1
  84. package/src/modules/gimme/index.ts +1 -1
  85. package/src/modules/highPingKicker/index.ts +1 -1
  86. package/src/modules/lottery/commands/nextDraw.js +3 -1
  87. package/src/modules/lottery/index.ts +1 -1
  88. package/src/modules/playerOnboarding/index.ts +2 -1
  89. package/src/modules/serverMessages/index.ts +1 -1
  90. package/src/modules/teleports/commands/deletetp.js +1 -1
  91. package/src/modules/teleports/commands/setprivate.js +6 -6
  92. package/src/modules/teleports/commands/setpublic.js +18 -25
  93. package/src/modules/teleports/commands/settp.js +2 -2
  94. package/src/modules/teleports/commands/teleport.js +3 -10
  95. package/src/modules/teleports/commands/teleportwaypoint.js +12 -1
  96. package/src/modules/teleports/commands/tplist.js +16 -15
  97. package/src/modules/teleports/functions/utils.js +19 -3
  98. package/src/modules/teleports/index.ts +1 -1
  99. package/src/modules/utils/index.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takaro/modules",
3
- "version": "0.0.1",
3
+ "version": "0.0.5",
4
4
  "description": "Built-in modules for Takaro",
5
5
  "main": "dist/main.js",
6
6
  "types": "dist/main.d.ts",
@@ -17,10 +17,6 @@
17
17
  "keywords": [],
18
18
  "author": "",
19
19
  "license": "ISC",
20
- "dependencies": {
21
- "@takaro/apiclient": "^0.0.1"
22
- },
23
- "devDependencies": {
24
- "@takaro/test": "0.0.1"
25
- }
26
- }
20
+ "dependencies": {},
21
+ "devDependencies": {}
22
+ }
@@ -1,13 +1,33 @@
1
1
  import 'reflect-metadata';
2
2
  import { getModules } from '@takaro/modules';
3
3
  import { writeFile } from 'fs/promises';
4
+ import { readdir, readFile } from 'node:fs/promises';
5
+ import path from 'path';
6
+
7
+ const __dirname = path.dirname(new URL(import.meta.url).pathname);
4
8
 
5
9
  async function main() {
10
+ // Built in modules
11
+ // TODO: we should probably 'export' them in CI and save it as JSON so it's consistent with the community modules
6
12
  const modules = await getModules();
7
13
  const modulesJson = JSON.stringify(modules, null, 2);
8
14
  await writeFile('dist/modules.json', modulesJson);
9
15
  await writeFile('../web-docs/pages/modules.json', modulesJson);
10
16
  await writeFile('../e2e/src/web-main/fixtures/modules.json', modulesJson);
17
+
18
+ // Community modules
19
+ const files = await readdir(`${__dirname}/../src/community-modules/modules`);
20
+ const communityModules: Array<string> = [];
21
+
22
+ for (const file of files) {
23
+ const content = await readFile(`${__dirname}/../src/community-modules/modules/${file}`, 'utf-8');
24
+ communityModules.push(JSON.parse(content));
25
+ }
26
+
27
+ const communityModulesJson = JSON.stringify(communityModules, null, 2);
28
+ await writeFile('dist/community-modules.json', communityModulesJson);
29
+ await writeFile('../web-docs/pages/community-modules.json', communityModulesJson);
30
+ await writeFile('../e2e/src/web-main/fixtures/community-modules.json', communityModulesJson);
11
31
  }
12
32
 
13
33
  // eslint-disable-next-line no-console
@@ -74,7 +74,7 @@ export class IPermission extends TakaroDTO<IPermission> {
74
74
  friendlyName: string;
75
75
  @IsOptional()
76
76
  @IsBoolean()
77
- canHaveCount?: boolean = false;
77
+ canHaveCount?: boolean;
78
78
  }
79
79
 
80
80
  export class BuiltinModule<T> extends TakaroDTO<T> {
@@ -1,5 +1,4 @@
1
- import { IntegrationTest, expect } from '@takaro/test';
2
- import { IModuleTestsSetupData, modulesTestSetup } from '@takaro/test';
1
+ import { IntegrationTest, expect, IModuleTestsSetupData, modulesTestSetup, EventsAwaiter } from '@takaro/test';
3
2
  import { GameEvents } from '../dto/index.js';
4
3
 
5
4
  const group = 'Aliases';
@@ -23,26 +22,26 @@ const tests = [
23
22
  },
24
23
  },
25
24
  }),
26
- }
25
+ },
27
26
  );
28
27
 
29
- const setEvents = this.setupData.eventAwaiter.waitForEvents(GameEvents.CHAT_MESSAGE, 1);
28
+ const setEvents = (await new EventsAwaiter().connect(this.client)).waitForEvents(GameEvents.CHAT_MESSAGE, 1);
30
29
  await this.client.command.commandControllerTrigger(this.setupData.gameserver.id, {
31
30
  msg: '/settp test',
32
31
  playerId: this.setupData.players[0].id,
33
32
  });
34
33
 
35
34
  expect((await setEvents).length).to.be.eq(1);
36
- expect((await setEvents)[0].data.msg).to.be.eq('Teleport test set.');
35
+ expect((await setEvents)[0].data.meta.msg).to.be.eq('Teleport test set.');
37
36
 
38
- const events = this.setupData.eventAwaiter.waitForEvents(GameEvents.CHAT_MESSAGE, 1);
37
+ const events = (await new EventsAwaiter().connect(this.client)).waitForEvents(GameEvents.CHAT_MESSAGE, 1);
39
38
  await this.client.command.commandControllerTrigger(this.setupData.gameserver.id, {
40
39
  msg: '/tellyport test',
41
40
  playerId: this.setupData.players[0].id,
42
41
  });
43
42
 
44
43
  expect((await events).length).to.be.eq(1);
45
- expect((await events)[0].data.msg).to.be.eq('Teleported to test.');
44
+ expect((await events)[0].data.meta.msg).to.be.eq('Teleported to test.');
46
45
  },
47
46
  }),
48
47
  ];
@@ -0,0 +1,72 @@
1
+ import { IntegrationTest, expect, IModuleTestsSetupData, modulesTestSetup, EventsAwaiter } from '@takaro/test';
2
+ import { GameEvents } from '../dto/index.js';
3
+ import { EventChatMessageChannelEnum } from '@takaro/apiclient';
4
+
5
+ const group = 'Bug repros';
6
+
7
+ const tests = [
8
+ new IntegrationTest<IModuleTestsSetupData>({
9
+ group,
10
+ snapshot: false,
11
+ name: 'Bug repro: can trigger 2 hooks for the same event inside a single module',
12
+ setup: modulesTestSetup,
13
+ test: async function () {
14
+ const genFn = (param: string) => {
15
+ return `import { data, takaro } from '@takaro/helpers';
16
+ async function main() {
17
+ const { player } = data;
18
+ await takaro.gameserver.gameServerControllerSendMessage(data.gameServerId, {
19
+ message: '${param} hook',
20
+ });
21
+ }
22
+ await main();`;
23
+ };
24
+
25
+ const mod = (
26
+ await this.client.module.moduleControllerCreate({
27
+ name: 'Test module',
28
+ })
29
+ ).data.data;
30
+ // Add the buggy hooks
31
+ await this.client.hook.hookControllerCreate({
32
+ name: 'Test hook 1',
33
+ moduleId: mod.id,
34
+ regex: 'test msg',
35
+ eventType: 'chat-message',
36
+ function: genFn('First'),
37
+ });
38
+
39
+ await this.client.hook.hookControllerCreate({
40
+ name: 'Test hook 2',
41
+ moduleId: mod.id,
42
+ regex: 'test msg',
43
+ eventType: 'chat-message',
44
+ function: genFn('Second'),
45
+ });
46
+
47
+ await this.client.gameserver.gameServerControllerInstallModule(this.setupData.gameserver.id, mod.id);
48
+
49
+ const events = (await new EventsAwaiter().connect(this.client)).waitForEvents(GameEvents.CHAT_MESSAGE, 2);
50
+
51
+ await this.client.hook.hookControllerTrigger({
52
+ eventType: 'chat-message',
53
+ gameServerId: this.setupData.gameserver.id,
54
+ moduleId: mod.id,
55
+ playerId: this.setupData.players[0].id,
56
+ eventMeta: {
57
+ msg: 'test msg',
58
+ channel: EventChatMessageChannelEnum.Global,
59
+ },
60
+ });
61
+
62
+ expect((await events).length).to.be.eq(2);
63
+ expect((await events).map((e) => e.data.meta.msg)).to.include.members(['First hook', 'Second hook']);
64
+ },
65
+ }),
66
+ ];
67
+
68
+ describe(group, function () {
69
+ tests.forEach((test) => {
70
+ test.run();
71
+ });
72
+ });
@@ -1,5 +1,4 @@
1
- import { IntegrationTest, expect } from '@takaro/test';
2
- import { IModuleTestsSetupData, modulesTestSetup } from '@takaro/test';
1
+ import { IntegrationTest, expect, IModuleTestsSetupData, modulesTestSetup, EventsAwaiter } from '@takaro/test';
3
2
  import { GameEvents } from '../dto/gameEvents.js';
4
3
  import { CommandArgumentCreateDTO } from '@takaro/apiclient';
5
4
 
@@ -77,7 +76,7 @@ const tests = [
77
76
  },
78
77
  ]),
79
78
  test: async function () {
80
- const events = this.setupData.eventAwaiter.waitForEvents(GameEvents.CHAT_MESSAGE, 1);
79
+ const events = (await new EventsAwaiter().connect(this.client)).waitForEvents(GameEvents.CHAT_MESSAGE, 1);
81
80
 
82
81
  await this.client.command.commandControllerTrigger(this.setupData.gameserver.id, {
83
82
  msg: '/test "test"',
@@ -85,8 +84,8 @@ const tests = [
85
84
  });
86
85
 
87
86
  expect((await events).length).to.be.eq(1);
88
- expect((await events)[0].data.msg).to.be.eq(
89
- 'The value for "test" should be a number. Please correct it and try again.'
87
+ expect((await events)[0].data.meta.msg).to.be.eq(
88
+ 'The value for "test" should be a number. Please correct it and try again.',
90
89
  );
91
90
  },
92
91
  }),
@@ -100,7 +99,7 @@ const tests = [
100
99
  { name: 'number', type: 'number', position: 2, defaultValue: '42' },
101
100
  ]),
102
101
  test: async function () {
103
- const events = this.setupData.eventAwaiter.waitForEvents(GameEvents.CHAT_MESSAGE, 1);
102
+ const events = (await new EventsAwaiter().connect(this.client)).waitForEvents(GameEvents.CHAT_MESSAGE, 1);
104
103
 
105
104
  await this.client.command.commandControllerTrigger(this.setupData.gameserver.id, {
106
105
  msg: '/test "test"',
@@ -108,7 +107,7 @@ const tests = [
108
107
  });
109
108
 
110
109
  expect((await events).length).to.be.eq(1);
111
- expect((await events)[0].data.msg).to.be.eq('{"name":"test","public":false,"number":42}');
110
+ expect((await events)[0].data.meta.msg).to.be.eq('{"name":"test","public":false,"number":42}');
112
111
  },
113
112
  }),
114
113
  new IntegrationTest<IModuleTestsSetupData>({
@@ -120,7 +119,7 @@ const tests = [
120
119
  { name: 'public', type: 'boolean', position: 1 },
121
120
  ]),
122
121
  test: async function () {
123
- const events = this.setupData.eventAwaiter.waitForEvents(GameEvents.CHAT_MESSAGE, 1);
122
+ const events = (await new EventsAwaiter().connect(this.client)).waitForEvents(GameEvents.CHAT_MESSAGE, 1);
124
123
 
125
124
  await this.client.command.commandControllerTrigger(this.setupData.gameserver.id, {
126
125
  msg: '/test "test test" true',
@@ -128,7 +127,7 @@ const tests = [
128
127
  });
129
128
 
130
129
  expect((await events).length).to.be.eq(1);
131
- expect((await events)[0].data.msg).to.be.eq('{"name":"test test","public":true}');
130
+ expect((await events)[0].data.meta.msg).to.be.eq('{"name":"test test","public":true}');
132
131
  },
133
132
  }),
134
133
  new IntegrationTest<IModuleTestsSetupData>({
@@ -137,7 +136,7 @@ const tests = [
137
136
  snapshot: false,
138
137
  setup: playerArgSetup,
139
138
  test: async function () {
140
- const events = this.setupData.eventAwaiter.waitForEvents(GameEvents.CHAT_MESSAGE, 2);
139
+ const events = (await new EventsAwaiter().connect(this.client)).waitForEvents(GameEvents.CHAT_MESSAGE, 2);
141
140
 
142
141
  const pogRes = await this.client.playerOnGameserver.playerOnGameServerControllerSearch({
143
142
  filters: {
@@ -153,8 +152,8 @@ const tests = [
153
152
  });
154
153
 
155
154
  expect((await events).length).to.be.eq(2);
156
- expect((await events)[0].data.msg).to.be.eq(pog.gameId);
157
- expect((await events)[1].data.msg).to.be.eq(pog.positionX?.toString());
155
+ expect((await events)[0].data.meta.msg).to.be.eq(pog.gameId);
156
+ expect((await events)[1].data.meta.msg).to.be.eq(pog.positionX?.toString());
158
157
  },
159
158
  }),
160
159
  new IntegrationTest<IModuleTestsSetupData>({
@@ -163,7 +162,7 @@ const tests = [
163
162
  snapshot: false,
164
163
  setup: playerArgSetup,
165
164
  test: async function () {
166
- const events = this.setupData.eventAwaiter.waitForEvents(GameEvents.CHAT_MESSAGE, 2);
165
+ const events = (await new EventsAwaiter().connect(this.client)).waitForEvents(GameEvents.CHAT_MESSAGE, 2);
167
166
 
168
167
  const pogRes = await this.client.playerOnGameserver.playerOnGameServerControllerSearch({
169
168
  filters: {
@@ -179,8 +178,8 @@ const tests = [
179
178
  });
180
179
 
181
180
  expect((await events).length).to.be.eq(2);
182
- expect((await events)[0].data.msg).to.be.eq(pog.gameId);
183
- expect((await events)[1].data.msg).to.be.eq(pog.positionX?.toString());
181
+ expect((await events)[0].data.meta.msg).to.be.eq(pog.gameId);
182
+ expect((await events)[1].data.meta.msg).to.be.eq(pog.positionX?.toString());
184
183
  },
185
184
  }),
186
185
  new IntegrationTest<IModuleTestsSetupData>({
@@ -189,7 +188,7 @@ const tests = [
189
188
  snapshot: false,
190
189
  setup: playerArgSetup,
191
190
  test: async function () {
192
- const events = this.setupData.eventAwaiter.waitForEvents(GameEvents.CHAT_MESSAGE, 2);
191
+ const events = (await new EventsAwaiter().connect(this.client)).waitForEvents(GameEvents.CHAT_MESSAGE, 2);
193
192
 
194
193
  const pogRes = await this.client.playerOnGameserver.playerOnGameServerControllerSearch({
195
194
  filters: {
@@ -205,8 +204,8 @@ const tests = [
205
204
  });
206
205
 
207
206
  expect((await events).length).to.be.eq(2);
208
- expect((await events)[0].data.msg).to.be.eq(pog.gameId);
209
- expect((await events)[1].data.msg).to.be.eq(pog.positionX?.toString());
207
+ expect((await events)[0].data.meta.msg).to.be.eq(pog.gameId);
208
+ expect((await events)[1].data.meta.msg).to.be.eq(pog.positionX?.toString());
210
209
  },
211
210
  }),
212
211
  new IntegrationTest<IModuleTestsSetupData>({
@@ -215,7 +214,7 @@ const tests = [
215
214
  snapshot: false,
216
215
  setup: playerArgSetup,
217
216
  test: async function () {
218
- const events = this.setupData.eventAwaiter.waitForEvents(GameEvents.CHAT_MESSAGE, 2);
217
+ const events = (await new EventsAwaiter().connect(this.client)).waitForEvents(GameEvents.CHAT_MESSAGE, 2);
219
218
 
220
219
  const pogRes = await this.client.playerOnGameserver.playerOnGameServerControllerSearch({
221
220
  filters: {
@@ -231,8 +230,8 @@ const tests = [
231
230
  });
232
231
 
233
232
  expect((await events).length).to.be.eq(2);
234
- expect((await events)[0].data.msg).to.be.eq(pog.gameId);
235
- expect((await events)[1].data.msg).to.be.eq(pog.positionX?.toString());
233
+ expect((await events)[0].data.meta.msg).to.be.eq(pog.gameId);
234
+ expect((await events)[1].data.meta.msg).to.be.eq(pog.positionX?.toString());
236
235
  },
237
236
  }),
238
237
  new IntegrationTest<IModuleTestsSetupData>({
@@ -241,7 +240,7 @@ const tests = [
241
240
  snapshot: false,
242
241
  setup: playerArgSetup,
243
242
  test: async function () {
244
- const events = this.setupData.eventAwaiter.waitForEvents(GameEvents.CHAT_MESSAGE, 1);
243
+ const events = (await new EventsAwaiter().connect(this.client)).waitForEvents(GameEvents.CHAT_MESSAGE, 1);
245
244
 
246
245
  // Find a letter contained in one of the players' names
247
246
  const letterToSearch = ['e', 'a'].find((letter) => {
@@ -256,7 +255,7 @@ const tests = [
256
255
  });
257
256
 
258
257
  expect((await events).length).to.be.eq(1);
259
- expect((await events)[0].data.msg).to.match(/Multiple players found/);
258
+ expect((await events)[0].data.meta.msg).to.match(/Multiple players found/);
260
259
  },
261
260
  }),
262
261
  new IntegrationTest<IModuleTestsSetupData>({
@@ -265,7 +264,7 @@ const tests = [
265
264
  snapshot: false,
266
265
  setup: playerArgSetup,
267
266
  test: async function () {
268
- const events = this.setupData.eventAwaiter.waitForEvents(GameEvents.CHAT_MESSAGE, 1);
267
+ const events = (await new EventsAwaiter().connect(this.client)).waitForEvents(GameEvents.CHAT_MESSAGE, 1);
269
268
 
270
269
  await this.client.command.commandControllerTrigger(this.setupData.gameserver.id, {
271
270
  msg: '/test itsimpossiblethatwewilleverfindaplayerwiththisnameright',
@@ -273,7 +272,7 @@ const tests = [
273
272
  });
274
273
 
275
274
  expect((await events).length).to.be.eq(1);
276
- expect((await events)[0].data.msg).to.match(/No player found with the name or ID/);
275
+ expect((await events)[0].data.meta.msg).to.match(/No player found with the name or ID/);
277
276
  },
278
277
  }),
279
278
  ];