Compare commits

...

8 commits

Author SHA1 Message Date
Ethan Lane 92ef0041ff v0.1.0
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-03 21:12:59 +01:00
Ethan Lane 499ad6faa9 Set to 0.0.1 2023-09-03 21:12:47 +01:00
Ethan Lane f8af969104 Update client ids
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-03 21:03:03 +01:00
Ethan Lane 8b3fb062f0 Add deployment scripts (#18)
All checks were successful
continuous-integration/drone/push Build is passing
#3

Reviewed-on: https://gitea.vylpes.xyz/External/card-drop/pulls/18
Co-authored-by: Ethan Lane <ethan@vylpes.com>
Co-committed-by: Ethan Lane <ethan@vylpes.com>
2023-09-03 20:52:55 +01:00
Ethan Lane 58d1541e47 feature/5-drop-command (#17)
#5

Reviewed-on: https://gitea.vylpes.xyz/External/card-drop/pulls/17
Co-authored-by: Ethan Lane <ethan@vylpes.com>
Co-committed-by: Ethan Lane <ethan@vylpes.com>
2023-09-03 20:27:29 +01:00
Ethan Lane 51d97bacd5 Create timer function to initialise card database from filesystem (#14)
- Create a new datasource for an in-memory sqlite database
- Create a setup function to initialise the card database

#13

Reviewed-on: https://gitea.vylpes.xyz/External/card-drop/pulls/14
Co-authored-by: Ethan Lane <ethan@vylpes.com>
Co-committed-by: Ethan Lane <ethan@vylpes.com>
2023-09-03 17:26:45 +01:00
Ethan Lane c2c2998fe8 Create base Google Drive Helper (#12)
Reviewed-on: https://gitea.vylpes.xyz/External/card-drop/pulls/12
Co-authored-by: Ethan Lane <ethan@vylpes.com>
Co-committed-by: Ethan Lane <ethan@vylpes.com>
2023-08-26 11:39:45 +01:00
Ethan Lane c706737369 Create initial bot framework (#7)
#1

Reviewed-on: https://gitea.vylpes.xyz/External/card-drop/pulls/7
Co-authored-by: Ethan Lane <ethan@vylpes.com>
Co-committed-by: Ethan Lane <ethan@vylpes.com>
2023-08-19 16:56:22 +01:00
56 changed files with 7426 additions and 0 deletions

26
.dev.env Normal file
View file

@ -0,0 +1,26 @@
# Security Warning! Do not commit this file to any VCS!
# This is a local file to speed up development process,
# so you don't have to change your environment variables.
#
# This is not applied to `.env.template`!
# Template files must be committed to the VCS, but must not contain
# any secret values.
BOT_TOKEN=
BOT_VER=0.1.0 DEV
BOT_AUTHOR=Vylpes
BOT_OWNERID=147392775707426816
BOT_CLIENTID=682942374040961060
ABOUT_FUNDING=
ABOUT_REPO=
DB_HOST=127.0.0.1
DB_PORT=3301
DB_NAME=carddrop
DB_AUTH_USER=dev
DB_AUTH_PASS=dev
DB_SYNC=true
DB_LOGGING=true
DB_CARD_FILE=:memory:

71
.drone.yml Normal file
View file

@ -0,0 +1,71 @@
---
kind: pipeline
name: deployment
steps:
- name: deploy
image: appleboy/drone-ssh
settings:
host: 192.168.68.121
username: vylpes
password:
from_secret: ssh_password
port: 22
script:
- sh /home/vylpes/scripts/card-drop/deploy_prod.sh
trigger:
event:
- tag
---
kind: pipeline
name: staging
steps:
- name: stage
image: appleboy/drone-ssh
settings:
host: 192.168.68.121
username: vylpes
password:
from_secret: ssh_password
port: 22
script:
- sh /home/vylpes/scripts/card-drop/deploy_stage.sh
trigger:
branch:
- develop
event:
- push
---
kind: pipeline
name: integration
steps:
- name: build
image: node
commands:
- yarn install --frozen-lockfile
- yarn build
- name: test
image: node
commands:
- yarn install --frozen-lockfile
- yarn test
trigger:
branch:
- main
- develop
- hotfix/*
- feature/*
- renovate/*
event:
- push

18
.gitea/ISSUE_TEMPLATE.md Normal file
View file

@ -0,0 +1,18 @@
Epic: \
Story Points:
---
*No description*
## Acceptance Criteria
*No acceptance criteria*
## Subtasks
*No subtasks*
## Notes
*No notes*

View file

@ -0,0 +1,29 @@
# Description
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
Fixes # (issue)
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
# How Has This Been Tested?
Please describe the tests that you ran to verify the changes. Provide instructions so we can reproduce. Please also list any relevant details to your test configuration.
# Checklist
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that provde my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules

110
.gitignore vendored Normal file
View file

@ -0,0 +1,110 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
config.json
.DS_Store
ormconfig.json
gdrive-credentials.json
cards/

26
.prod.env Normal file
View file

@ -0,0 +1,26 @@
# Security Warning! Do not commit this file to any VCS!
# This is a local file to speed up development process,
# so you don't have to change your environment variables.
#
# This is not applied to `.env.template`!
# Template files must be committed to the VCS, but must not contain
# any secret values.
BOT_TOKEN=
BOT_VER=0.1.0
BOT_AUTHOR=Vylpes
BOT_OWNERID=147392775707426816
BOT_CLIENTID=1093810443589529631
ABOUT_FUNDING=
ABOUT_REPO=
DB_HOST=127.0.0.1
DB_PORT=3321
DB_NAME=carddrop
DB_AUTH_USER=prod
DB_AUTH_PASS=prod
DB_SYNC=false
DB_LOGGING=false
DB_CARD_FILE=:memory:

26
.stage.env Normal file
View file

@ -0,0 +1,26 @@
# Security Warning! Do not commit this file to any VCS!
# This is a local file to speed up development process,
# so you don't have to change your environment variables.
#
# This is not applied to `.env.template`!
# Template files must be committed to the VCS, but must not contain
# any secret values.
BOT_TOKEN=
BOT_VER=0.1.0 BETA
BOT_AUTHOR=Vylpes
BOT_OWNERID=147392775707426816
BOT_CLIENTID=1147976642942214235
ABOUT_FUNDING=
ABOUT_REPO=
DB_HOST=127.0.0.1
DB_PORT=3311
DB_NAME=carddrop
DB_AUTH_USER=stage
DB_AUTH_PASS=stage
DB_SYNC=false
DB_LOGGING=false
DB_CARD_FILE=:memory:

View file

@ -0,0 +1,10 @@
CREATE TABLE `inventory` (
`Id` varchar(255) NOT NULL,
`WhenCreated` datetime NOT NULL,
`WhenUpdated` datetime NOT NULL,
`UserId` varchar(255) NOT NULL,
`CardNumber` varchar(255) NOT NULL,
`Quantity` int NOT NULL,
`ClaimId` varchar(255) NOT NULL,
PRIMARY KEY (`Id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

31
docker-compose.prod.yml Normal file
View file

@ -0,0 +1,31 @@
version: "3.9"
volumes:
prod_database_data:
services:
# discord:
# build: .
database:
image: mysql/mysql-server
command: --default-authentication-plugin=mysql_native_password
restart: always
environment:
- MYSQL_DATABASE=carddrop
- MYSQL_USER=prod
- MYSQL_PASSWORD=prod
- MYSQL_ROOT_PASSWORD=root
- MYSQL_ROOT_HOST=0.0.0.0
ports:
- "3321:3306"
volumes:
- prod_database_data:/var/lib/mysql
phpmyadmin:
image: phpmyadmin
restart: always
ports:
- "3322:80"
environment:
- PMA_ARBITRARY=1

31
docker-compose.stage.yml Normal file
View file

@ -0,0 +1,31 @@
version: "3.9"
volumes:
stage_database_data:
services:
# discord:
# build: .
database:
image: mysql/mysql-server
command: --default-authentication-plugin=mysql_native_password
restart: always
environment:
- MYSQL_DATABASE=carddrop
- MYSQL_USER=stage
- MYSQL_PASSWORD=stage
- MYSQL_ROOT_PASSWORD=root
- MYSQL_ROOT_HOST=0.0.0.0
ports:
- "3311:3306"
volumes:
- stage_database_data:/var/lib/mysql
phpmyadmin:
image: phpmyadmin
restart: always
ports:
- "3312:80"
environment:
- PMA_ARBITRARY=1

31
docker-compose.yml Normal file
View file

@ -0,0 +1,31 @@
version: "3.9"
volumes:
dev_database_data:
services:
# discord:
# build: .
database:
image: mysql/mysql-server
command: --default-authentication-plugin=mysql_native_password
restart: always
environment:
- MYSQL_DATABASE=carddrop
- MYSQL_USER=dev
- MYSQL_PASSWORD=dev
- MYSQL_ROOT_PASSWORD=root
- MYSQL_ROOT_HOST=0.0.0.0
ports:
- "3301:3306"
volumes:
- dev_database_data:/var/lib/mysql
phpmyadmin:
image: phpmyadmin
restart: always
ports:
- "3302:80"
environment:
- PMA_ARBITRARY=1

5
jest.config.json Normal file
View file

@ -0,0 +1,5 @@
{
"preset": "ts-jest",
"testEnvironment": "node",
"setupFiles": ["./jest.setup.js"]
}

3
jest.setup.js Normal file
View file

@ -0,0 +1,3 @@
jest.setTimeout(1 * 1000); // 1 second
jest.resetModules();
jest.resetAllMocks();

48
package.json Normal file
View file

@ -0,0 +1,48 @@
{
"name": "card-drop",
"version": "0.1.0",
"main": "./dist/bot.js",
"typings": "./dist",
"scripts": {
"clean": "rm -rf node_modules/ dist/",
"build": "tsc",
"start": "node ./dist/bot.js",
"test": "jest --passWithNoTests",
"db:up": "typeorm migration:run -d dist/database/dataSources/appDataSource.js",
"db:down": "typeorm migration:revert -d dist/database/dataSources/appDataSource.js",
"db:create": "typeorm migration:create ./src/database/migrations/app/new",
"release": "np --no-publish"
},
"repository": "https://gitea.vylpes.xyz/External/card-drop.git",
"author": "Ethan Lane <ethan@vylpes.com>",
"license": "MIT",
"bugs": {
"url": "https//gitea.vylpes.xyz/External/card-drop/issues",
"email": "helpdesk@vylpes.com"
},
"homepage": "https://gitea.vylpes.xyz/External/card-drop",
"funding": "https://ko-fi.com/vylpes",
"dependencies": {
"@discordjs/rest": "^1.1.0",
"@types/jest": "^29.0.0",
"@types/uuid": "^9.0.0",
"discord.js": "^14.3.0",
"dotenv": "^16.0.0",
"googleapis": "^126.0.0",
"jest": "^29.0.0",
"jest-mock-extended": "^3.0.0",
"minimatch": "9.0.2",
"mysql": "^2.18.1",
"sqlite3": "^5.1.6",
"ts-jest": "^29.0.0",
"typeorm": "0.3.17"
},
"resolutions": {
"**/semver": "^7.5.2"
},
"devDependencies": {
"@types/node": "^20.0.0",
"np": "^8.0.4",
"typescript": "^5.0.0"
}
}

4
renovate.json Normal file
View file

@ -0,0 +1,4 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"baseBranches": ["develop"]
}

23
scripts/deploy_prod.sh Normal file
View file

@ -0,0 +1,23 @@
#! /bin/bash
export PATH="$HOME/.yarn/bin:$PATH"
export PATH="$HOME/.nodeuse/bin:$PATH"
export BOT_TOKEN=$(cat $HOME/scripts/card-drop/prod_key.txt)
cd ~/apps/card-drop/card-drop_prod \
&& git checkout main \
&& git fetch \
&& git pull \
&& docker compose --file docker-compose.prod.yml down \
&& (pm2 stop card-drop_prod || true) \
&& (pm2 delete card-drop_prod || true) \
&& cp .prod.env .env \
&& yarn clean \
&& yarn install --frozen-lockfile \
&& yarn build \
&& docker compose --file docker-compose.prod.yml up -d \
&& echo "Sleeping for 10 seconds to let database load..." \
&& sleep 10 \
&& yarn run db:up \
&& NODE_ENV=production pm2 start --name card-drop_prod dist/bot.js

23
scripts/deploy_stage.sh Normal file
View file

@ -0,0 +1,23 @@
#! /bin/bash
export PATH="$HOME/.yarn/bin:$PATH"
export PATH="$HOME/.nodeuse/bin:$PATH"
export BOT_TOKEN=$(cat $HOME/scripts/card-drop/stage_key.txt)
cd ~/apps/card-drop/card-drop_stage \
&& git checkout develop \
&& git fetch \
&& git pull \
&& docker compose --file docker-compose.stage.yml down \
&& (pm2 stop card-drop_stage || true) \
&& (pm2 delete card-drop_stage || true) \
&& cp .stage.env .env \
&& yarn clean \
&& yarn install --frozen-lockfile \
&& yarn build \
&& docker compose --file docker-compose.stage.yml up -d \
&& echo "Sleeping for 10 seconds to let database load..." \
&& sleep 10 \
&& yarn run db:up \
&& NODE_ENV=production pm2 start --name card-drop_stage dist/bot.js

View file

@ -0,0 +1,111 @@
import { existsSync, readdirSync } from "fs";
import CardDataSource from "../database/dataSources/cardDataSource";
import Card from "../database/entities/card/Card";
import Series from "../database/entities/card/Series";
import path from "path";
import { CardRarity } from "../constants/CardRarity";
export default class CardSetupFunction {
public async Execute() {
await this.ClearDatabase();
await this.ReadSeries();
await this.ReadCards();
}
private async ClearDatabase() {
const cardRepository = CardDataSource.getRepository(Card);
await cardRepository.clear();
const seriesRepository = CardDataSource.getRepository(Series);
await seriesRepository.clear();
}
private async ReadSeries() {
const seriesDir = readdirSync(path.join(process.cwd(), 'cards'));
const seriesRepository = CardDataSource.getRepository(Series);
const seriesToSave: Series[] = [];
for (let dir of seriesDir) {
const dirPart = dir.split(' ');
const seriesId = dirPart.shift();
const seriesName = dirPart.join(' ');
const series = new Series(seriesId!, seriesName, dir);
seriesToSave.push(series);
}
await seriesRepository.save(seriesToSave);
}
private async ReadCards() {
const loadedSeries = await Series.FetchAll(Series, [ "Cards", "Cards.Series" ]);
const cardRepository = CardDataSource.getRepository(Card);
const cardsToSave: Card[] = [];
for (let series of loadedSeries) {
const bronzeExists = existsSync(path.join(process.cwd(), 'cards', series.Path, 'BRONZE'));
const goldExists = existsSync(path.join(process.cwd(), 'cards', series.Path, 'GOLD'));
const legendaryExists = existsSync(path.join(process.cwd(), 'cards', series.Path, 'LEGENDARY'));
const silverExists = existsSync(path.join(process.cwd(), 'cards', series.Path, 'SILVER'));
const cardDirBronze = bronzeExists ? readdirSync(path.join(process.cwd(), 'cards', series.Path, 'BRONZE')) : [];
const cardDirGold = goldExists ? readdirSync(path.join(process.cwd(), 'cards', series.Path, 'GOLD')) : [];
const cardDirLegendary = legendaryExists ? readdirSync(path.join(process.cwd(), 'cards', series.Path, 'LEGENDARY')) : [];
const cardDirSilver = silverExists ? readdirSync(path.join(process.cwd(), 'cards', series.Path, 'SILVER')) : [];
for (let file of cardDirBronze) {
const filePart = file.split('.');
const cardId = filePart[0];
const cardName = filePart[0];
const card = new Card(cardId, cardName, CardRarity.Bronze, path.join(path.join(process.cwd(), 'cards', series.Path, 'BRONZE', file)), series);
cardsToSave.push(card);
}
for (let file of cardDirGold) {
const filePart = file.split('.');
const cardId = filePart[0];
const cardName = filePart[0];
const card = new Card(cardId, cardName, CardRarity.Gold, path.join(path.join(process.cwd(), 'cards', series.Path, 'GOLD', file)), series);
cardsToSave.push(card);
}
for (let file of cardDirLegendary) {
const filePart = file.split('.');
const cardId = filePart[0];
const cardName = filePart[0];
const card = new Card(cardId, cardName, CardRarity.Legendary, path.join(path.join(process.cwd(), 'cards', series.Path, 'LEGENDARY', file)), series);
cardsToSave.push(card);
}
for (let file of cardDirSilver) {
const filePart = file.split('.');
const cardId = filePart[0];
const cardName = filePart[0];
const card = new Card(cardId, cardName, CardRarity.Silver, path.join(path.join(process.cwd(), 'cards', series.Path, 'SILVER', file)), series);
cardsToSave.push(card);
}
}
await cardRepository.save(cardsToSave);
console.log(`Loaded ${cardsToSave.length} cards to database`);
}
}

37
src/bot.ts Normal file
View file

@ -0,0 +1,37 @@
import * as dotenv from "dotenv";
import { CoreClient } from "./client/client";
import { IntentsBitField } from "discord.js";
import Registry from "./registry";
dotenv.config();
const requiredConfigs: string[] = [
"BOT_TOKEN",
"BOT_VER",
"BOT_AUTHOR",
"BOT_OWNERID",
"BOT_CLIENTID",
"DB_HOST",
"DB_PORT",
"DB_AUTH_USER",
"DB_AUTH_PASS",
"DB_SYNC",
"DB_LOGGING",
]
requiredConfigs.forEach(config => {
if (!process.env[config]) {
throw `${config} is required in .env`;
}
});
const client = new CoreClient([
IntentsBitField.Flags.Guilds,
IntentsBitField.Flags.GuildMembers,
]);
Registry.RegisterCommands();
Registry.RegisterEvents();
Registry.RegisterButtonEvents();
client.start();

38
src/buttonEvents/Claim.ts Normal file
View file

@ -0,0 +1,38 @@
import { ButtonInteraction } from "discord.js";
import { ButtonEvent } from "../type/buttonEvent";
import Inventory from "../database/entities/app/Inventory";
import { CoreClient } from "../client/client";
export default class Claim extends ButtonEvent {
public override async execute(interaction: ButtonInteraction) {
if (!interaction.guild || !interaction.guildId) return;
const cardNumber = interaction.customId.split(' ')[1];
const claimId = interaction.customId.split(' ')[2];
const userId = interaction.user.id;
const claimed = await Inventory.FetchOneByClaimId(claimId);
if (claimed) {
await interaction.reply('This card has already been claimed');
return;
}
if (claimId != CoreClient.ClaimId) {
await interaction.reply('This card has expired');
return;
}
let inventory = await Inventory.FetchOneByCardNumberAndUserId(userId, cardNumber);
if (!inventory) {
inventory = new Inventory(userId, cardNumber, 1, claimId);
} else {
inventory.SetQuantity(inventory.Quantity + 1);
}
await inventory.Save(Inventory, inventory);
await interaction.reply('Card claimed');
}
}

View file

@ -0,0 +1,48 @@
import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, CacheType, EmbedBuilder } from "discord.js";
import { ButtonEvent } from "../type/buttonEvent";
import CardDropHelper from "../helpers/CardDropHelper";
import { readFileSync } from "fs";
import { CardRarityToColour, CardRarityToString } from "../constants/CardRarity";
import { v4 } from "uuid";
import { CoreClient } from "../client/client";
export default class Reroll extends ButtonEvent {
public override async execute(interaction: ButtonInteraction) {
if (!interaction.guild || !interaction.guildId) return;
const randomCard = await CardDropHelper.GetRandomCard();
const image = readFileSync(randomCard.Path);
const attachment = new AttachmentBuilder(image, { name: `${randomCard.Id}.png` });
const embed = new EmbedBuilder()
.setTitle(randomCard.Name)
.setDescription(randomCard.Series.Name)
.setFooter({ text: CardRarityToString(randomCard.Rarity) })
.setColor(CardRarityToColour(randomCard.Rarity))
.setImage(`attachment://${randomCard.Id}.png`);
const row = new ActionRowBuilder<ButtonBuilder>();
const claimId = v4();
row.addComponents(
new ButtonBuilder()
.setCustomId(`claim ${randomCard.CardNumber} ${claimId}`)
.setLabel("Claim")
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setCustomId(`reroll`)
.setLabel("Reroll")
.setStyle(ButtonStyle.Secondary));
await interaction.reply({
embeds: [ embed ],
files: [ attachment ],
components: [ row ],
});
CoreClient.ClaimId = claimId;
}
}

105
src/client/client.ts Normal file
View file

@ -0,0 +1,105 @@
import { Client } from "discord.js";
import * as dotenv from "dotenv";
import { EventType } from "../constants/EventType";
import ICommandItem from "../contracts/ICommandItem";
import IEventItem from "../contracts/IEventItem";
import { Command } from "../type/command";
import { Events } from "./events";
import { Util } from "./util";
import CardSetupFunction from "../Functions/CardSetupFunction";
import CardDataSource from "../database/dataSources/cardDataSource";
import CardDropHelper from "../helpers/CardDropHelper";
import IButtonEventItem from "../contracts/IButtonEventItem";
import { ButtonEvent } from "../type/buttonEvent";
import AppDataSource from "../database/dataSources/appDataSource";
export class CoreClient extends Client {
private static _commandItems: ICommandItem[];
private static _eventItems: IEventItem[];
private static _buttonEvents: IButtonEventItem[];
private _events: Events;
private _util: Util;
private _cardSetupFunc: CardSetupFunction;
public static ClaimId: string;
public static get commandItems(): ICommandItem[] {
return this._commandItems;
}
public static get eventItems(): IEventItem[] {
return this._eventItems;
}
public static get buttonEvents(): IButtonEventItem[] {
return this._buttonEvents;
}
constructor(intents: number[]) {
super({ intents: intents });
dotenv.config();
CoreClient._commandItems = [];
CoreClient._eventItems = [];
CoreClient._buttonEvents = [];
this._events = new Events();
this._util = new Util();
this._cardSetupFunc = new CardSetupFunction();
}
public async start() {
if (!process.env.BOT_TOKEN) {
console.error("BOT_TOKEN is not defined in .env");
return;
}
await AppDataSource.initialize()
.then(() => console.log("App Data Source Initialised"))
.catch(err => console.error("Error initialising App Data Source", err));
await CardDataSource.initialize()
.then(() => console.log("Card Data Source Initialised"))
.catch(err => console.error("Error initialising Card Data Source", err));
super.on("interactionCreate", this._events.onInteractionCreate);
super.on("ready", this._events.onReady);
await this._cardSetupFunc.Execute();
await super.login(process.env.BOT_TOKEN);
this._util.loadEvents(this, CoreClient._eventItems);
this._util.loadSlashCommands(this);
}
public static RegisterCommand(name: string, command: Command, serverId?: string) {
const item: ICommandItem = {
Name: name,
Command: command,
ServerId: serverId,
};
CoreClient._commandItems.push(item);
}
public static RegisterEvent(eventType: EventType, func: Function) {
const item: IEventItem = {
EventType: eventType,
ExecutionFunction: func,
};
CoreClient._eventItems.push(item);
}
public static RegisterButtonEvent(buttonId: string, event: ButtonEvent) {
const item: IButtonEventItem = {
ButtonId: buttonId,
Event: event,
};
CoreClient._buttonEvents.push(item);
}
}

22
src/client/events.ts Normal file
View file

@ -0,0 +1,22 @@
import { Interaction } from "discord.js";
import ChatInputCommand from "./interactionCreate/ChatInputCommand";
import Button from "./interactionCreate/Button";
export class Events {
public async onInteractionCreate(interaction: Interaction) {
if (!interaction.guildId) return;
if (interaction.isChatInputCommand()) {
ChatInputCommand.onChatInput(interaction);
}
if (interaction.isButton()) {
Button.onButtonClicked(interaction);
}
}
// Emit when bot is logged in and ready to use
public onReady() {
console.log("Ready");
}
}

View file

@ -0,0 +1,17 @@
import { ButtonInteraction, Interaction } from "discord.js";
import { CoreClient } from "../client";
export default class Button {
public static async onButtonClicked(interaction: ButtonInteraction) {
if (!interaction.isButton) return;
const item = CoreClient.buttonEvents.find(x => x.ButtonId == interaction.customId.split(' ')[0]);
if (!item) {
await interaction.reply('Event not found');
return;
}
item.Event.execute(interaction);
}
}

View file

@ -0,0 +1,27 @@
import { Interaction } from "discord.js";
import { CoreClient } from "../client";
import ICommandItem from "../../contracts/ICommandItem";
export default class ChatInputCommand {
public static async onChatInput(interaction: Interaction) {
if (!interaction.isChatInputCommand()) return;
const item = CoreClient.commandItems.find(x => x.Name == interaction.commandName && !x.ServerId);
const itemForServer = CoreClient.commandItems.find(x => x.Name == interaction.commandName && x.ServerId == interaction.guildId);
let itemToUse: ICommandItem;
if (!itemForServer) {
if (!item) {
await interaction.reply('Command not found');
return;
}
itemToUse = item;
} else {
itemToUse = itemForServer;
}
itemToUse.Command.execute(interaction);
}
}

95
src/client/util.ts Normal file
View file

@ -0,0 +1,95 @@
import { Client, REST, Routes, SlashCommandBuilder } from "discord.js";
import { EventType } from "../constants/EventType";
import IEventItem from "../contracts/IEventItem";
import { CoreClient } from "./client";
export class Util {
public loadSlashCommands(client: Client) {
const registeredCommands = CoreClient.commandItems;
const globalCommands = registeredCommands.filter(x => !x.ServerId);
const guildCommands = registeredCommands.filter(x => x.ServerId);
const globalCommandData: SlashCommandBuilder[] = globalCommands
.filter(x => x.Command.CommandBuilder)
.flatMap(x => x.Command.CommandBuilder);
const guildIds: string[] = [];
for (let command of guildCommands) {
if (!guildIds.find(x => x == command.ServerId)) {
guildIds.push(command.ServerId!);
}
}
const rest = new REST({ version: '10' }).setToken(process.env.BOT_TOKEN!);
rest.put(
Routes.applicationCommands(process.env.BOT_CLIENTID!),
{
body: globalCommandData
}
);
for (let guild of guildIds) {
const guildCommandData = guildCommands.filter(x => x.ServerId == guild)
.filter(x => x.Command.CommandBuilder)
.flatMap(x => x.Command.CommandBuilder);
if (!client.guilds.cache.has(guild)) continue;
rest.put(
Routes.applicationGuildCommands(process.env.BOT_CLIENTID!, guild),
{
body: guildCommandData
}
)
}
}
// Load the events
loadEvents(client: Client, events: IEventItem[]) {
events.forEach((e) => {
switch(e.EventType) {
case EventType.ChannelCreate:
client.on('channelCreate', (channel) => e.ExecutionFunction(channel));
break;
case EventType.ChannelDelete:
client.on('channelDelete', (channel) => e.ExecutionFunction(channel));
break;
case EventType.ChannelUpdate:
client.on('channelUpdate', (channel) => e.ExecutionFunction(channel));
break;
case EventType.GuildBanAdd:
client.on('guildBanAdd', (ban) => e.ExecutionFunction(ban));
break;
case EventType.GuildBanRemove:
client.on('guildBanRemove', (ban) => e.ExecutionFunction(ban));
break;
case EventType.GuildCreate:
client.on('guildCreate', (guild) => e.ExecutionFunction(guild));
break;
case EventType.GuildMemberAdd:
client.on('guildMemberAdd', (member) => e.ExecutionFunction(member));
break;
case EventType.GuildMemberRemove:
client.on('guildMemberRemove', (member) => e.ExecutionFunction(member));
break;
case EventType.GuildMemberUpdate:
client.on('guildMemberUpdate', (oldMember, newMember) => e.ExecutionFunction(oldMember, newMember));
break;
case EventType.MessageCreate:
client.on('messageCreate', (message) => e.ExecutionFunction(message));
break;
case EventType.MessageDelete:
client.on('messageDelete', (message) => e.ExecutionFunction(message));
break;
case EventType.MessageUpdate:
client.on('messageUpdate', (oldMessage, newMessage) => e.ExecutionFunction(oldMessage, newMessage));
break;
default:
console.error('Event not implemented.');
}
});
}
}

56
src/commands/about.ts Normal file
View file

@ -0,0 +1,56 @@
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, EmbedBuilder, SlashCommandBuilder } from "discord.js";
import EmbedColours from "../constants/EmbedColours";
import { Command } from "../type/command";
export default class About extends Command {
constructor() {
super();
super.CommandBuilder = new SlashCommandBuilder()
.setName('about')
.setDescription('About Bot');
}
public override async execute(interaction: CommandInteraction) {
const fundingLink = process.env.ABOUT_FUNDING;
const repoLink = process.env.ABOUT_REPO;
const embed = new EmbedBuilder()
.setColor(EmbedColours.Ok)
.setTitle("About")
.setDescription("Discord Bot made by Vylpes");
embed.addFields([
{
name: "Version",
value: process.env.BOT_VER!,
inline: true,
},
{
name: "Author",
value: process.env.BOT_AUTHOR!,
inline: true,
},
]);
const row = new ActionRowBuilder<ButtonBuilder>();
if (repoLink) {
row.addComponents(
new ButtonBuilder()
.setURL(repoLink)
.setLabel("Repo")
.setStyle(ButtonStyle.Link));
}
if (fundingLink) {
row.addComponents(
new ButtonBuilder()
.setURL(fundingLink)
.setLabel("Funding")
.setStyle(ButtonStyle.Link));
}
await interaction.reply({ embeds: [ embed ], components: row.components.length > 0 ? [ row ] : [] });
}
}

