Skip to content

Commit

Permalink
Custom server notifications (#50)
Browse files Browse the repository at this point in the history
* work done so far on notifications

* somewhat improved notifications logic

* update constants

* minimum game version check and some other updates to notifications

* update deps

* some bug fixes

* fix linting issues
  • Loading branch information
touhidurrr authored Nov 24, 2024
1 parent 9f1c86b commit 4c25fc8
Show file tree
Hide file tree
Showing 14 changed files with 543 additions and 120 deletions.
Binary file modified bun.lockb
Binary file not shown.
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "uncivserver.xyz",
"version": "4.8.0",
"version": "4.9.0",
"description": "An Open Source, Free to Play, Unciv Multiplayer Server written in Javascript",
"author": "Md. Touhidur Rahman",
"license": "BSD-3-Clause",
Expand Down Expand Up @@ -36,18 +36,19 @@
"@elysiajs/static": "1.0.3",
"bytes": "^3.1.2",
"date-fns": "^4.1.0",
"discord-api-types": "^0.37.105",
"discord-api-types": "^0.37.107",
"elysia": "^1.1.25",
"ioredis": "^5.4.1",
"lru-cache": "^11.0.2",
"mongodb": "^6.10.0",
"node-cache": "^5.1.2"
"mongodb": "^6.11.0",
"node-cache": "^5.1.2",
"random": "^5.1.1"
},
"devDependencies": {
"@elysiajs/eden": "^1.1.3",
"@types/bun": "^1.1.13",
"@types/bytes": "^3.1.4",
"prettier": "^3.3.3",
"typescript": "^5.6.3"
"typescript": "^5.7.2"
}
}
13 changes: 13 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import bytes from 'bytes';
import type { APIEmbed } from 'discord-api-types/v10';

// utils
export const isProduction = process.env.NODE_ENV === 'production';
Expand Down Expand Up @@ -34,3 +35,15 @@ export const GAME_ID_REGEX = /^[\da-f]{8}-([\da-f]{4}-){3}[\da-f]{12}(_Preview)?
export const START_TEST_TIMEOUT = 30_000;
export const START_TEST_FETCH_TIMEOUT = 5_000;
export const TEST_GAME_ID = '00000000-0000-0000-0000-000000000000';

// support
export const SUPPORT_CHANNEL_NAME = 'Buy Me A Coffee';
export const SUPPORT_URL = 'https://buymeacoffee.com/touhidurrr';
export const SUPPORT_MESSAGE = `Enjoying UncivServer.xyz? Consider supporting the project at https://uncivserver.xyz/support !`;
export const SUPPORT_EMBED_MESSAGE = `Enjoying **UncivServer.xyz**? Consider supporting the project at [${SUPPORT_CHANNEL_NAME}](${SUPPORT_URL})!`;

export const SUPPORT_EMBED: Readonly<APIEmbed> = Object.freeze({
title: 'Support the Project',
description: SUPPORT_EMBED_MESSAGE,
color: 0xffdd00,
});
10 changes: 6 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Elysia } from 'elysia';
import { staticPlugin } from '@elysiajs/static';
import { filesRoute } from '@routes/files';
import { infoPlugin } from '@routes/info';
import {
DEFAULT_HOST,
DEFAULT_PORT,
isDevelopment,
MAX_CONTENT_LENGTH,
MIN_CONTENT_LENGTH,
SUPPORT_URL,
} from '@constants';
import { staticPlugin } from '@elysiajs/static';
import { filesRoute } from '@routes/files';
import { infoPlugin } from '@routes/info';
import { Elysia } from 'elysia';

const port = process.env.PORT ?? DEFAULT_PORT;
const hostname = process.env.HOST ?? DEFAULT_HOST;
Expand All @@ -25,6 +26,7 @@ export const app = new Elysia()
.use(staticPlugin({ prefix: '/' }))
.use(infoPlugin)
.get('/isalive', true)
.all('/support', ctx => ctx.redirect(SUPPORT_URL, 303))
.use(filesRoute)
.listen({ port, hostname });

Expand Down
46 changes: 46 additions & 0 deletions src/lib/generateRandomNotification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { SUPPORT_MESSAGE } from '@constants';
import type { Notification, UncivJSON } from '@localTypes/unciv';
import random from 'random';

const defaultNotification = 'Welcome to UncivServer.xyz!';
const defaultNotificationIcon = 'NotificationIcons/RobotArm';

const notificationIcons = [defaultNotificationIcon, 'NotificationIcons/ServerRack'];

const randomNotifications = [
`${defaultNotification} Again!`,
"It's your turn!",
'Time to make some moves!',
'Hi! Server here. Have a nice day!',
"Don't forget to press 'Next Turn' when you're done!",
"Hi, there! @touhidurrr here from UncivServer.xyz. Don't forget I might be watching your every move...",
"Let's speed through some turns!",
];

const supportNotificationIcons = ['NotificationIcons/DollarSign'];

const randomSupportNotifications = [SUPPORT_MESSAGE];

const supportNotificationsProbability = 0.2;

export function generateRandomNotification(gameData: UncivJSON): Notification {
let text = defaultNotification;
let icons = [defaultNotificationIcon];

if (gameData.turns) {
if (random.float() < supportNotificationsProbability) {
text = random.choice(randomSupportNotifications)!;
icons[0] = random.choice(supportNotificationIcons)!;
} else {
text = random.choice(randomNotifications)!;
icons[0] = random.choice(notificationIcons)!;
}
}

return {
text,
icons,
category: 'General',
actions: [],
};
}
2 changes: 1 addition & 1 deletion src/lib/getAppBaseURL.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { hostname } from 'node:os';
import { DEFAULT_PORT } from '@constants';
import { hostname } from 'node:os';

