Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
0123785
Update dependencies
RappyTV Jun 13, 2025
a89bb71
Move icons into data directory
RappyTV Jun 13, 2025
398f626
Generate rsa keypair on first run
RappyTV Jun 13, 2025
be83f36
Stop using deprecated error status method
RappyTV Jun 13, 2025
55e18fd
Improve gift codes
RappyTV Jun 13, 2025
7736f2f
Rename jwt file
RappyTV Jun 15, 2025
f2bafde
Move generateSecureCode into crypto util file
RappyTV Jun 15, 2025
69a487e
implement staff page api
RappyTV Jun 15, 2025
276e7ac
Reduce database requests
RappyTV Jun 15, 2025
fdc1427
Use permission bitfields instead of string arrays
RappyTV Jun 15, 2025
3e4f026
Use new permissions, protect staff management routes
RappyTV Jun 15, 2025
c63a437
Add more permissions, use them in the bot
RappyTV Jun 15, 2025
a725b48
Give default role administrator permissions
RappyTV Jun 16, 2025
2c35e15
Merge pull request #221 from Global-Tags/feat/permission-bitfields
RappyTV Jun 16, 2025
15845f1
Improve player api keys
RappyTV Jun 16, 2025
69d5fc2
Improve gift code routes
RappyTV Jun 16, 2025
d43989f
db: Improve role schema
RappyTV Jun 16, 2025
521ca9e
discord notifier: Improve role logs
RappyTV Jun 16, 2025
2c4256f
roles: Improve API routes
RappyTV Jun 16, 2025
fb237be
staff: Improve routes
RappyTV Jun 16, 2025
f646e09
translations: Update translations
RappyTV Jun 16, 2025
34ef12a
Merge pull request #222 from Global-Tags/feat/database-and-route-changes
RappyTV Jun 16, 2025
3509125
roles: Fix translations
RappyTV Jun 16, 2025
e5f0902
translations: Require prefix
RappyTV Jun 17, 2025
569fd11
translations: Add validation workflow
RappyTV Jun 17, 2025
37d8250
ci: Use ts with bun
RappyTV Jun 17, 2025
639befb
ci: Use correct bun action version
RappyTV Jun 17, 2025
7ffb3c2
Merge pull request #223 from Global-Tags/feat/translation-validator
RappyTV Jun 17, 2025
d61426a
types: Improve position and icon enums
RappyTV Jun 17, 2025
daf8802
db: Create report schema
RappyTV Jun 17, 2025
baf0c1f
reports: Update player context routes
RappyTV Jun 17, 2025
3a1cda9
reports: Add basic report routes
RappyTV Jun 17, 2025
e43796a
gift codes: Update schema
RappyTV Jun 22, 2025
b3e7aca
staff: Modernize schemas
RappyTV Jun 22, 2025
ac80a89
db: Improve schemas
RappyTV Jun 22, 2025
3ff29e3
players: Improve schema
RappyTV Jun 23, 2025
574d456
mailer: Move templates into data directory
RappyTV Jun 23, 2025
36f517d
roles: Make duration nullable
RappyTV Jun 23, 2025
0b693cf
docs: Update massively
RappyTV Jun 24, 2025
262824f
deps: Update dependencies
RappyTV Jun 24, 2025
368aa5d
data: Add data directory accessor
RappyTV Jun 24, 2025
7526b89
errors: Handle body parse errors
RappyTV Jun 25, 2025
7ab0a4e
db: Fix db schema issues
RappyTV Jun 25, 2025
87e2b87
players: Merge settins routes into one
RappyTV Jun 25, 2025
5c6a1ff
watchlist: Implement methods
RappyTV Jul 22, 2025
84ba156
docs: Add missing comment to WatchlistAlert#getWatchlistPeriod
RappyTV Sep 3, 2025
14256c6
chore: Clean up
RappyTV Sep 3, 2025
d3f2a54
feat: Add application schema and routes
RappyTV Sep 3, 2025
deacf37
feat: Add account lock management
RappyTV Sep 3, 2025
1b7f30a
Merge pull request #224 from Global-Tags/feat/database-and-route-changes
RappyTV Dec 25, 2025
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
76 changes: 76 additions & 0 deletions .github/scripts/validate-translations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { sync as globSync } from 'fast-glob';
import { readFileSync } from 'fs';
import { resolve } from 'path';