54
src/commands/drop.ts Normal file
View file

@ -0,0 +1,54 @@
import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, EmbedBuilder, SlashCommandBuilder } from "discord.js";
import { Command } from "../type/command";
import CardDropHelper from "../helpers/CardDropHelper";
import { CardRarityToColour, CardRarityToString } from "../constants/CardRarity";
import { readFileSync } from "fs";
import { CoreClient } from "../client/client";
import { v4 } from "uuid";
export default class Drop extends Command {
constructor() {
super();
super.CommandBuilder = new SlashCommandBuilder()
.setName('drop')
.setDescription('Summon a new card drop');
}
public override async execute(interaction: CommandInteraction) {
const randomCard = await CardDropHelper.GetRandomCard();
const image = readFileSync(randomCard.Path);
const attachment = new AttachmentBuilder(image, { name: `${randomCard.Id}.png` });
const embed = new EmbedBuilder()
.setTitle(randomCard.Name)
.setDescription(randomCard.Series.Name)
.setFooter({ text: CardRarityToString(randomCard.Rarity) })
.setColor(CardRarityToColour(randomCard.Rarity))
.setImage(`attachment://${randomCard.Id}.png`);
const row = new ActionRowBuilder<ButtonBuilder>();
const claimId = v4();
row.addComponents(
new ButtonBuilder()
.setCustomId(`claim ${randomCard.CardNumber} ${claimId}`)
.setLabel("Claim")
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setCustomId(`reroll`)
.setLabel("Reroll")
.setStyle(ButtonStyle.Secondary));
await interaction.reply({
embeds: [ embed ],
files: [ attachment ],
components: [ row ],
});
CoreClient.ClaimId = claimId;
}
}

