Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: serveme.tf integration #124

Merged
merged 1 commit into from
Dec 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@
"@kitajs/html": "4.2.6",
"@kitajs/ts-html-plugin": "4.1.1",
"@tailwindcss/typography": "0.5.15",
"@tf2pickup-org/serveme-tf-client": "0.1.2",
"async-mutex": "0.5.0",
"autoprefixer": "10.4.20",
"country-flag-icons": "1.5.13",
"croner": "9.0.0",
"cssnano": "7.0.6",
"date-fns": "4.1.0",
Expand Down
19 changes: 19 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions public/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import './htmx.js'
import 'https://unpkg.com/htmx-ext-head-support@2.0.2/head-support.js'
import 'https://unpkg.com/htmx-ext-ws@2.0.1/ws.js'
import 'https://unpkg.com/hyperscript.org@0.9.13/dist/_hyperscript.min.js'
import 'https://unpkg.com/htmx.org@1.9.12/dist/ext/remove-me.js'

import './countdown.js'
import './fade-scroll.js'
Expand Down
56 changes: 47 additions & 9 deletions src/admin/game-servers/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,49 @@
import { z } from 'zod'
import { standardAdminPage } from '../plugins/standard-admin-page'
import fp from 'fastify-plugin'
import { GameServersPage } from './views/html/game-servers.page'
import type { ZodTypeProvider } from 'fastify-type-provider-zod'
import { PlayerRole } from '../../database/models/player.model'
import { z } from 'zod'
import { configuration } from '../../configuration'
import { RegionList } from './views/html/serveme-tf-preferred-region'

export default standardAdminPage({
path: '/admin/game-servers',
page: async user => await GameServersPage({ user }),
bodySchema: z.object({}),
// eslint-disable-next-line @typescript-eslint/no-empty-function
save: async () => {},
})
export default fp(
// eslint-disable-next-line @typescript-eslint/require-await
async app => {
app
.withTypeProvider<ZodTypeProvider>()
.get(
'/admin/game-servers',
{
config: {
authorize: [PlayerRole.admin],
},
},
async (request, reply) => {
return reply.status(200).html(GameServersPage({ user: request.user! }))
},
)
.put(
'/admin/game-servers/serveme-tf/preferred-region',
{
config: {
authorize: [PlayerRole.admin],
},
schema: {
body: z.object({
servemeTfPreferredRegion: z.string().transform(val => (val === 'none' ? null : val)),
}),
},
},
async (request, reply) => {
await configuration.set(
'serveme_tf.preferred_region',
request.body.servemeTfPreferredRegion,
)
return reply.status(200).html(RegionList({ saveResult: { success: true } }))
},
)
},
{
name: `admin - game servers`,
},
)
12 changes: 12 additions & 0 deletions src/admin/game-servers/views/html/game-servers.page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { User } from '../../../../auth/types/user'
import { servemeTf } from '../../../../serveme-tf'
import { Admin } from '../../../views/html/admin'
import { ServemeTfPreferredRegion } from './serveme-tf-preferred-region'
import { ServemeTfStatus } from './serveme-tf-status'
import { StaticGameServerList } from './static-game-server-list'

export async function GameServersPage(props: { user: User }) {
Expand All @@ -9,6 +12,15 @@ export async function GameServersPage(props: { user: User }) {
<h4 class="pb-4">Static servers</h4>
<StaticGameServerList />
</div>

<div class="admin-panel-set mt-4">
<div class="flex flex-row gap-2">
<h4 class="pb-4">serveme.tf</h4>
<ServemeTfStatus />
</div>

{servemeTf.isEnabled && <ServemeTfPreferredRegion />}
</div>
</Admin>
)
}
67 changes: 67 additions & 0 deletions src/admin/game-servers/views/html/serveme-tf-preferred-region.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { memoize } from 'lodash-es'
import { servemeTf } from '../../../../serveme-tf'
import getUnicodeFlagIcon from 'country-flag-icons/unicode'
import { configuration } from '../../../../configuration'
import { IconLoader3 } from '../../../../html/components/icons'

export function ServemeTfPreferredRegion() {
return (
<p>
<dl>
<dt>
<label for="serveme-tf-preferred-region-select">
Preferred region for reserved servers
</label>
</dt>
<dd>
<div>
<RegionList />
</div>
<span class="text-sm text-abru-light-75">
If a game server from the preferred region is not available, another one will be picked
up instead.
</span>
</dd>
</dl>
</p>
)
}

const getRegions = memoize(async () => await servemeTf.listRegions())

type SaveResult = { success: true }

