diff --git a/hasura/metadata/actions.graphql b/hasura/metadata/actions.graphql index fa0bd09d..487b3087 100644 --- a/hasura/metadata/actions.graphql +++ b/hasura/metadata/actions.graphql @@ -2684,3 +2684,24 @@ type Mutation { event_ids: [uuid!]! ): SuccessOutput } + +type Mutation { + setServerSettings( + server_id: uuid! + map_rotation: ServerMapRotationInput + plugins: [ServerPluginInput!] + access: ServerAccessInput + ): SuccessOutput +} + +input ServerMapRotationInput { + map_ids: [uuid!]! + shuffle: Boolean! +} + +input ServerAccessInput { + restricted: Boolean! + min_role: String + steam_ids: [String!]! + event_ids: [uuid!]! +} diff --git a/hasura/metadata/actions.yaml b/hasura/metadata/actions.yaml index 78489f63..528cbbfa 100644 --- a/hasura/metadata/actions.yaml +++ b/hasura/metadata/actions.yaml @@ -2030,6 +2030,14 @@ actions: permissions: - role: administrator comment: Restrict who may connect to a community server + - name: setServerSettings + definition: + kind: synchronous + handler: '{{HASURA_GRAPHQL_ACTIONS_HOOK}}' + forward_client_headers: true + permissions: + - role: administrator + comment: Save a community server's rotation, plugins and access, restarting it at most once custom_types: enums: [] input_objects: @@ -2042,6 +2050,8 @@ custom_types: - name: UtilityPlaybookStepInput - name: UtilitySightlinePairInput - name: ServerPluginInput + - name: ServerMapRotationInput + - name: ServerAccessInput objects: - name: Award - name: AwardRecipient diff --git a/src/dedicated-servers/dedicated-server-config.service.ts b/src/dedicated-servers/dedicated-server-config.service.ts index 970698e9..2dadd9c1 100644 --- a/src/dedicated-servers/dedicated-server-config.service.ts +++ b/src/dedicated-servers/dedicated-server-config.service.ts @@ -38,10 +38,48 @@ export class DedicatedServerConfigService { private readonly rcon: RconService, ) {} + // One save from the settings console: the pod settings are written first and + // the server restarts once for all of them, while access applies live. + public async saveSettings( + serverId: string, + settings: { + mapRotation: { mapIds: Array; shuffle: boolean } | null; + plugins: Array<{ slug: string; enabled: boolean }> | null; + access: { + restricted: boolean; + minRole: string | null; + steamIds: Array; + eventIds: Array; + } | null; + }, + ): Promise { + if (settings.mapRotation) { + await this.setMapRotation( + serverId, + settings.mapRotation.mapIds, + settings.mapRotation.shuffle, + { restart: false }, + ); + } + + if (settings.plugins) { + await this.setPlugins(serverId, settings.plugins, { restart: false }); + } + + if (settings.access) { + await this.setAccess(serverId, settings.access); + } + + if (settings.mapRotation || settings.plugins) { + await this.restart(await this.communityServer(serverId)); + } + } + public async setMapRotation( serverId: string, mapIds: Array, shuffle: boolean, + options: { restart?: boolean } = {}, ): Promise { const server = await this.communityServer(serverId); @@ -89,12 +127,15 @@ export class DedicatedServerConfigService { ); }); - await this.restart(server); + if (options.restart !== false) { + await this.restart(server); + } } public async setPlugins( serverId: string, plugins: Array<{ slug: string; enabled: boolean }>, + options: { restart?: boolean } = {}, ): Promise { const server = await this.communityServer(serverId); @@ -115,7 +156,9 @@ export class DedicatedServerConfigService { ); }); - await this.restart(server); + if (options.restart !== false) { + await this.restart(server); + } } // Enforced by the Player Management plugin, which rereads the list on a diff --git a/src/dedicated-servers/dedicated-servers.controller.ts b/src/dedicated-servers/dedicated-servers.controller.ts index 828fab95..603644fb 100644 --- a/src/dedicated-servers/dedicated-servers.controller.ts +++ b/src/dedicated-servers/dedicated-servers.controller.ts @@ -142,6 +142,42 @@ export class DedicatedServersController { })); } + @HasuraAction() + public async setServerSettings(data: { + user: User; + server_id: string; + map_rotation?: { map_ids: Array; shuffle: boolean } | null; + plugins?: Array<{ slug: string; enabled: boolean }> | null; + access?: { + restricted: boolean; + min_role?: string | null; + steam_ids: Array; + event_ids: Array; + } | null; + }) { + this.assertAdministrator(data.user); + + await this.dedicatedServerConfig.saveSettings(data.server_id, { + mapRotation: data.map_rotation + ? { + mapIds: data.map_rotation.map_ids, + shuffle: data.map_rotation.shuffle, + } + : null, + plugins: data.plugins ?? null, + access: data.access + ? { + restricted: data.access.restricted, + minRole: data.access.min_role ?? null, + steamIds: data.access.steam_ids, + eventIds: data.access.event_ids, + } + : null, + }); + + return { success: true }; + } + @HasuraAction() public async setServerMapRotation(data: { user: User; diff --git a/test/server-map-rotation.spec.ts b/test/server-map-rotation.spec.ts index 4dcda73f..6d7f8f42 100644 --- a/test/server-map-rotation.spec.ts +++ b/test/server-map-rotation.spec.ts @@ -178,6 +178,47 @@ describe("dedicated server config (SQL-driven)", () => { }); }); + describe("saveSettings", () => { + beforeEach(async () => { + await postgres.query( + `INSERT INTO game_plugins (slug, kind, name, author, description) + VALUES ('csroll', 'game', 'csroll', 'tester', 'a test plugin') + ON CONFLICT (slug) DO NOTHING`, + ); + await postgres.query( + `INSERT INTO game_plugin_installs (plugin_slug, version, channel) + VALUES ('csroll', NULL, 'Auto') ON CONFLICT (plugin_slug) DO NOTHING`, + ); + }); + + // Saving the rotation and the plugins used to be two saves, each of + // which restarted the server. + it("restarts the server once for rotation and plugin changes together", async () => { + await service.saveSettings(serverId, { + mapRotation: { mapIds: [await map("rotation-a")], shuffle: false }, + plugins: [{ slug: "csroll", enabled: true }], + access: null, + }); + + expect(await rotation()).toHaveLength(1); + expect(dedicatedServers.rebuildDedicatedServer).toHaveBeenCalledTimes(1); + }); + + it("does not restart for an access change alone", async () => { + await service.saveSettings(serverId, { + mapRotation: null, + plugins: null, + access: { restricted: true, minRole: null, steamIds: [], eventIds: [] }, + }); + + const [server] = await postgres.query< + Array<{ access_restricted: boolean }> + >(`SELECT access_restricted FROM servers WHERE id = $1`, [serverId]); + expect(server.access_restricted).toBe(true); + expect(dedicatedServers.rebuildDedicatedServer).not.toHaveBeenCalled(); + }); + }); + describe("setPlugins", () => { beforeEach(async () => { for (const slug of ["csroll", "map-chooser"]) {