export function getAppBaseURL() {
return `http://${hostname()}:${DEFAULT_PORT}` as const;
Expand Down
4 changes: 3 additions & 1 deletion src/lib/getRandomColor.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import random from 'random';

export function getRandomColor() {
return Math.floor(0x1000000 * Math.random());
return random.int(0x000000, 0xffffff);
}
1 change: 1 addition & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { generateRandomNotification } from './generateRandomNotification';
export { getAppBaseURL } from './getAppBaseURL';
export { getRandomBase64String } from './getRandomBase64String';
export { getRandomColor } from './getRandomColor';
Expand Down
2 changes: 1 addition & 1 deletion src/routes/files/delete.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { db } from '@services/mongodb';
import cache from '@services/cache';
import { db } from '@services/mongodb';
import type { Elysia } from 'elysia';

export const deleteFile = (app: Elysia) =>
Expand Down
49 changes: 42 additions & 7 deletions src/routes/files/put.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import type { Elysia } from 'elysia';
import { db } from '@services/mongodb';
import { syncGame } from '@services/sync';
import { generateRandomNotification } from '@lib';
import type { UncivJSON } from '@localTypes/unciv';
import cache from '@services/cache';
import { isDiscordTokenValid, sendNewTurnNotification } from '@services/discord';
import { db } from '@services/mongodb';
import { syncGame } from '@services/sync';
import { pack, unpack } from '@services/uncivGame';
import { type Elysia } from 'elysia';

export const putFile = (app: Elysia) =>
app.put(
// ctx.game should contain parsed game data
// ctx.game is null if parsing fails
app.state('game', null as UncivJSON | null).put(
'/:gameId',
async ({ body, params: { gameId } }) => {
// for performance reasons, just store the file in cache and return ok
Expand All @@ -17,7 +22,7 @@ export const putFile = (app: Elysia) =>
// afterHandle is called after the route handler is executed but before the response is sent
// do not use any synchronous code here as it will block the response
// this notice is only valid for this file, not for the entire project
afterHandle: async ({ body, params: { gameId } }) => {
afterHandle: async ({ body, params: { gameId }, store: { game } }) => {
// save on mongodb
db.UncivServer.updateOne(
{ _id: gameId },
Expand All @@ -29,8 +34,38 @@ export const putFile = (app: Elysia) =>
syncGame(gameId, body as string);

// send turn notification
if (isDiscordTokenValid && gameId.endsWith('_Preview')) {
sendNewTurnNotification(body as string);
if (game !== null && isDiscordTokenValid && gameId.endsWith('_Preview')) {
sendNewTurnNotification(game!);
}
},

// used for injecting notifications
// in case an injection is possible, we need to repack the body to update it
transform: ctx => {
if (ctx.params.gameId.endsWith('_Preview')) return;
// need to think of a better way of doing this
// ideally there should be no try-catch here
// if parsing fails then we should just let it happen
// this way bad game data will not be saved
// but current tests are not good enough to ensure this
try {
ctx.store.game = unpack(ctx.body as string);
if (
ctx.store.game.version.number >= 4 &&
ctx.store.game.version.createdWith.number > 1074
) {
const targetCiv = ctx.store.game.civilizations.find(
civ => civ.civName == ctx.store.game!.currentPlayer
);
if (targetCiv) {
const newNotification = generateRandomNotification(ctx.store.game);
if (targetCiv.notifications) targetCiv.notifications.push(newNotification);
else targetCiv.notifications = [newNotification];
ctx.body = pack(ctx.store.game);
}
}
} catch (err) {
console.error(`[PutBodyTransformError]:\n`, err);
}
},
}
Expand Down
24 changes: 4 additions & 20 deletions src/services/discord.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { SUPPORT_EMBED } from '@constants';
import { REST } from '@discordjs/rest';
import { getRandomColor } from '@lib';
import type { UncivJSON } from '@localTypes/unciv';
import {
Routes,
type RESTPostAPIChannelMessageJSONBody,
type RESTPostAPIChannelMessageResult,
type RESTPostAPICurrentUserCreateDMChannelResult,
} from 'discord-api-types/rest/v10';
import { db } from './mongodb';
import type { UncivJSON } from '@localTypes/unciv';
import type { APIEmbed } from 'discord-api-types/v10';
import { unpack } from './uncivGame';

const DISCORD_TOKEN = process.env.DISCORD_TOKEN;

Expand Down Expand Up @@ -49,22 +48,7 @@ async function getDMChannel(discordId: string) {
return res.id;
}

const supportEmbed: Readonly<APIEmbed> = Object.freeze({
title: 'Support the Project',
description:
'Enjoying **UncivServer.xyz**? Consider supporting the project at [Buy Me A Coffee](https://buymeacoffee.com/touhidurrr)',
color: 0xffdd00,
});

export async function sendNewTurnNotification(gameData: string) {
let game: UncivJSON;
try {
game = unpack(gameData);
} catch (err) {
console.error('[TurnNotifier] error parsing game data:', err);
return;
}

export async function sendNewTurnNotification(game: UncivJSON) {
const { turns, gameId, civilizations, currentPlayer, gameParameters } = game;

// find currentPlayer's ID
Expand Down Expand Up @@ -149,7 +133,7 @@ export async function sendNewTurnNotification(gameData: string) {
},
],
},
supportEmbed,
SUPPORT_EMBED,
],
}).catch(err => {
console.error('[TurnNotifier] error sending notification:', {
Expand Down
Loading

0 comments on commit 4c25fc8

Please sign in to comment.