View file

@ -0,0 +1,34 @@
import EmbedColours from "./EmbedColours";
export enum CardRarity {
Bronze,
Silver,
Gold,
Legendary,
}
export function CardRarityToString(rarity: CardRarity): string {
switch (rarity) {
case CardRarity.Bronze:
return "Bronze";
case CardRarity.Silver:
return "Silver";
case CardRarity.Gold:
return "Gold";
case CardRarity.Legendary:
return "Legendary";
}
}
export function CardRarityToColour(rarity: CardRarity): number {
switch (rarity) {
case CardRarity.Bronze:
return EmbedColours.BronzeCard;
case CardRarity.Silver:
return EmbedColours.SilverCard;
case CardRarity.Gold:
return EmbedColours.GoldCard;
case CardRarity.Legendary:
return EmbedColours.LegendaryCard;
}
}

View file

@ -0,0 +1,7 @@
export default class EmbedColours {
public static readonly Ok = 0x3050ba;
public static readonly BronzeCard = 0xcd7f32;
public static readonly SilverCard = 0xc0c0c0;
public static readonly GoldCard = 0xffd700;
public static readonly LegendaryCard = 0x50c878;
}

View file

@ -0,0 +1,15 @@
export enum EventType {
ChannelCreate,
ChannelDelete,
ChannelUpdate,
GuildBanAdd,
GuildBanRemove,
GuildCreate,
GuildMemberAdd,
GuildMemberRemove,
GuildMemberUpdate,
MessageCreate,
MessageDelete,
MessageUpdate,
Ready,
}

