Skip to content

Commit

Permalink
feat: 🚧 works on initial version
Browse files Browse the repository at this point in the history
  • Loading branch information
swiizyy committed Apr 11, 2024
1 parent 4282a86 commit e0dfdab
Show file tree
Hide file tree
Showing 18 changed files with 768 additions and 127 deletions.
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
{
"name": "@birthdayybot/util",
"version": "1.0.0",
"author": "@nikolaischunk",
"author": "@swiizyy",
"license": "Apache-2.0",
"private": true,
"main": "dist/main.js",
"type": "module",
"imports": {
"#api/*": "./dist/api/*.js",
"#lib/*": "./dist/lib/*.js"
},
"scripts": {
Expand All @@ -25,7 +26,7 @@
"@discordjs/builders": "^1.6.3",
"@discordjs/collection": "^1.5.1",
"@discordjs/core": "^0.6.0",
"@prisma/client": "^5.0.0",
"@prisma/client": "^5.12.1",
"@sapphire/async-queue": "^1.5.0",
"@sapphire/duration": "^1.1.0",
"@sapphire/result": "^2.6.4",
Expand All @@ -37,6 +38,7 @@
"@skyra/shared-http-pieces": "^1.0.3",
"@skyra/start-banner": "^2.0.0",
"discord-api-types": "^0.37.43",
"fastify": "^4.26.2",
"gradient-string": "^2.0.2",
"limax": "^4.1.0",
"tslib": "^2.6.0"
Expand All @@ -59,7 +61,7 @@
"lint-staged": "^13.2.3",
"prettier": "^2.8.8",
"pretty-quick": "^3.1.3",
"prisma": "^5.0.0",
"prisma": "^5.12.1",
"typescript": "^5.1.6"
},
"resolutions": {
Expand Down
9 changes: 9 additions & 0 deletions prisma/migrations/20240411105032_inital/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- CreateTable
CREATE TABLE "guild" (
"id" BIGINT NOT NULL,
"maximum_announcement_length" SMALLINT NOT NULL DEFAULT 0,
"maximum_giveable_birthday_roles" SMALLINT NOT NULL DEFAULT 1,
"maximum_birthday_list_amount" SMALLINT NOT NULL DEFAULT 10,

CONSTRAINT "guild_pkey" PRIMARY KEY ("id")
);
3 changes: 3 additions & 0 deletions prisma/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
106 changes: 27 additions & 79 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -1,87 +1,35 @@
generator client {
provider = "prisma-client-js"
provider = "prisma-client-js"
}

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

model User {
// The user's Discord ID
id BigInt @id
// Whether or not they have currently subscribed to a premium plan
premium Boolean @default(false)
// The tier of the user
tier Tiers @default(Free)
// Maximum usage guild Premium
// @tier 1 (Premium)
// @tier 3 (Supporter)
maximumPremium Int @default(0) @map("maximum_premium") @db.SmallInt
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
transactions Transaction[]
guild Guild[]
@@index([id])
@@map("user")
provider = "postgresql"
url = env("DATABASE_URL")
}

model Guild {
// The guild's Discord ID
id BigInt @id
// The guild's name on Discord
name String? @db.Text
// The Discord ID of the user who activated the premium
userId BigInt @map("user_id")
// The tier of the guild
tier Tiers @default(Free)
// Whether or not they have currently subscribed to a premium plan
premium Boolean @default(false)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: NoAction)
@@index([id])
@@map("guild")
}

model Transaction {
// This is the transaction ID from the payment provider
id Int @id
// The user's Discord ID
userId BigInt @map("user_id")
// The amount of money that was paid
amount Int
// The tier of the transaction
tier Tiers @default(Free)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id])
@@index([id])
@@map("transaction")
}

enum Tiers {
Free
Premium
Supporter
CustomBot
// The guild's Discord ID
id BigInt @id
/// The maximum amount of characters for the announcement message
/// @tier 1 (128)
/// @tier 2 (256)
/// @tier 3 (512)
/// @default 0
maximumAnnouncementLength Int @default(0) @map("maximum_announcement_length") @db.SmallInt
/// The maximum amount of roles that the bot can give to the user
/// @tier 1 (3)
/// @tier 2 (5)
/// @tier 3 (10)
/// @default 1
maximumGiveableBirthdayRoles Int @default(1) @map("maximum_giveable_birthday_roles") @db.SmallInt
/// The maximum birthday amount that can be seen in advance with the /birthdayy list command
/// @tier 1 (30)
/// @tier 2 (50)
/// @tier 3 (100)
/// @default 10
maximumBirthdayListAmount Int @default(10) @map("maximum_birthday_list_amount") @db.SmallInt
@@map("guild")
}
13 changes: 8 additions & 5 deletions src/.env
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ SENTRY_DSN=
HTTP_ADDRESS=0.0.0.0
HTTP_PORT=3000

