Skip to content

Commit

Permalink
Merge pull request #8 from diogomartino/6-admin-panel
Browse files Browse the repository at this point in the history
FEATURE: admin panel
  • Loading branch information
diogomartino authored Feb 3, 2024
2 parents 7819b86 + 7a760ae commit 0cdd5b2
Show file tree
Hide file tree
Showing 22 changed files with 814 additions and 60 deletions.
25 changes: 24 additions & 1 deletion app.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ package main

import (
"context"
"fmt"
backupsmanager "palword-ds-gui/backups-manager"
dedicatedserver "palword-ds-gui/dedicated-server"
rconclient "palword-ds-gui/rcon-client"
"palword-ds-gui/steamcmd"
"palword-ds-gui/utils"

"github.com/gocolly/colly/v2"
"github.com/wailsapp/wails/v2/pkg/runtime"
)

Expand All @@ -15,14 +18,16 @@ type App struct {
steamCmd *steamcmd.SteamCMD
dedicatedServer *dedicatedserver.DedicatedServer
backupsManager *backupsmanager.BackupManager
rconClient *rconclient.RconClient
reactReady bool
}

func NewApp(server *dedicatedserver.DedicatedServer, cmd *steamcmd.SteamCMD, backupManager *backupsmanager.BackupManager) *App {
func NewApp(server *dedicatedserver.DedicatedServer, cmd *steamcmd.SteamCMD, backupManager *backupsmanager.BackupManager, rconClient *rconclient.RconClient) *App {
return &App{
steamCmd: cmd,
dedicatedServer: server,
backupsManager: backupManager,
rconClient: rconClient,
}
}

Expand Down Expand Up @@ -67,3 +72,21 @@ func (a *App) shutdown(ctx context.Context) {
func (a *App) OpenInBrowser(url string) {
runtime.BrowserOpenURL(a.ctx, url)
}

func (a *App) GetSteamProfileURL(steamid string) {
profileURL := fmt.Sprintf("https://steamcommunity.com/profiles/%s", steamid)
c := colly.NewCollector()

var profileImageURL string

c.OnHTML(".playerAvatarAutoSizeInner > img", func(e *colly.HTMLElement) {
profileImageURL = e.Attr("src")
runtime.EventsEmit(a.ctx, "RETURN_STEAM_IMAGE", fmt.Sprintf("%s|%s", steamid, profileImageURL))
})

err := c.Visit(profileURL)

if err != nil {
return
}
}
46 changes: 36 additions & 10 deletions frontend/src/actions/app.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { appSliceActions } from '../store/app-slice';
import { store } from '../store';
import { LoadingStatus } from '../types';
import { LoadingStatus, TSettings } from '../types';
import { DesktopApi } from '../desktop';
import { settingsSelector } from '../selectors/app';
import { settingsSelector, steamImagesCacheSelector } from '../selectors/app';

export const setLoadingStatus = (loadingStatus: LoadingStatus) => {
store.dispatch(appSliceActions.setLoadingStatus(loadingStatus));
Expand All @@ -16,22 +16,33 @@ export const setLaunchParams = (launchParams: string) => {
store.dispatch(appSliceActions.setLaunchParams(launchParams ?? ''));
};

export const saveSettings = () => {
const state = store.getState();
const settings = settingsSelector(state);
export const setRconCredentials = (host: string, password: string) => {
store.dispatch(appSliceActions.setRconCredentials({ host, password }));
};

localStorage.setItem('settings', JSON.stringify(settings));
export const saveSettings = (settings?: TSettings) => {
let targetSettings;

if (settings) {
targetSettings = settings;
} else {
// only get the settings from the store if it's not passed as an argument so we can call this function from the store
const state = store.getState();
targetSettings = settingsSelector(state);
}

localStorage.setItem('settings', JSON.stringify(targetSettings));
};

export const initApp = () => {
export const initApp = async () => {
const state = store.getState();
const { backup } = settingsSelector(state);

DesktopApi.server.readConfig();
DesktopApi.server.readSaveName();
await DesktopApi.server.readConfig();
await DesktopApi.server.readSaveName();

if (backup.enabled) {
DesktopApi.backups.start(backup.intervalHours, backup.keepCount);
await DesktopApi.backups.start(backup.intervalHours, backup.keepCount);
}
};

Expand Down Expand Up @@ -72,3 +83,18 @@ export const checkForUpdates = async () => {
console.error(error);
}
};

export const addSteamImage = (steamId: string, imageUrl: string) => {
store.dispatch(appSliceActions.addSteamImage({ steamId, imageUrl }));
};

export const cacheSteamImage = async (steamId: string) => {
const state = store.getState();
const steamCache = steamImagesCacheSelector(state);

if (steamCache[steamId]) {
return;
}

DesktopApi.getProfileImageURL(steamId);
};
30 changes: 20 additions & 10 deletions frontend/src/components/layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,32 @@ type TLayoutProps = {
className?: string;
title?: string;
subtitle?: string | React.ReactNode;
rightSlot?: React.ReactNode;
};

const Layout = ({ children, className, title, subtitle }: TLayoutProps) => {
const Layout = ({
children,
className,
title,
subtitle,
rightSlot
}: TLayoutProps) => {
return (
<div className="flex h-full">
<Sidebar />
<main className={cx('flex-1 p-4 overflow-auto', className)}>
{(title || subtitle) && (
<div className="flex flex-col gap-1">
{title && <p className="text-4xl">{title}</p>}
{subtitle && typeof subtitle === 'string' && (
<p className="text-sm text-neutral-500">{subtitle}</p>
)}
{subtitle && typeof subtitle !== 'string' && subtitle}
</div>
)}
<div className="flex items-center justify-between">
{(title || subtitle) && (
<div className="flex flex-col gap-1">
{title && <p className="text-4xl">{title}</p>}
{subtitle && typeof subtitle === 'string' && (
<p className="text-sm text-neutral-500">{subtitle}</p>
)}
{subtitle && typeof subtitle !== 'string' && subtitle}
</div>
)}
{rightSlot && <div>{rightSlot}</div>}
</div>
{children}
</main>
</div>
Expand Down
104 changes: 102 additions & 2 deletions frontend/src/desktop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import { AppEvent, TGenericFunction } from './types';
import { EventsOff, EventsOn } from './wailsjs/runtime/runtime';
import * as DedicatedServer from './wailsjs/go/dedicatedserver/DedicatedServer';
import * as BackupManager from './wailsjs/go/backupsmanager/BackupManager';
import * as RconClient from './wailsjs/go/rconclient/RconClient';
import * as App from './wailsjs/go/main/App';
import { parseConfig, serializeConfig } from './helpers/config-parser';
import { setConfig, setSaveName } from './actions/server';
import { TConfig } from './types/server-config';
import { ConfigKey, TConfig } from './types/server-config';
import { store } from './store';
import { launchParamsSelector } from './selectors/app';
import { launchParamsSelector, rconCredentialsSelector } from './selectors/app';
import { RconCommand, TRconInfo, TRconPlayer } from './types/rcon';
import { setRconCredentials } from './actions/app';

export const DesktopApi = {
onAppEvent: (
Expand All @@ -33,12 +36,19 @@ export const DesktopApi = {
initApp: async () => {
await App.InitApp();
},
getProfileImageURL: async (steamID64: string) => {
await App.GetSteamProfileURL(steamID64);
},
server: {
readConfig: async () => {
const configString = await DedicatedServer.ReadConfig();
const config = parseConfig(configString);

setConfig(config);
setRconCredentials(
`127.0.0.1:${config[ConfigKey.RCONPort]}`,
config[ConfigKey.AdminPassword]
);
},
writeConfig: async (config: TConfig) => {
const serializedConfig = serializeConfig(config);
Expand Down Expand Up @@ -102,5 +112,95 @@ export const DesktopApi = {
restore: async (backupFileName: string) => {
await BackupManager.Restore(backupFileName);
}
},
rcon: {
execute: async (command: string) => {
const state = store.getState();
const rconCredentials = rconCredentialsSelector(state);

const result = await RconClient.Execute(
rconCredentials.host,
rconCredentials.password,
command
);

return result.trim();
},
getInfo: async (): Promise<TRconInfo | undefined> => {
try {
const result = (
(await DesktopApi.rcon.execute(RconCommand.INFO)) || ''
).trim();

const regex = /Welcome to Pal Server\[(.*?)\]\s*(.*)/;
const match = result.match(regex);
const [, version, name] = match || [];

return {
version,
name
};
} catch {
//
}

return undefined;
},
getPlayers: async (): Promise<TRconPlayer[]> => {
try {
const result = (
(await DesktopApi.rcon.execute(RconCommand.SHOW_PLAYERS)) || ''
).trim();

const lines = result.split('\n');

lines.shift(); // remove the first line which is the header

const players = lines.map((line) => {
const [name, uid, steamId] = line.split(',').map((s) => s.trim());
const player: TRconPlayer = {
name,
uid,
steamId
};

DesktopApi.getProfileImageURL(steamId); // crawl steam profile image and cache the url in the store so we don't have to do it again for the same player

return player;
});

return players;
} catch {
//
}

return [];
},
save: async () => {
await DesktopApi.rcon.execute(RconCommand.SAVE);
},
shutdown: async (
message: string = 'Server is being shutdown',
seconds?: number
) => {
const command = `${RconCommand.SHUTDOWN} ${seconds ?? 1} ${message}`;

await DesktopApi.rcon.execute(command);
},
sendMessage: async (messages: string) => {
const command = `${RconCommand.BROADCAST} ${messages}`;

await DesktopApi.rcon.execute(command);
},
ban: async (uid: string) => {
const command = `${RconCommand.BAN} ${uid}`;

await DesktopApi.rcon.execute(command);
},
kick: async (uid: string) => {
const command = `${RconCommand.KICK} ${uid}`;

await DesktopApi.rcon.execute(command);
}
}
};
2 changes: 1 addition & 1 deletion frontend/src/helpers/config-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ OptionSettings=({__SETTINGS__})`;
if (configTypes[key] === 'string') {
settings.push(`${key}="${sanitizedValue}"`);
} else if (configTypes[key] === 'boolean') {
settings.push(`${key}=${sanitizedValue ? 'True' : 'False'}`);
settings.push(`${key}=${value ? 'True' : 'False'}`);
} else {
settings.push(`${key}=${sanitizedValue}`);
}
Expand Down
17 changes: 16 additions & 1 deletion frontend/src/hooks/use-events-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import {
TGenericFunction,
TGenericObject
} from '../types';
import { checkForUpdates, initApp, setLoadingStatus } from '../actions/app';
import {
addSteamImage,
checkForUpdates,
initApp,
setLoadingStatus
} from '../actions/app';
import { addConsoleEntry } from '../actions/console';
import { setStatus } from '../actions/server';

Expand All @@ -34,6 +39,16 @@ const useEventsInit = () => {
useEffect(() => {
const unsubscribes: TGenericFunction[] = [];

DesktopApi.onAppEvent(
AppEvent.RETURN_STEAM_IMAGE,
(resultString: string) => {
const [steamId, imageUrl] = resultString.split('|');

addSteamImage(steamId, imageUrl);
},
unsubscribes
);

DesktopApi.onAppEvent(
AppEvent.SET_LOADING_STATUS,
(status: LoadingStatus) => {
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/hooks/use-rcon-credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { useSelector } from 'react-redux';
import { rconCredentialsSelector } from '../selectors/app';

const useRconCredentials = () => useSelector(rconCredentialsSelector);

export default useRconCredentials;
6 changes: 6 additions & 0 deletions frontend/src/hooks/use-steam-images.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { useSelector } from 'react-redux';
import { steamImagesCacheSelector } from '../selectors/app';

const useSteamImages = () => useSelector(steamImagesCacheSelector);

export default useSteamImages;
12 changes: 6 additions & 6 deletions frontend/src/screens/about/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,14 @@ const About = () => {
</p>
<p>
Licensed under the{' '}
<a
className="text-blue-500 hover:underline"
href="https://opensource.org/licenses/MIT"
target="_blank"
rel="noreferrer"
<span
className="text-blue-500 hover:underline cursor-pointer"
onClick={() =>
DesktopApi.openUrl('https://opensource.org/licenses/MIT')
}
>
MIT License
</a>
</span>
.
</p>
<p>
Expand Down
Loading

0 comments on commit 0cdd5b2

Please sign in to comment.