View file

@ -0,0 +1,59 @@
import { Column, DeepPartial, EntityTarget, PrimaryColumn, ObjectLiteral, FindOptionsWhere } from "typeorm";
import { v4 } from "uuid";
import AppDataSource from "../database/dataSources/appDataSource";
export default class AppBaseEntity {
constructor() {
this.Id = v4();
this.WhenCreated = new Date();
this.WhenUpdated = new Date();
}
@PrimaryColumn()
Id: string;
@Column()
WhenCreated: Date;
@Column()
WhenUpdated: Date;
public async Save<T extends AppBaseEntity>(target: EntityTarget<T>, entity: DeepPartial<T>): Promise<void> {
this.WhenUpdated = new Date();
const repository = AppDataSource.getRepository<T>(target);
await repository.save(entity);
}
public static async Remove<T extends AppBaseEntity>(target: EntityTarget<T>, entity: T): Promise<void> {
const repository = AppDataSource.getRepository<T>(target);
await repository.remove(entity);
}
public static async FetchAll<T extends AppBaseEntity>(target: EntityTarget<T>, relations?: string[]): Promise<T[]> {
const repository = AppDataSource.getRepository<T>(target);
const all = await repository.find({ relations: relations || [] });
return all;
}
public static async FetchOneById<T extends AppBaseEntity>(target: EntityTarget<T>, id: string, relations?: string[]): Promise<T | null> {
const repository = AppDataSource.getRepository<T>(target);
const single = await repository.findOne({ where: ({ Id: id } as FindOptionsWhere<T>), relations: relations || {} });
return single;
}
public static async Any<T extends ObjectLiteral>(target: EntityTarget<T>): Promise<boolean> {
const repository = AppDataSource.getRepository<T>(target);
const any = await repository.find();
return any.length > 0;
}
}