# The Redis server configuration
REDIS_PORT=8287
REDIS_PASSWORD=redis
REDIS_HOST=localhost
REDIS_DB=0
API_ADDRESS=0.0.0.0
API_PORT=3001

CLIENT_OWNERS="696324357940838492 267614892821970945"

INTERNAL_API_TOKEN=

# The database URL for Prisma, needs to be copied in a local `.env` at the root
# for Prisma's operations to work.
DATABASE_URL="postgresql://user:password@localhost:5432/database?schema=public"
4 changes: 4 additions & 0 deletions src/api/routes/_load.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import '#api/routes/guilds/[...id]';
import '#api/routes/index';
import "#api/routes/webhooks/vote";

45 changes: 45 additions & 0 deletions src/api/routes/guilds/[...id].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { container } from '@sapphire/pieces';
import { isNullish, isNullishOrEmpty } from '@sapphire/utilities';

container.server.route({
url: '/guilds/:id',
method: 'GET',
handler: async (request, reply) => {
if (isNullishOrEmpty(request.headers.authorization)) {
return reply.code(401).send({ success: false, message: 'Missing authorization' });
}

const mappings = getMappings(request.headers.authorization);
if (!mappings) {
return reply.code(403).send({ success: false, message: 'Missing access to this resource' });
}

if (typeof request.params !== 'object' || isNullish(request.params) || !('id' in request.params)) {
return reply.code(400).send({ success: false, message: 'Missing parameters' });
}

let id: bigint;
try {
id = BigInt(request.params.id as string);
} catch {
return reply.code(400).send({ success: false, message: 'Invalid Guild ID' });
}

const data = await container.prisma.guild.findFirst({ where: { id }, select: mappings.properties });
return reply.code(200).send(data ?? mappings.defaults);
}
});

const Mappings = {
properties: { maximumAnnouncementLength: true, maximumGiveableBirthdayRoles: true, maximumBirthdayListAmount: true},
defaults: { maximumAnnouncementLength: 0, maximumGiveableBirthdayRoles: 1, maximumBirthdayListAmount: 10}
} as const;

function getMappings(token: string) {
switch (token) {
case process.env.INTERNAL_API_TOKEN:
return Mappings;
default:
return null;
}
}
7 changes: 7 additions & 0 deletions src/api/routes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { container } from '@sapphire/pieces';

container.server.route({
url: '/',
method: 'GET',
handler: () => ({ data: 'Hello world' })
});
Empty file added src/api/routes/webhooks/vote.ts
Empty file.
118 changes: 118 additions & 0 deletions src/commands/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { SlashCommandIntegerOption, SlashCommandStringOption } from '@discordjs/builders';
import { codeBlock, isNullish } from '@sapphire/utilities';
import { envParseArray } from '@skyra/env-utilities';
import { Command, RegisterCommand, RegisterSubCommand } from '@skyra/http-framework';
import { blue, bold, red, yellow } from '@skyra/logger';
import { MessageFlags, PermissionFlagsBits } from 'discord-api-types/v10';

