Skip to content

Commit

Permalink
Merge pull request #994 from gettakaro/module-functions
Browse files Browse the repository at this point in the history
  • Loading branch information
niekcandaele authored Mar 10, 2024
2 parents a41b1ab + f6241de commit ca097c5
Show file tree
Hide file tree
Showing 98 changed files with 1,277 additions and 476 deletions.
1 change: 1 addition & 0 deletions .mocharc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ module.exports = {
timeout: 30000,
require: ['ts-node/register'],
recursive: true,
['reporter-option']: ['maxDiffSize=16000'],
};
1 change: 0 additions & 1 deletion packages/app-api/src/controllers/GameServerController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,6 @@ class CommandExecuteInputDTO extends TakaroDTO<CommandExecuteInputDTO> {
class MessageSendInputDTO extends TakaroDTO<MessageSendInputDTO> {
@IsString()
@MinLength(1)
@MaxLength(150)
message!: string;

@Type(() => IMessageOptsDTO)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const tests = [
snapshot: true,
name: 'Install a built-in module',
setup: defaultSetup,
filteredFields: ['gameserverId', 'moduleId'],
filteredFields: ['gameserverId', 'moduleId', 'functionId', 'commandId'],
test: async function () {
return this.client.gameserver.gameServerControllerInstallModule(
this.setupData.gameserver.id,
Expand Down Expand Up @@ -81,7 +81,7 @@ const tests = [
snapshot: true,
name: 'Update installation config',
setup: defaultSetup,
filteredFields: ['gameserverId', 'moduleId'],
filteredFields: ['gameserverId', 'moduleId', 'functionId', 'commandId'],
test: async function () {
await this.client.gameserver.gameServerControllerInstallModule(
this.setupData.gameserver.id,
Expand Down Expand Up @@ -131,7 +131,7 @@ const tests = [
});
},
expectedStatus: 400,
filteredFields: ['moduleId'],
filteredFields: ['moduleId', 'functionId', 'commandId'],
}),
new IntegrationTest<ISetupData>({
group,
Expand Down Expand Up @@ -161,7 +161,7 @@ const tests = [
}),
});
},
filteredFields: ['moduleId', 'gameserverId'],
filteredFields: ['moduleId', 'gameserverId', 'functionId'],
}),
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ const tests = [
}
);
},
filteredFields: ['gameserverId', 'moduleId'],
filteredFields: ['gameserverId', 'moduleId', 'functionId'],
}),
new IntegrationTest<ISetupData>({
group,
Expand Down Expand Up @@ -236,7 +236,7 @@ const tests = [
}
);
},
filteredFields: ['gameserverId', 'moduleId'],
filteredFields: ['gameserverId', 'moduleId', 'functionId'],
}),
new IntegrationTest<ISetupData>({
group,
Expand All @@ -257,7 +257,7 @@ const tests = [

return installRes;
},
filteredFields: ['gameserverId', 'moduleId'],
filteredFields: ['gameserverId', 'moduleId', 'functionId'],
}),
];

Expand Down
13 changes: 13 additions & 0 deletions packages/app-api/src/db/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { CommandOutputDTO } from '../service/CommandService.js';
import { FunctionOutputDTO } from '../service/FunctionService.js';
import { getSystemConfigSchema } from '../lib/systemConfig.js';
import { PERMISSION_TABLE_NAME, PermissionModel } from './role.js';
import { FunctionModel } from './function.js';

export const MODULE_TABLE_NAME = 'modules';
export class ModuleModel extends TakaroModel {
Expand All @@ -26,6 +27,7 @@ export class ModuleModel extends TakaroModel {
cronJobs: CronJobOutputDTO[];
hooks: HookOutputDTO[];
commands: CommandOutputDTO[];
functions: FunctionOutputDTO[];

static get relationMappings() {
return {
Expand Down Expand Up @@ -53,6 +55,14 @@ export class ModuleModel extends TakaroModel {
to: `${COMMANDS_TABLE_NAME}.moduleId`,
},
},
functions: {
relation: Model.HasManyRelation,
modelClass: FunctionModel,
join: {
from: `${MODULE_TABLE_NAME}.id`,
to: 'functions.moduleId',
},
},

permissions: {
relation: Model.HasManyRelation,
Expand Down Expand Up @@ -89,6 +99,7 @@ export class ModuleRepo extends ITakaroRepo<ModuleModel, ModuleOutputDTO, Module
'cronJobs',
'hooks',
'commands',
'functions',
'cronJobs.function',
'hooks.function',
'commands.function',
Expand Down Expand Up @@ -126,6 +137,7 @@ export class ModuleRepo extends ITakaroRepo<ModuleModel, ModuleOutputDTO, Module
})
)
),
functions: await Promise.all(item.functions.map((func) => new FunctionOutputDTO().construct(func))),
};