View file

@ -0,0 +1,60 @@
import { Column, DeepPartial, EntityTarget, PrimaryColumn, ObjectLiteral, FindOptionsWhere } from "typeorm";
import { v4 } from "uuid";
import AppDataSource from "../database/dataSources/appDataSource";
import CardDataSource from "../database/dataSources/cardDataSource";
export default class CardBaseEntity {
constructor() {
this.Id = v4();
this.WhenCreated = new Date();
this.WhenUpdated = new Date();
}
@PrimaryColumn()
Id: string;
@Column()
WhenCreated: Date;
@Column()
WhenUpdated: Date;
public async Save<T extends CardBaseEntity>(target: EntityTarget<T>, entity: DeepPartial<T>): Promise<void> {
this.WhenUpdated = new Date();
const repository = CardDataSource.getRepository<T>(target);
await repository.save(entity);
}
public static async Remove<T extends CardBaseEntity>(target: EntityTarget<T>, entity: T): Promise<void> {
const repository = CardDataSource.getRepository<T>(target);
await repository.remove(entity);
}
public static async FetchAll<T extends CardBaseEntity>(target: EntityTarget<T>, relations?: string[]): Promise<T[]> {
const repository = CardDataSource.getRepository<T>(target);
const all = await repository.find({ relations: relations || [] });
return all;
}
public static async FetchOneById<T extends CardBaseEntity>(target: EntityTarget<T>, id: string, relations?: string[]): Promise<T | null> {
const repository = CardDataSource.getRepository<T>(target);
const single = await repository.findOne({ where: ({ Id: id } as FindOptionsWhere<T>), relations: relations || {} });
return single;
}
public static async Any<T extends ObjectLiteral>(target: EntityTarget<T>): Promise<boolean> {
const repository = CardDataSource.getRepository<T>(target);
const any = await repository.find();
return any.length > 0;
}
}