export async function RegionList(props?: { saveResult?: SaveResult }) {
const regions = await getRegions()
const selected = await configuration.get('serveme_tf.preferred_region')

return (
<div id="serveme-tf-preferred-region" class="flex flex-row items-center gap-2">
<select
name="servemeTfPreferredRegion"
id="serveme-tf-preferred-region-select"
hx-put="/admin/game-servers/serveme-tf/preferred-region"
hx-trigger="change"
hx-target="#serveme-tf-preferred-region"
hx-swap="outerHTML"
hx-disabled-elt="this"
class="peer"
>
<option value="none" selected={selected === null}>
none
</option>
{regions.map(region => (
<option value={region} selected={region === selected} safe>
{getUnicodeFlagIcon(region)} {region}
</option>
))}
</select>
<IconLoader3 class="hidden animate-spin peer-[.htmx-request]:block" />
{props?.saveResult?.success === true && (
<span remove-me="3s" class="text-green-600">
saved!
</span>
)}
</div>
)
}
16 changes: 16 additions & 0 deletions src/admin/game-servers/views/html/serveme-tf-status.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { IconCheck, IconX } from '../../../../html/components/icons'
import { servemeTf } from '../../../../serveme-tf'

export function ServemeTfStatus() {
return servemeTf.isEnabled ? (
<span class="flex flex-row text-green-600">
<IconCheck />
enabled
</span>
) : (
<span class="flex flex-row text-red-600">
<IconX />
disabled
</span>
)
}
4 changes: 2 additions & 2 deletions src/admin/plugins/standard-admin-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function standardAdminPage<Input, Output>({
},
},
async (request, reply) => {
reply.status(200).html(await page(request.user!))
return reply.status(200).html(page(request.user!))
},
)
.post(
Expand All @@ -47,7 +47,7 @@ export function standardAdminPage<Input, Output>({
async (request, reply) => {
await save(request.body as Input)
requestContext.set('messages', { success: ['Configuration saved'] })
reply.status(200).html(await page(request.user!))
return reply.status(200).html(page(request.user!))
},
)
},
Expand Down
2 changes: 1 addition & 1 deletion src/database/models/configuration-entry.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export const configurationSchema = z.discriminatedUnion('key', [
value: z.string().nullable().default(null),
}),
z.object({
key: z.literal('serveme.tf.ban_gameservers'),
key: z.literal('serveme_tf.ban_gameservers'),
value: z.array(z.string()).default([]),
}),
z.object({
Expand Down
9 changes: 9 additions & 0 deletions src/database/models/game.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ export interface GameServer {
name: string
address: string
port: string

// if logSecret is undefined, a random one will be assigned automatically
logSecret?: string

rcon: {
address: string
port: string
password: string
}
}

export interface GameModel {
Expand Down
4 changes: 4 additions & 0 deletions src/environment.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { z } from 'zod'
import dotenv from 'dotenv'
import { KnownEndpoint } from '@tf2pickup-org/serveme-tf-client'

dotenv.config()

Expand All @@ -19,6 +20,9 @@ const environmentSchema = z.object({
GAME_SERVER_SECRET: z.string(),
THUMBNAIL_SERVICE_URL: z.string().url(),

SERVEME_TF_API_ENDPOINT: z.string().default(KnownEndpoint.europe),
SERVEME_TF_API_KEY: z.string().optional(),

TWITCH_CLIENT_ID: z.string().optional(),
TWITCH_CLIENT_SECRET: z.string().optional(),
})
Expand Down
29 changes: 15 additions & 14 deletions src/game-servers/assign.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,40 @@
import { GameServerProvider, type GameModel } from '../database/models/game.model'
import { type GameModel, type GameServer } from '../database/models/game.model'
import { games } from '../games'
import { staticGameServers } from '../static-game-servers'
import { events } from '../events'
import { GameEventType } from '../database/models/game-event.model'
import { logger } from '../logger'
import { Mutex } from 'async-mutex'
import { servemeTf } from '../serveme-tf'

const mutex = new Mutex()

export async function assign(game: GameModel) {
await mutex.runExclusive(async () => {
const staticGameServer = await staticGameServers.assign(game)
if (!staticGameServer) {
throw new Error(`no free servers available for game ${game.number}`)
}
const gameServer = await assignFirstFree(game)

game = await games.update(game.number, {
$set: {
gameServer: {
id: staticGameServer.id,
name: staticGameServer.name,
address: staticGameServer.address,
port: staticGameServer.port,
provider: GameServerProvider.static,
},
gameServer,
},
$push: {
events: {
event: GameEventType.gameServerAssigned,
at: new Date(),
gameServerName: staticGameServer.name,
gameServerName: gameServer.name,
},
},
})
logger.info({ game }, `game ${game.number} assigned to game server ${staticGameServer.name}`)
logger.info({ game }, `game ${game.number} assigned to game server ${gameServer.name}`)
events.emit('game:gameServerAssigned', { game })
})
}

function assignFirstFree(game: GameModel): Promise<GameServer> {
return staticGameServers
.assign(game)
.catch(() => servemeTf.assign(game))
.catch(() => {
throw new Error(`no free servers available for game ${game.number}`)
})
}
2 changes: 1 addition & 1 deletion src/games/plugins/sync-clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export default fp(async app => {
events.on(
'game:playerConnectionStatusUpdated',
safe(async ({ game, player, playerConnectionStatus }) => {
app.gateway.broadcast(
app.gateway.to({ url: `/games/${game.number}` }).send(
async () =>
await PlayerConnectionStatusIndicator({
steamId: player,
Expand Down
Loading
Loading