return new ModuleOutputDTO().construct(parsed);
Expand All @@ -149,6 +161,7 @@ export class ModuleRepo extends ITakaroRepo<ModuleModel, ModuleOutputDTO, Module
.withGraphJoined('hooks.function')
.withGraphJoined('commands.function')
.withGraphJoined('commands.arguments')
.withGraphJoined('functions')
.withGraphJoined('permissions')
.orderBy('commands.name', 'DESC');

Expand Down
6 changes: 5 additions & 1 deletion packages/app-api/src/executors/executeFunction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ interface IFunctionResult {
logs: TakaroEventFunctionLog[];
}

export type FunctionExecutor = (code: string, data: Record<string, unknown>, token: string) => Promise<IFunctionResult>;
export type FunctionExecutor = (
code: string,
data: IHookJobData | ICommandJobData | ICronJobData,
token: string
) => Promise<IFunctionResult>;

async function getJobToken(domainId: string) {
const tokenRes = await takaro.domain.domainControllerGetToken({
Expand Down
45 changes: 38 additions & 7 deletions packages/app-api/src/executors/executeLocal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import vm from 'node:vm';
import { FunctionExecutor } from './executeFunction.js';
import { config } from '../config.js';
import { TakaroEventFunctionLog } from '@takaro/modules';
import { IHookJobData, ICommandJobData, ICronJobData } from '@takaro/queues';

/**
* !!!!!!!!!!!!!!!!!!!!! node:vm is not secure, don't use this in production !!!!!!!!!!!!!!!!!
*/
export const executeFunctionLocal: FunctionExecutor = async (
fn: string,
data: Record<string, unknown>,
data: IHookJobData | ICommandJobData | ICronJobData,
token: string
) => {
data.token = token;
Expand All @@ -36,17 +37,35 @@ export const executeFunctionLocal: FunctionExecutor = async (
});

const toEval = new vm.SourceTextModule(fn, { context: contextifiedObject });

const { takaro, data: hydratedData } = getTakaro(data, patchedConsole);

const monkeyPatchedGetData = () => {
return data;
return hydratedData;
};

const monkeyPatchedGetTakaro = function () {
return getTakaro(data, patchedConsole);
return takaro;
};

await toEval.link((specifier: string, referencingModule) => {
const syntheticHelpersModule = new vm.SyntheticModule(
['getTakaro', 'checkPermission', 'TakaroUserError', 'getData', 'axios', '_', 'lodash', 'nextCronJobRun'],
const userModules = data.module.module.functions
.filter((f) => f.name)
.map((f) => ({ name: f.name, code: f.code })) as { name: string; code: string }[];

await toEval.link(async (specifier: string, referencingModule) => {
const takaroHelpersModule = new vm.SyntheticModule(
[
'getTakaro',
'checkPermission',
'TakaroUserError',
'getData',
'axios',
'_',
'lodash',
'nextCronJobRun',
'takaro',
'data',
],
function () {
this.setExport('getTakaro', monkeyPatchedGetTakaro);
this.setExport('checkPermission', checkPermission);
Expand All @@ -56,12 +75,24 @@ export const executeFunctionLocal: FunctionExecutor = async (
this.setExport('_', _);
this.setExport('lodash', _);
this.setExport('nextCronJobRun', nextCronJobRun);
this.setExport('takaro', takaro);
this.setExport('data', hydratedData);
},
{ context: referencingModule.context }
);

if (specifier === '@takaro/helpers') {
return syntheticHelpersModule;
return takaroHelpersModule;
} else {
const userModuleData = userModules.find((m) => `./${m.name}.js` === specifier);
if (userModuleData) {
const userModule = new vm.SourceTextModule(userModuleData.code, { context: referencingModule.context });
await userModule.link(() => {
return takaroHelpersModule;
});
await userModule.evaluate();
return userModule;
}
}

throw new Error(`Unable to resolve dependency: ${specifier}`);
Expand Down
34 changes: 26 additions & 8 deletions packages/app-api/src/service/FunctionService.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,51 @@
import { TakaroService } from './Base.js';

import { IsOptional, IsString } from 'class-validator';
import { IsOptional, IsString, IsUUID } from 'class-validator';
import { FunctionModel, FunctionRepo } from '../db/function.js';
import { TakaroDTO, TakaroModelDTO, traceableClass } from '@takaro/util';
import { ITakaroQuery } from '@takaro/db';
import { PaginatedOutput } from '../db/base.js';

export class FunctionOutputDTO extends TakaroModelDTO<FunctionOutputDTO> {
@IsString()
code!: string;
code: string;

@IsString()
@IsOptional()
name: string;

@IsUUID()
@IsOptional()
moduleId?: string;
}

export class FunctionCreateDTO extends TakaroDTO<FunctionCreateDTO> {
@IsString()
@IsOptional()
code?: string;

@IsString()
@IsOptional()
name: string;

@IsUUID()
@IsOptional()
moduleId?: string;
}

export class FunctionUpdateDTO extends TakaroDTO<FunctionUpdateDTO> {
@IsString()
code!: string;
@IsOptional()
code: string;

@IsString()
@IsOptional()
name: string;
}

const defaultFunctionCode = `import { getTakaro, getData } from '@takaro/helpers';
const defaultFunctionCode = `import { data, takaro } from '@takaro/helpers';
async function main() {
const data = await getData();
const takaro = await getTakaro(data);
// TODO: write my function...
const {} = data;
}
await main();`;

Expand Down
42 changes: 34 additions & 8 deletions packages/app-api/src/service/GameServerService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { TakaroService } from './Base.js';
import { GameServerModel, GameServerRepo } from '../db/gameserver.js';
import { IsBoolean, IsEnum, IsJSON, IsObject, IsOptional, IsString, IsUUID, Length } from 'class-validator';
import {
IsBoolean,
IsEnum,
IsJSON,
IsObject,
IsOptional,
IsString,
IsUUID,
Length,
ValidateNested,
} from 'class-validator';
import {
IMessageOptsDTO,
IGameServer,
Expand All @@ -27,6 +37,8 @@ import {
} from '@takaro/modules';
import { ITakaroQuery } from '@takaro/db';
import { PaginatedOutput } from '../db/base.js';
import type { ModuleOutputDTO as ModuleOutputDTOType } from './ModuleService.js';
import { ModuleOutputDTO } from './ModuleService.js';
import { ModuleService } from './ModuleService.js';

// Curse you ESM... :(
Expand All @@ -37,6 +49,7 @@ import { PlayerOnGameServerService, PlayerOnGameServerUpdateDTO } from './Player
import { ItemCreateDTO, ItemsService } from './ItemsService.js';
import { randomUUID } from 'crypto';
import { EVENT_TYPES, EventCreateDTO, EventService } from './EventService.js';
import { Type } from 'class-transformer';

const Ajv = _Ajv as unknown as typeof _Ajv.default;
const ajv = new Ajv({ useDefaults: true, strict: true });
Expand Down Expand Up @@ -109,6 +122,10 @@ export class ModuleInstallationOutputDTO extends TakaroModelDTO<ModuleInstallati
@IsUUID()
moduleId: string;

@ValidateNested()
@Type(() => ModuleOutputDTO)
module: ModuleOutputDTOType;

@IsObject()
userConfig: Record<string, any>;

Check warning on line 130 in packages/app-api/src/service/GameServerService.ts

View workflow job for this annotation

GitHub Actions / Run Prettier and Commit Changes

Unexpected any. Specify a different type

Check warning on line 130 in packages/app-api/src/service/GameServerService.ts

View workflow job for this annotation

GitHub Actions / node-ci (18.18)

Unexpected any. Specify a different type

Expand Down Expand Up @@ -225,7 +242,17 @@ export class GameServerService extends TakaroService<
}

async getModuleInstallation(gameserverId: string, moduleId: string) {
return this.repo.getModuleInstallation(gameserverId, moduleId);
const moduleService = new ModuleService(this.domainId);
const mod = await moduleService.findOne(moduleId);
const installation = await this.repo.getModuleInstallation(gameserverId, moduleId);

return new ModuleInstallationOutputDTO().construct({
gameserverId,
moduleId,
module: mod,
userConfig: installation.userConfig,
systemConfig: installation.systemConfig,
});
}

async installModule(gameserverId: string, moduleId: string, installDto?: ModuleInstallDTO) {
Expand Down Expand Up @@ -286,12 +313,7 @@ export class GameServerService extends TakaroService<
})
);

return new ModuleInstallationOutputDTO().construct({
gameserverId,
moduleId,
userConfig: installation.userConfig,
systemConfig: installation.systemConfig,
});
return this.getModuleInstallation(gameserverId, moduleId);
}

async uninstallModule(gameserverId: string, moduleId: string) {
Expand Down Expand Up @@ -386,6 +408,10 @@ export class GameServerService extends TakaroService<
}

async sendMessage(gameServerId: string, message: string, opts: IMessageOptsDTO) {
// Limit message length to 150 characters
// Longer than this and gameservers start acting _weird_
message = message.substring(0, 150);

const gameInstance = await this.getGame(gameServerId);
await gameInstance.sendMessage(message, opts);

Expand Down
Loading

0 comments on commit ca097c5

Please sign in to comment.