View file

@ -0,0 +1,4 @@
export interface IBaseResponse {
valid: boolean;
message?: string;
}

View file

@ -0,0 +1,6 @@
import { ButtonEvent } from "../type/buttonEvent";
export default interface IButtonEventItem {
ButtonId: string,
Event: ButtonEvent,
}

View file

@ -0,0 +1,7 @@
import { Command } from "../type/command";
export default interface ICommandItem {
Name: string,
Command: Command,
ServerId?: string,
}

View file

@ -0,0 +1,7 @@
import { EventType } from "../constants/EventType";
export default interface IEventItem {
EventType: EventType,
ExecutionFunction: Function,
}

View file

@ -0,0 +1,4 @@
export default interface IGDriveFolderListing {
id: string,
name: string,
};

View file

@ -0,0 +1,26 @@
import { DataSource } from "typeorm";
import * as dotenv from "dotenv";
dotenv.config();
const AppDataSource = new DataSource({
type: "mysql",
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
username: process.env.DB_AUTH_USER,
password: process.env.DB_AUTH_PASS,
database: process.env.DB_NAME,
synchronize: process.env.DB_SYNC == "true",
logging: process.env.DB_LOGGING == "true",
entities: [
"dist/database/entities/app/**/*.js",
],
migrations: [
"dist/database/migrations/app/**/*.js",
],
subscribers: [
"dist/database/subscribers/app/**/*.js",
],
});
export default AppDataSource;