@RegisterCommand((builder) =>
builder
.setName('config')
.setDescription("Manage a guild's features")
.setDMPermission(false)
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
)
export class UserCommand extends Command {
@RegisterSubCommand((builder) => builder.setName('get').setDescription("Gets a guild's features").addStringOption(getGuildOption))
public async get(interaction: Command.ChatInputInteraction, options: Options) {
if (!UserCommand.ClientOwners.includes(interaction.user.id)) {
return interaction.reply({ content: 'You cannot use this command.', flags: MessageFlags.Ephemeral });
}

const data = await this.container.prisma.guild.findFirst({ where: { id: BigInt(options.guild) } });
if (isNullish(data)) {
return interaction.reply({ content: 'There is no data recorded for that guild.', flags: MessageFlags.Ephemeral });
}

const lines = [
`${bold('Guild ID')}: ${bold(blue(data.id.toString().padStart(19, ' ')))}`,
`${bold('Maximum Announcement Length')}: ${formatRange(data.maximumAnnouncementLength, 128, 512)}`,
`${bold('Maximum Giveable Birthday Roles')}: ${formatRange(data.maximumGiveableBirthdayRoles, 1, 5)}`,
`${bold('Maximum Birthday List Amount')}: ${formatRange(data.maximumBirthdayListAmount, 10, 50)}`
];
return interaction.reply({ content: codeBlock('ansi', lines.join('\n')), flags: MessageFlags.Ephemeral });
}

@RegisterSubCommand((builder) =>
builder
.setName('set')
.setDescription("Updates a guild's features")
.addStringOption(getGuildOption)
.addIntegerOption(getIntegerOption(128, 512, 'maximum-announcement-length', 'The maximum announcement length'))
.addIntegerOption(getIntegerOption(1, 5, 'maximum-giveable-birthday-roles', 'The maximum giveable birthday roles'))
.addIntegerOption(getIntegerOption(10, 50, 'maximum-birthday-list-amount', 'The maximum birthday list amount'))
)
public async set(interaction: Command.ChatInputInteraction, options: SetOptions) {
if (!UserCommand.ClientOwners.includes(interaction.user.id)) {
return interaction.reply({ content: 'You cannot use this command.', flags: MessageFlags.Ephemeral });
}

const id = BigInt(options.guild);
const data = {
maximumAnnouncementLength: options['maximum-announcement-length'],
maximumGiveableBirthdayRoles: options['maximum-giveable-birthday-roles'],
maximumBirthdayListAmount: options['maximum-birthday-list-amount']
};
try {
await this.container.prisma.guild.upsert({
where: { id },
create: { id, ...data },
update: data
});
return interaction.reply({ content: 'Updated.', flags: MessageFlags.Ephemeral });
} catch (error) {
this.container.logger.error(error);

return interaction.reply({
content: 'I was not able to update the configuration, please check my logs and/or try again later.',
flags: MessageFlags.Ephemeral
});
}
}

@RegisterSubCommand((builder) => builder.setName('reset').setDescription("Resets a guild's features").addStringOption(getGuildOption))
public async reset(interaction: Command.ChatInputInteraction, options: Options) {
if (!UserCommand.ClientOwners.includes(interaction.user.id)) {
return interaction.reply({ content: 'You cannot use this command.', flags: MessageFlags.Ephemeral });
}

const data = await this.container.prisma.guild.delete({ where: { id: BigInt(options.guild) } });
const content = isNullish(data) ? 'There is no data recorded for that guild.' : "Successfully deleted the specified guild's data.";
return interaction.reply({ content, flags: MessageFlags.Ephemeral });
}

private static readonly ClientOwners = envParseArray('CLIENT_OWNERS');
}

interface Options {
guild: string;
}

interface SetOptions extends Options {
'maximum-announcement-length': number;
'maximum-giveable-birthday-roles': number;
'maximum-birthday-list-amount': number;
}

function getGuildOption() {
return new SlashCommandStringOption()
.setName('guild')
.setDescription('The ID of the guild to manage')
.setMinLength(17)
.setMaxLength(19)
.setRequired(true);
}

function getIntegerOption(min: number, max: number, name: string, description: string) {
return new SlashCommandIntegerOption().setName(name).setDescription(`${description} (${min}-${max})`).setMinValue(min).setMaxValue(max);
}

function formatRange(value: number, min: number, max: number) {
if (value === min) return `${blue(format(value))}${red(format(max))}`;
if (value === max) return `${yellow(format(min))}${blue(format(value))}`;
return `${yellow(format(min))}${blue(format(value))}${red(format(max))}`;
}

function format(value: number) {
return value.toString().padStart(3, ' ');
}
23 changes: 16 additions & 7 deletions src/lib/setup/all.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import { setup as envRun } from '@skyra/env-utilities';
import { setup as envSetup } from '@skyra/env-utilities';
import { initializeSentry, setInvite, setRepository } from '@skyra/shared-http-pieces';


import '@skyra/shared-http-pieces/register';

envSetup(new URL('../../../src/.env', import.meta.url));
setInvite('948377113457745990', '326417868864');
setRepository('https://github.com/BirthdayyBot/util');
initializeSentry();


import '#lib/setup/api';
import '#lib/setup/fastify';
import '#lib/setup/logger';
import '#lib/setup/prisma';
import '@skyra/shared-http-pieces/register';

export function setup() {
envRun(new URL('../../../src/.env', import.meta.url));

setRepository('iriss');
setInvite('948377113457745990', '326417868864');
initializeSentry();

export async function setup() {
// Load all routes
await import('#api/routes/_load');
}
Loading

0 comments on commit e0dfdab

Please sign in to comment.