diff --git a/assets/brawl_logo.png b/assets/brawl_logo.png new file mode 100644 index 0000000..07ee074 Binary files /dev/null and b/assets/brawl_logo.png differ diff --git a/bun.lock b/bun.lock index e2557e4..0a0c0df 100644 --- a/bun.lock +++ b/bun.lock @@ -12,7 +12,7 @@ }, "devDependencies": { "@types/bun": "latest", - "prisma": "^6.6.0", + "prisma": "^6.7.0", }, "peerDependencies": { "typescript": "^5", @@ -78,17 +78,17 @@ "@prisma/client": ["@prisma/client@6.6.0", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-vfp73YT/BHsWWOAuthKQ/1lBgESSqYqAWZEYyTdGXyFAHpmewwWL2Iz6ErIzkj4aHbuc6/cGSsE6ZY+pBO04Cg=="], - "@prisma/config": ["@prisma/config@6.6.0", "", { "dependencies": { "esbuild": ">=0.12 <1", "esbuild-register": "3.6.0" } }, "sha512-d8FlXRHsx72RbN8nA2QCRORNv5AcUnPXgtPvwhXmYkQSMF/j9cKaJg+9VcUzBRXGy9QBckNzEQDEJZdEOZ+ubA=="], + "@prisma/config": ["@prisma/config@6.7.0", "", { "dependencies": { "esbuild": ">=0.12 <1", "esbuild-register": "3.6.0" } }, "sha512-di8QDdvSz7DLUi3OOcCHSwxRNeW7jtGRUD2+Z3SdNE3A+pPiNT8WgUJoUyOwJmUr5t+JA2W15P78C/N+8RXrOA=="], - "@prisma/debug": ["@prisma/debug@6.6.0", "", {}, "sha512-DL6n4IKlW5k2LEXzpN60SQ1kP/F6fqaCgU/McgaYsxSf43GZ8lwtmXLke9efS+L1uGmrhtBUP4npV/QKF8s2ZQ=="], + "@prisma/debug": ["@prisma/debug@6.7.0", "", {}, "sha512-RabHn9emKoYFsv99RLxvfG2GHzWk2ZI1BuVzqYtmMSIcuGboHY5uFt3Q3boOREM9de6z5s3bQoyKeWnq8Fz22w=="], - "@prisma/engines": ["@prisma/engines@6.6.0", "", { "dependencies": { "@prisma/debug": "6.6.0", "@prisma/engines-version": "6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a", "@prisma/fetch-engine": "6.6.0", "@prisma/get-platform": "6.6.0" } }, "sha512-nC0IV4NHh7500cozD1fBoTwTD1ydJERndreIjpZr/S3mno3P6tm8qnXmIND5SwUkibNeSJMpgl4gAnlqJ/gVlg=="], + "@prisma/engines": ["@prisma/engines@6.7.0", "", { "dependencies": { "@prisma/debug": "6.7.0", "@prisma/engines-version": "6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed", "@prisma/fetch-engine": "6.7.0", "@prisma/get-platform": "6.7.0" } }, "sha512-3wDMesnOxPrOsq++e5oKV9LmIiEazFTRFZrlULDQ8fxdub5w4NgRBoxtWbvXmj2nJVCnzuz6eFix3OhIqsZ1jw=="], - "@prisma/engines-version": ["@prisma/engines-version@6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a", "", {}, "sha512-JzRaQ5Em1fuEcbR3nUsMNYaIYrOT1iMheenjCvzZblJcjv/3JIuxXN7RCNT5i6lRkLodW5ojCGhR7n5yvnNKrw=="], + "@prisma/engines-version": ["@prisma/engines-version@6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed", "", {}, "sha512-EvpOFEWf1KkJpDsBCrih0kg3HdHuaCnXmMn7XFPObpFTzagK1N0Q0FMnYPsEhvARfANP5Ok11QyoTIRA2hgJTA=="], - "@prisma/fetch-engine": ["@prisma/fetch-engine@6.6.0", "", { "dependencies": { "@prisma/debug": "6.6.0", "@prisma/engines-version": "6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a", "@prisma/get-platform": "6.6.0" } }, "sha512-Ohfo8gKp05LFLZaBlPUApM0M7k43a0jmo86YY35u1/4t+vuQH9mRGU7jGwVzGFY3v+9edeb/cowb1oG4buM1yw=="], + "@prisma/fetch-engine": ["@prisma/fetch-engine@6.7.0", "", { "dependencies": { "@prisma/debug": "6.7.0", "@prisma/engines-version": "6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed", "@prisma/get-platform": "6.7.0" } }, "sha512-zLlAGnrkmioPKJR4Yf7NfW3hftcvqeNNEHleMZK9yX7RZSkhmxacAYyfGsCcqRt47jiZ7RKdgE0Wh2fWnm7WsQ=="], - "@prisma/get-platform": ["@prisma/get-platform@6.6.0", "", { "dependencies": { "@prisma/debug": "6.6.0" } }, "sha512-3qCwmnT4Jh5WCGUrkWcc6VZaw0JY7eWN175/pcb5Z6FiLZZ3ygY93UX0WuV41bG51a6JN/oBH0uywJ90Y+V5eA=="], + "@prisma/get-platform": ["@prisma/get-platform@6.7.0", "", { "dependencies": { "@prisma/debug": "6.7.0" } }, "sha512-i9IH5lO4fQwnMLvQLYNdgVh9TK3PuWBfQd7QLk/YurnAIg+VeADcZDbmhAi4XBBDD+hDif9hrKyASu0hbjwabw=="], "@puppeteer/browsers": ["@puppeteer/browsers@2.10.2", "", { "dependencies": { "debug": "^4.4.0", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.7.1", "tar-fs": "^3.0.8", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-i4Ez+s9oRWQbNjtI/3+jxr7OH508mjAKvza0ekPJem0ZtmsYHP3B5dq62+IaBHKaGCOuqJxXzvFLUhJvQ6jtsQ=="], @@ -230,7 +230,7 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "prisma": ["prisma@6.6.0", "", { "dependencies": { "@prisma/config": "6.6.0", "@prisma/engines": "6.6.0" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-SYCUykz+1cnl6Ugd8VUvtTQq5+j1Q7C0CtzKPjQ8JyA2ALh0EEJkMCS+KgdnvKW1lrxjtjCyJSHOOT236mENYg=="], + "prisma": ["prisma@6.7.0", "", { "dependencies": { "@prisma/config": "6.7.0", "@prisma/engines": "6.7.0" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-vArg+4UqnQ13CVhc2WUosemwh6hr6cr6FY2uzDvCIFwH8pu8BXVv38PktoMLVjtX7sbYThxbnZF5YiR8sN2clw=="], "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], diff --git a/commands.json b/commands.json index 5cbd2cb..e942a24 100644 --- a/commands.json +++ b/commands.json @@ -1 +1 @@ -[{"name":"mcs","type":1,"nsfw":false,"description":"Qeury a Minecraft server","contexts":[0,1,2],"integration_types":[0],"options":[{"name":"ip","description":"Server IP","type":3,"name_localizations":{},"description_localizations":{},"autocomplete":false},{"name":"port","description":"Server Port","type":10,"name_localizations":{},"description_localizations":{},"autocomplete":false}]},{"name":"ping","type":1,"nsfw":false,"description":"Show latency with Discord","contexts":[0,1,2],"integration_types":[0],"options":[]}] \ No newline at end of file +[{"name":"mcs","type":1,"nsfw":false,"description":"Qeury a Minecraft server","contexts":[0,1,2],"integration_types":[0],"options":[{"name":"ip","description":"Server IP","type":3,"name_localizations":{},"description_localizations":{},"autocomplete":false},{"name":"port","description":"Server Port","type":10,"name_localizations":{},"description_localizations":{},"autocomplete":false}]},{"name":"brawlbind","type":1,"nsfw":false,"description":"Associate your Discord account with your Brawl Stars account","contexts":[0,1,2],"integration_types":[0],"options":[{"name":"playertag","description":"Your Brawl Stars player tag","required":true,"type":3,"name_localizations":{},"description_localizations":{},"autocomplete":false}]},{"name":"ping","type":1,"nsfw":false,"description":"Show latency with Discord","contexts":[0,1,2],"integration_types":[0],"options":[]},{"name":"brawlstats","type":1,"nsfw":false,"description":"Query your Brawl Stars player stats","contexts":[0,1,2],"integration_types":[0],"options":[]}] \ No newline at end of file diff --git a/package.json b/package.json index 811e3f0..7ff7d5b 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,10 @@ "private": true, "devDependencies": { "@types/bun": "latest", - "prisma": "^6.6.0" + "prisma": "^6.7.0" }, "scripts": { - "dev": "bun --env-file=.env --hot src/index.ts" + "dev": "bun --env-file=.env --watch src/index.ts" }, "peerDependencies": { "typescript": "^5" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 74c83a6..56be57b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -21,4 +21,13 @@ model mcsBind { @@map("mcs_binds") @@index([channelId]) +} + +model brawlPlayer { + id String @id @default(auto()) @map("_id") @db.ObjectId + userId String + playerTag String + + @@map("brawl_players") + @@index([userId]) } \ No newline at end of file diff --git a/src/commands/brawlBind.ts b/src/commands/brawlBind.ts new file mode 100644 index 0000000..e6d3288 --- /dev/null +++ b/src/commands/brawlBind.ts @@ -0,0 +1,86 @@ +import { + Command, + Declare, + Options, + createStringOption, + type CommandContext +} from 'seyfert'; + +import { prisma } from '..'; + +const options = { + playertag: createStringOption({ + description: 'Your Brawl Stars player tag', + required: true, + }), +} + +@Declare({ + name: 'brawlbind', + description: 'Associate your Discord account with your Brawl Stars account', +}) +@Options(options) +export default class brawlBindCommand extends Command { + async run(ctx: CommandContext) { + let tag = ctx.options.playertag + if (!tag.startsWith("#")) tag = "#" + tag + const playerTagRegex = /^#[0-9A-Z]{6,10}$/ + if (!playerTagRegex.test(ctx.options.playertag)) { + await ctx.write({ + content: 'Invalid player tag (Example: `#A1B2C3D4E`)' + }) + return + } + const userId: string = ctx.author.id + console.log(tag + `\n` + userId) + const res = await prisma.brawlPlayer.findFirst({ + where: { + userId: userId + } + }) + if (res && res.playerTag !== tag) { + try { + await prisma.brawlPlayer.update({ + where: { + id: res.id, + userId: userId + }, + data: { + playerTag: tag + } + }) + } catch (e) { + await ctx.write({ + content: `${e}` + }) + return + } + await ctx.write({ + content: `Updated your playertag from ${res.playerTag} to \`${tag}\`` + }) + return + } else if (res && res.playerTag === tag) { + await ctx.write({ + content: `Nothing Changed, playertag already associated to \`${tag}\`` + }) + } + if (!res) { + try { + await prisma.brawlPlayer.create({ + data: { + userId: userId, + playerTag: tag + } + }) + } catch (e) { + await ctx.write({ + content: `${e}` + }) + } + await ctx.write({ + content: `Associated to your playertag \`${tag}\`` + }) + return + } + } +} \ No newline at end of file diff --git a/src/commands/brawlStats.ts b/src/commands/brawlStats.ts new file mode 100644 index 0000000..9de7160 --- /dev/null +++ b/src/commands/brawlStats.ts @@ -0,0 +1,143 @@ +import { + AttachmentBuilder, + Command, + Declare, + Embed, + Options, + createStringOption, + type CommandContext +} from 'seyfert'; + +import { prisma, browser } from '..'; + +interface BrawlPlayer { + name: string; + tag: string; + namecolor: string; + nameColor: string; + icon: any; + trophies: number; + highestTrophies: number; + expLevel: number; + expPoints: number; + isQualifiedFromChampionshipChallenge: boolean; + "3vs3Victories": number; + soloVictories: number; + duoVictories: number; + bestRoboRumbleTime: number; + bestTimeAsBigBrawler: number; + club: any; + brawlers: any[]; +} + +@Declare({ + name: 'brawlstats', + description: 'Query your Brawl Stars player stats' +}) + +export default class brawlStatsCommand extends Command { + async run(ctx: CommandContext) { + const res = await prisma.brawlPlayer.findFirst({ + where: { + userId: ctx.author.id + } + }) + if (!res) { + await ctx.write({ + content: `Your Discord account has not associated to a Brawl Stars playertag, use \`brawlbind\` to associate` + }) + return + } + let tag = res.playerTag.replace('#', '%23') + const data = await fetchBrawlPlayer(tag) as BrawlPlayer + if (!data || data === null) { + await ctx.write({ + content: `ERROR, CONTACT ADMIN` + }) + return + } + + const page = await browser.newPage(); + const result = generateBrawlStatsHtml(data) + + await page.setViewport({ width: 650, height: 200 }) + await page.setContent(result) + await page.waitForSelector('body') + const screenshot = await page.screenshot({ + encoding: 'base64', + type: 'webp', + fullPage: true + }) + await page.close() + + const buffer = Buffer.from(screenshot, 'base64') + await ctx.write({ + files: [ + new AttachmentBuilder() + .setName('brawlstats.png') + .setFile('buffer', buffer) + ] + }); + return + } +} + +function generateBrawlStatsHtml(data: BrawlPlayer): string { + const stats = [ + `Player: ${data.name}`, + `Trophies: ${data.trophies}`, + `3vs3 Victories: ${data['3vs3Victories']}`, + `Solo Victories: ${data.soloVictories}`, + `Duo Victories: ${data.duoVictories}`, + `Total Brawlers: ${data.brawlers.length}` + ].join('
') + + return ` + + + + + + + + + +
+
+ Brawl Stars Logo +
${stats}
+
+
+ + +`; +} + +async function fetchBrawlPlayer(playerTag: string) { + const brawlUrl = `https://api.brawlstars.com/v1/players/${playerTag}` + try { + const res = await fetch(brawlUrl, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${process.env.BRAWL_KEY}`, + 'Accept': 'application/json' + } + }) + if (!res.ok) { + console.error(`Error fetching Brawl Stars player data: ${res.statusText}`) + return null + } + return res.json() + } catch (e) { + console.error(e) + return null + } +} \ No newline at end of file