View file

@ -0,0 +1,22 @@
import { DataSource } from "typeorm";
import * as dotenv from "dotenv";
dotenv.config();
const CardDataSource = new DataSource({
type: "sqlite",
database: process.env.DB_CARD_FILE!,
synchronize: true,
logging: process.env.DB_LOGGING == "true",
entities: [
"dist/database/entities/card/**/*.js",
],
migrations: [
"dist/database/migrations/card/**/*.js",
],
subscribers: [
"dist/database/subscribers/card/**/*.js",
],
});
export default CardDataSource;

View file

@ -0,0 +1,47 @@
import { Column, Entity } from "typeorm";
import AppBaseEntity from "../../../contracts/AppBaseEntity";
import AppDataSource from "../../dataSources/appDataSource";
@Entity()
export default class Inventory extends AppBaseEntity {
constructor(userId: string, cardNumber: string, quantity: number, claimId: string) {
super();
this.UserId = userId;
this.CardNumber = cardNumber;
this.Quantity = quantity;
this.ClaimId = claimId;
}
@Column()
UserId: string;
@Column()
CardNumber: string;
@Column()
Quantity: number;
@Column()
ClaimId: string;
public SetQuantity(quantity: number) {
this.Quantity = quantity;
}
public static async FetchOneByCardNumberAndUserId(userId: string, cardNumber: string): Promise<Inventory | null> {
const repository = AppDataSource.getRepository(Inventory);
const single = await repository.findOne({ where: { UserId: userId, CardNumber: cardNumber }});
return single;
}
public static async FetchOneByClaimId(claimId: string): Promise<Inventory | null> {
const repository = AppDataSource.getRepository(Inventory);
const single = await repository.findOne({ where: { ClaimId: claimId }});
return single;
}
}

View file

@ -0,0 +1,32 @@
import { Column, Entity, ManyToOne } from "typeorm";
import CardBaseEntity from "../../../contracts/CardBaseEntity";
import { CardRarity } from "../../../constants/CardRarity";
import Series from "./Series";
@Entity()
export default class Card extends CardBaseEntity {
constructor(cardNumber: string, name: string, rarity: CardRarity, path: string, series: Series) {
super();
this.CardNumber = cardNumber;
this.Name = name;
this.Rarity = rarity;
this.Path = path;
this.Series = series;
}
@Column()
CardNumber: string
@Column()
Name: string;
@Column()
Rarity: CardRarity;
@Column()
Path: string
@ManyToOne(() => Series, x => x.Cards)
Series: Series;
}

View file

@ -0,0 +1,23 @@
import { Column, Entity, OneToMany } from "typeorm";
import CardBaseEntity from "../../../contracts/CardBaseEntity";
import Card from "./Card";
@Entity()
export default class Series extends CardBaseEntity {
constructor(id: string, name: string, path: string) {
super();
this.Id = id;
this.Name = name;
this.Path = path;
}
@Column()
Name: string;
@Column()
Path: string;
@OneToMany(() => Card, x => x.Series)
Cards: Card[];
}

View file

@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from "typeorm"
import MigrationHelper from "../../../../helpers/MigrationHelper"
export class CreateBase1693769942868 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
MigrationHelper.Up('1693769942868-CreateBase', '0.1', [
"01-table/Inventory",
], queryRunner);
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}

View file

View file

@ -0,0 +1,38 @@
import { CardRarity } from "../constants/CardRarity";
import CardDataSource from "../database/dataSources/cardDataSource";
import Card from "../database/entities/card/Card";
import Series from "../database/entities/card/Series";
export default class CardDropHelper {
public static async GetRandomCard(): Promise<Card> {
const seriesRepository = CardDataSource.getRepository(Series);
const allSeries = await Series.FetchAll(Series, [ "Cards", "Cards.Series" ]);
const allSeriesWithCards = allSeries.filter(x => x.Cards.length > 0);
const randomSeriesIndex = Math.floor(Math.random() * allSeriesWithCards.length);
const randomSeries = allSeriesWithCards[randomSeriesIndex];
const randomRarity = Math.random() * 100;
let cardRarity: CardRarity;
const bronzeChance = 62;
const silverChance = bronzeChance + 31;
const goldChance = silverChance + 6.4;
if (randomRarity < bronzeChance) cardRarity = CardRarity.Bronze;
else if (randomRarity < silverChance) cardRarity = CardRarity.Silver;
else if (randomRarity < goldChance) cardRarity = CardRarity.Gold;
else cardRarity = CardRarity.Legendary;
const allCards = randomSeries.Cards.filter(x => x.Rarity == cardRarity);
const randomCardIndex = Math.floor(Math.random() * allCards.length);
const randomCard = allCards[randomCardIndex];
return randomCard;
}
}

View file

@ -0,0 +1,83 @@
import { Auth, drive_v3, google } from "googleapis";
import IGDriveFolderListing from "../contracts/IGDriveFolderListing";
import path, { resolve } from "path";
import os from 'os';
import uuid, { v4 } from 'uuid';
import { createWriteStream } from "fs";
export default class GoogleDriveHelper {
private _auth: Auth.GoogleAuth;
private _drive: drive_v3.Drive;
constructor() {
this._auth = new google.auth.GoogleAuth({
keyFile: "gdrive-credentials.json",
scopes: [
"https://www.googleapis.com/auth/drive.readonly",
"https://www.googleapis.com/auth/drive.metadata.readonly",
],
});
this._drive = google.drive( { version: "v3", auth: this._auth });
}
public async listFolder(folderId: string, pageSize: number): Promise<IGDriveFolderListing[]> {
const params = {
pageSize: pageSize,
fields: "nextPageToken, files(id, name)",
q: `'${folderId}' in parents and trashed=false`
}
const res = await this._drive.files.list(params);
return res.data.files as IGDriveFolderListing[];
}
public downloadFile(fileId: string) {
const res = this._drive.files.get({
fileId: fileId,
alt: 'media',
}, {
responseType: 'stream',
})
.then(res => {
return new Promise((resolve, reject) => {
const filePath = path.join(process.cwd(), 'temp', v4());
const dest = createWriteStream(filePath);
let progress = 0;
res.data
.on('end', () => {
resolve(filePath);
})
.on('error', err => {
reject(err);
})
.on('data', d => {
progress += d.length;
})
.pipe(dest);
});
})
}
public async exportFile(fileId: string, mimeType: string) {
const destPath = path.join(process.cwd(), 'temp', v4());
const dest = createWriteStream(destPath);
const res = await this._drive.files.export({
fileId: fileId,
mimeType: mimeType
}, {
responseType: 'stream',
});
await new Promise((resolve, reject) => {
res.data
.on('error', reject)
.pipe(dest)
.on('error', reject)
.on('finish', resolve);
})
}
}