const KEY_REGEX = /(['`])\$\.(\w+(?:\.\w+)+)(?=;;|\1|$)/g;
const LOCALE_FILE = resolve('locales/en_us.json');
const SRC_FILES = globSync(['src/**/*.ts'], { dot: false });

type LocaleObject = Record<string, any>;

const localeData: LocaleObject = JSON.parse(readFileSync(LOCALE_FILE, 'utf-8'));
const flatLocaleKeys = flattenKeys(localeData);

const usedKeysMap: Map<string, Set<string>> = new Map();

for (const file of SRC_FILES) {
const content = readFileSync(file, 'utf-8');
let match: RegExpExecArray | null;

while ((match = KEY_REGEX.exec(content)) !== null) {
const key = match[2];
if (!usedKeysMap.has(key)) {
usedKeysMap.set(key, new Set());
}
usedKeysMap.get(key)!.add(file);
}
}

const usedKeys = [...usedKeysMap.keys()];
const missingKeys = usedKeys.filter((key) => !flatLocaleKeys.has(key));
const unusedKeys = [...flatLocaleKeys].filter((key) => !usedKeysMap.has(key));

console.log('\n🔍 Localization Check Results:\n');

if (missingKeys.length) {
console.log('❌ Missing keys in en_us.json:');
for (const key of missingKeys) {
const files = [...(usedKeysMap.get(key) ?? [])];
for (const file of files) {
console.log(` - ${key} (${file})`);
}
}
} else {
console.log('✅ No missing keys.');
}

if (unusedKeys.length) {
console.log('\n⚠️ Unused keys in en_us.json:');
for (const key of unusedKeys) {
console.log(` - ${key}`);
}
} else {
console.log('✅ No unused keys.');
}

if (missingKeys.length || unusedKeys.length) {
process.exit(1);
}

function flattenKeys(obj: LocaleObject, prefix = ''): Set<string> {
const keys = new Set<string>();

for (const key in obj) {
const fullKey = prefix ? `${prefix}.${key}` : key;

if (typeof obj[key] === 'object' && obj[key] !== null) {
for (const subKey of flattenKeys(obj[key], fullKey)) {
keys.add(subKey);
}
} else {
keys.add(fullKey);
}
}

return keys;
}
29 changes: 29 additions & 0 deletions .github/workflows/validate-translations.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Validate Localization Keys

on:
push:
paths:
- 'src/**/*.ts'
- 'locales/en_us.json'
- '.github/scripts/validate-translations.ts'
- '.github/workflows/validate-translations.yml'
pull_request:

jobs:
validate-translations:
runs-on: ubuntu-latest

steps:
- name: Checkout repo
uses: actions/checkout@v4

- name: Set up bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Install dependencies
run: bun i fast-glob

- name: Run localization key check
run: bun .github/scripts/validate-translations.ts
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
icons
data/*
!data/mail
node_modules
.venv
.DS_Store
Expand Down
80 changes: 40 additions & 40 deletions bun.lock

Large diffs are not rendered by default.

File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
31 changes: 19 additions & 12 deletions locales/en_us.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,25 @@
"noTag": "Please set a tag first!",
"playerNoTag": "This player does not have a tag!",
"playerBanned": "This player is banned!",
"malformedAuthHeader": "You've entered a malformed authorization header!",
"noIcon": "The icon was not found!"
"noIcon": "The icon was not found!",
"invalid_bitfield": "You provided an invalid bitfield!"
},
"gift_codes": {
"not_found": "Gift code not found!",
"already_redeemed": "You have already redeemed this gift code!",
"already_have_role": "You already have the role!",
"created": "The gift code was successfully created!",
"deleted": "The gift code was successfully deleted!",
"redeemed_temporarily": "You have received the \"<role>\" role temporarily!",
"redeemed_permanently": "You have received the \"<role>\" role permanently!"
},
"api_keys": {
"not_found": "The API key was not found!",
"already_exists": "An API key with this name already exists!",
"created": "The API key was successfully created!",
"regenerated": "The API key was successfully regenerated!",
"deleted": "The API key was successfully deleted!"
},
"ban": {
"not_found": "Ban not found!",
"already_banned": "This player is already banned!",
"not_banned": "This player is not banned!",
"no_reason": "No reason provided",
"banned": "The player was successfully banned!",
"unbanned": "The player was successfully unbanned!"
},
Expand Down Expand Up @@ -85,7 +80,7 @@
"alreadyReported": "You've already reported this player's tag!",
"invalidReason": "You have to provide a valid reason!",
"validation": "Keep the \"reason\" field between <min> and <max> characters.",
"success": "The player was successfully reported!",
"success": "You have successfully created the report with the ID #<id>!",
"immune": "This player cannot be reported!",
"delete": {
"not_found": "The report was not found!",
Expand All @@ -102,6 +97,22 @@
"admin": "The player's tag was successfully updated!"
}
},
"staff": {
"categories": {
"not_found": "The category was not found!",
"delete": {
"success": "The category was successfully deleted!"
}
},
"members": {
"not_found": "Unknown staff member!",
"invalid_uuid": "The UUID is not valid!",
"already_exists": "This UUID is already assigned to a staff member!",
"delete": {
"success": "The staff member was successfully deleted!"
}
}
},
"referral": {
"self": "You can't refer yourself!",
"alreadyReferred": "You have already marked a player as your referrer!",
Expand Down Expand Up @@ -151,10 +162,6 @@
},
"roles": {
"not_found": "The role was not found!",
"create": {
"already_exists": "The role '<role>' already exists!",
"success": "The role was successfully created!"
},
"delete": {
"success": "The role was successfully deleted!"
}
Expand Down
16 changes: 8 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,26 @@
"dependencies": {
"@elysiajs/cors": "^1.3.3",
"@elysiajs/swagger": "1.3.0",
"@sentry/bun": "^9.28.1",
"axios": "^1.9.0",
"@sentry/bun": "^9.31.0",
"axios": "^1.10.0",
"chalk": "^5.4.1",
"change-case": "^5.4.4",
"croner": "^9.0.0",
"discord.js": "^14.19.3",
"croner": "^9.1.0",
"discord.js": "^14.20.0",
"dotenv": "^16.5.0",
"elysia": "1.3.4",
"elysia": "1.3.5",
"elysia-ip": "^1.0.10",
"jsonwebtoken": "^9.0.2",
"minimist": "^1.2.8",
"moment": "^2.30.1",
"mongoose": "^8.15.1",
"mongoose": "^8.16.0",
"ms": "^2.1.3",
"nodemailer": "^7.0.3",
"sharp": "^0.34.2"
},
"devDependencies": {
"@types/bun": "^1.2.15",
"@types/jsonwebtoken": "^9.0.9",
"@types/bun": "^1.2.17",
"@types/jsonwebtoken": "^9.0.10",
"@types/minimist": "^1.2.5",
"@types/nodemailer": "^6.4.17"
}
Expand Down
21 changes: 10 additions & 11 deletions src/auth/AuthProvider.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { existsSync, readdirSync } from "fs";
import { join } from "path";
import Logger from "../libs/Logger";
import players from "../database/schemas/players";
import { Permission } from "../types/Permission";
import { Player, PlayerDocument } from "../database/schemas/Player";
import { stripUUID } from "../libs/game-profiles";

export type SessionData = {
uuid: string | null,
equal: boolean,
hasPermission: (permission: Permission) => boolean
player: PlayerDocument | null,
self: boolean
}

export default abstract class AuthProvider {
Expand All @@ -20,15 +19,15 @@ export default abstract class AuthProvider {
}

public async getSession(token: string, uuid?: string | null): Promise<SessionData> {
const tokenUuid = await this.getUUID(token);
const tokenUUID = await this.getUUID(token);
if(uuid) uuid = stripUUID(uuid);
if(!tokenUuid) return { uuid: tokenUuid, equal: tokenUuid == uuid, hasPermission: (permission: Permission) => false };
const data = await players.findOne({ uuid: tokenUuid });
if(!data) return { uuid: tokenUuid, equal: tokenUuid == uuid, hasPermission: (permission: Permission) => false };
if(!tokenUUID) return { uuid: null, player: null, self: false };
const data = await Player.findOne({ uuid: tokenUUID });
if(!data) return { uuid: tokenUUID, player: null, self: tokenUUID == uuid };
return {
uuid: tokenUuid,
equal: uuid == tokenUuid,
hasPermission: (permission: Permission) => data.hasPermission(permission)
uuid: tokenUUID,
player: data,
self: uuid == tokenUUID
}
}
public abstract getUUID(token: string): Promise<string | null>;
Expand Down
4 changes: 2 additions & 2 deletions src/auth/providers/ApiKeyProvider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import players from "../../database/schemas/players";
import { Player } from "../../database/schemas/Player";
import AuthProvider from "../AuthProvider";

export default class ApiKeyProvider extends AuthProvider {
Expand All @@ -8,7 +8,7 @@ export default class ApiKeyProvider extends AuthProvider {

async getUUID(token: string): Promise<string | null> {
token = AuthProvider.trimTokenType(token);
const player = await players.findOne({ 'api_keys.key': token });
const player = await Player.findOne({ 'api_keys.key': token });
if(!player) return null;
const usedKey = player.api_keys.find(key => key.key === token);
if(usedKey) {
Expand Down
4 changes: 2 additions & 2 deletions src/bot/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const buttons = new Collection<string, Button>();
export const menus = new Collection<string, SelectMenu>();
export const modals = new Collection<string, Modal>();

(async () => {
export async function registerFeatures() {
const eventDir = join(__dirname, 'events');
const commandDir = join(__dirname, 'commands');
const buttonDir = join(__dirname, 'buttons');
Expand Down Expand Up @@ -73,7 +73,7 @@ export const modals = new Collection<string, Modal>();

modals.set(modal.id, modal);
});
})();
};

export const spawn = () => client.login(config.discordBot.token);
export const destroy = () => client.destroy();
Expand Down
37 changes: 12 additions & 25 deletions src/bot/buttons/Actions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ButtonInteraction, Message, GuildMember, ButtonStyle, ButtonBuilder, ActionRowBuilder, EmbedBuilder, MessageFlags } from "discord.js";
import Button from "../structs/Button";
import { colors } from "../bot";
import players, { Player } from "../../database/schemas/players";
import players, { PlayerDocument } from "../../database/schemas/Player";
import { Permission } from "../../types/Permission";
import { stripUUID, uuidRegex } from "../../libs/game-profiles";
import { config } from "../../libs/config";
Expand All @@ -19,7 +19,7 @@ export default class ActionsButton extends Button {
});
}

async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) {
async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) {
if(!player.canManagePlayers()) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ You\'re not allowed to perform this action!')], flags: [MessageFlags.Ephemeral] });
const uuid = interaction.customId.split('_')[1] || message.embeds[0]?.author?.name || message.embeds[0]?.fields[0]?.value.match(uuidRegex)?.[0];
if(!uuid) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] });
Expand All @@ -33,25 +33,25 @@ export default class ActionsButton extends Button {
const connections: InfoEntry[] = [];

general.push({ name: 'Tag', value: `\`${!!target.tag ? stripColors(target.tag) : '-'}\`` });
if(player.hasPermission(Permission.ManageTags)) {
if(player.hasPermission(Permission.ManagePlayerTags)) {
general.push({ name: 'Tag history', value: `\`${target.history.length}\`` });
moderation.push({ name: 'Tag clears', value: `\`${target.clears.filter(({ type }) => type == 'tag').length}\`` });
moderation.push({ name: 'Icon clears', value: `\`${target.clears.filter(({ type }) => type == 'icon').length}\`` });
}
general.push({ name: 'Language', value: `\`${target.last_language}\`` });
general.push({ name: 'Hidden role icon', value: `\`${target.hide_role_icon ? '✅' : '❌'}\`` });
general.push({ name: 'Referred by', value: `${(await (await target.getReferrer())?.getGameProfile())?.getFormattedHyperlink() || '`-`'}` });
if(player.hasPermission(Permission.ManageApiKeys)) general.push({ name: 'API Keys', value: `\`${target.api_keys.length}\`` });
if(player.hasPermission(Permission.ViewApiKeys)) general.push({ name: 'API Keys', value: `\`${target.api_keys.length}\`` });

if(player.hasPermission(Permission.ManageNotes)) moderation.push({ name: 'Notes', value: `\`${target.notes.length}\`` });
if(player.hasPermission(Permission.ManageReports)) moderation.push({ name: 'Reports', value: `\`${target.reports.length}\`` });
if(player.hasPermission(Permission.ManageWatchlist)) moderation.push({ name: 'Is on watchlist', value: `\`${target.watchlist ? '✅' : '❌'}\`` });
if(player.hasPermission(Permission.ManageBans)) {
if(player.hasPermission(Permission.ViewNotes)) moderation.push({ name: 'Notes', value: `\`${target.notes.length}\`` });
if(player.hasPermission(Permission.ViewReports)) moderation.push({ name: 'Reports', value: `\`${target.reports.length}\`` });
if(player.hasPermission(Permission.ManageWatchlistEntries)) moderation.push({ name: 'Is on watchlist', value: `\`${target.watchlist ? '✅' : '❌'}\`` });
if(player.hasPermission(Permission.ViewBans)) {
moderation.push({ name: 'Bans', value: `\`${target.bans.length}\`` });
moderation.push({ name: 'Active ban', value: `\`${target.isBanned() ? '✅' : '❌'}\`` });
}

if(player.hasPermission(Permission.ManageConnections)) {
if(player.hasPermission(Permission.ViewConnections)) {
connections.push({ name: 'Discord', value: target.connections.discord.id ? `[\`${target.connections.discord.id}\`](discord://-/users/${target.connections.discord.id})` : '`❌`' });
connections.push({ name: 'Email', value: target.connections.email.address ? config.discordBot.notifications.accountConnections.hideEmails ? hiddenConnectionLabel : `\`${target.connections.email.address}\`` : '`❌`' });
}
Expand All @@ -73,29 +73,16 @@ export default class ActionsButton extends Button {
new ButtonBuilder()
.setLabel('Account')
.setCustomId(`manageAccount_${target.uuid}`)
.setStyle(ButtonStyle.Primary)
.setDisabled(![
Permission.ManageConnections,
Permission.ManageRoles,
Permission.ManageApiKeys,
Permission.ManageTags
].some((permission) => player.hasPermission(permission))),
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setLabel('Moderation')
.setCustomId(`moderateAccount_${target.uuid}`)
.setStyle(ButtonStyle.Primary)
.setDisabled(![
Permission.ManageBans,
Permission.ManageNotes,
Permission.ManageReports,
Permission.ManageWatchlist,
Permission.ManageTags
].some((permission) => player.hasPermission(permission))),
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setLabel('Manage Tag')
.setCustomId(`manageTag_${target.uuid}`)
.setStyle(ButtonStyle.Primary)
.setDisabled(!player.hasPermission(Permission.ManageTags))
.setDisabled(!player.hasPermission(Permission.ManagePlayerTags))
);

interaction.reply({ embeds: [embed], components: [row], flags: [MessageFlags.Ephemeral] });
Expand Down
Loading
Loading