View file

@ -0,0 +1,20 @@
import { readFileSync } from "fs";
import { QueryRunner } from "typeorm";
export default class MigrationHelper {
public static Up(migrationName: string, version: string, queryFiles: string[], queryRunner: QueryRunner) {
for (let path of queryFiles) {
const query = readFileSync(`${process.cwd()}/database/${version}/${migrationName}/Up/${path}.sql`).toString();
queryRunner.query(query);
}
}
public static Down(migrationName: string, version: string, queryFiles: string[], queryRunner: QueryRunner) {
for (let path of queryFiles) {
const query = readFileSync(`${process.cwd()}/database/${version}/${migrationName}/Down/${path}.sql`).toString();
queryRunner.query(query);
}
}
}

View file

@ -0,0 +1,42 @@
export default class StringTools {
public static Capitalise(str: string): string {
const words = str.split(" ");
let result: string[] = [];
words.forEach(word => {
const firstLetter = word.substring(0, 1).toUpperCase();
const rest = word.substring(1);
result.push(firstLetter + rest);
});
return result.join(" ");
}
public static CapitaliseArray(str: string[]): string[] {
const res: string[] = [];
str.forEach(s => {
res.push(StringTools.Capitalise(s));
});
return res;
}
public static RandomString(length: number) {
let result = "";
const characters = 'abcdefghkmnpqrstuvwxyz23456789';
const charactersLength = characters.length;
for ( var i = 0; i < length; i++ ) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
public static ReplaceAll(str: string, find: string, replace: string) {
return str.replace(new RegExp(find, 'g'), replace);
}
}

View file

@ -0,0 +1,121 @@
import StringTools from "./StringTools";
export default class TimeLengthInput {
public readonly value: string;
constructor(input: string) {
this.value = StringTools.ReplaceAll(input, ',', '');
}
public GetDays(): number {
return this.GetValue('d');
}
public GetHours(): number {
return this.GetValue('h');
}
public GetMinutes(): number {
return this.GetValue('m');
}
public GetSeconds(): number {
return this.GetValue('s');
}
public GetMilliseconds(): number {
const days = this.GetDays();
const hours = this.GetHours();
const minutes = this.GetMinutes();
const seconds = this.GetSeconds();
let milliseconds = 0;
milliseconds += seconds * 1000;
milliseconds += minutes * 60 * 1000;
milliseconds += hours * 60 * 60 * 1000;
milliseconds += days * 24 * 60 * 60 * 1000;
return milliseconds;
}
public GetDateFromNow(): Date {
const now = Date.now();
const dateFromNow = now
+ (1000 * this.GetSeconds())
+ (1000 * 60 * this.GetMinutes())
+ (1000 * 60 * 60 * this.GetHours())
+ (1000 * 60 * 60 * 24 * this.GetDays());
return new Date(dateFromNow);
}
public GetLength(): string {
const days = this.GetDays();
const hours = this.GetHours();
const minutes = this.GetMinutes();
const seconds = this.GetSeconds();
const value = [];
if (days) {
value.push(`${days} days`);
}
if (hours) {
value.push(`${hours} hours`);
}
if (minutes) {
value.push(`${minutes} minutes`);
}
if (seconds) {
value.push(`${seconds} seconds`);
}
return value.join(", ");
}
public GetLengthShort(): string {
const days = this.GetDays();
const hours = this.GetHours();
const minutes = this.GetMinutes();
const seconds = this.GetSeconds();
const value = [];
if (days) {
value.push(`${days}d`);
}
if (hours) {
value.push(`${hours}h`);
}
if (minutes) {
value.push(`${minutes}m`);
}
if (seconds) {
value.push(`${seconds}s`);
}
return value.join(" ");
}
private GetValue(designation: string): number {
const valueSplit = this.value.split(' ');
const desString = valueSplit.find(x => x.charAt(x.length - 1) == designation);
if (!desString) return 0;
const desNumber = Number(desString.substring(0, desString.length - 1));
if (!desNumber) return 0;
return desNumber;
}
}

23
src/registry.ts Normal file
View file

@ -0,0 +1,23 @@
import { CoreClient } from "./client/client";
import About from "./commands/about";
import Drop from "./commands/drop";
import Claim from "./buttonEvents/Claim";
import Reroll from "./buttonEvents/Reroll";
export default class Registry {
public static RegisterCommands() {
CoreClient.RegisterCommand('about', new About());
CoreClient.RegisterCommand('drop', new Drop());
}
public static RegisterEvents() {
}
public static RegisterButtonEvents() {
CoreClient.RegisterButtonEvent('claim', new Claim());
CoreClient.RegisterButtonEvent('reroll', new Reroll());
}
}

7
src/type/buttonEvent.ts Normal file
View file

@ -0,0 +1,7 @@
import { ButtonInteraction } from "discord.js";
export class ButtonEvent {
public execute(interaction: ButtonInteraction) {
}
}

9
src/type/command.ts Normal file
View file

@ -0,0 +1,9 @@
import { CommandInteraction } from "discord.js";
export class Command {
public CommandBuilder: any;
public execute(interaction: CommandInteraction) {
}
}

0
tests/.gitkeep Normal file
View file

78
tsconfig.json Normal file
View file

@ -0,0 +1,78 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
"declaration": true, /* Generates corresponding '.d.ts' file. */
"declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./dist", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
"strictPropertyInitialization": false, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
},
"include": [
"./src",
],
"exclude": [
"./tests"
]
}

5512
yarn.lock Normal file

File diff suppressed because it is too large Load diff