Skip to content

Commit

Permalink
feat: Vote annoucements, and storage of topgg votes in db
Browse files Browse the repository at this point in the history
  • Loading branch information
dev-737 committed Jan 29, 2024
1 parent aaa0382 commit bc7b775
Show file tree
Hide file tree
Showing 26 changed files with 1,203 additions and 253 deletions.
910 changes: 820 additions & 90 deletions package-lock.json

Large diffs are not rendered by default.

17 changes: 10 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@
"type": "module",
"dependencies": {
"@prisma/client": "^5.8.1",
"@sentry/node": "^7.94.1",
"@sentry/node": "^7.98.0",
"@tensorflow/tfjs-node": "^4.16.0",
"@top-gg/sdk": "^3.1.6",
"common-tags": "^1.8.2",
"discord-arts": "^0.5.8",
"discord-hybrid-sharding": "^2.1.4",
"discord.js": "^14.14.1",
"dotenv": "^16.3.2",
"dotenv": "^16.4.1",
"express": "^4.18.2",
"google-translate-api-x": "^10.6.8",
"i18n": "^0.15.1",
"lodash": "^4.17.21",
Expand All @@ -40,22 +42,23 @@
},
"devDependencies": {
"@types/common-tags": "^1.8.4",
"@types/express": "^4.17.21",
"@types/i18n": "^0.13.10",
"@types/jest": "^29.5.11",
"@types/lodash": "^4.14.202",
"@types/node": "^20.11.5",
"@types/node": "^20.11.10",
"@types/source-map-support": "^0.5.10",
"@typescript-eslint/eslint-plugin": "^6.19.0",
"@typescript-eslint/parser": "^6.19.0",
"@typescript-eslint/eslint-plugin": "^6.19.1",
"@typescript-eslint/parser": "^6.19.1",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^8.56.0",
"husky": "^8.0.3",
"husky": "^9.0.6",
"jest": "^29.7.0",
"lint-staged": "^15.2.0",
"prettier": "^3.2.4",
"prisma": "^5.8.1",
"standard-version": "^9.5.0",
"ts-jest": "^29.1.1",
"ts-jest": "^29.1.2",
"tsc-watch": "^6.0.4",
"typescript": "^5.3.3"
},
Expand Down
5 changes: 4 additions & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,11 @@ model blacklistedUsers {
model userData {
id String @id @default(auto()) @map("_id") @db.ObjectId
userId String @unique
username String
// username is only guarenteed to be set and/or used for blacklisted users
username String?
locale String?
lastVoted Int?
voteCount Int @default(0)
blacklistedFrom hubBlacklist[]
// if user has seen the welcome message when they first use the network
viewedNetworkWelcome Boolean @default(false)
Expand Down
6 changes: 3 additions & 3 deletions src/SuperClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export default abstract class SuperClient extends Client {
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
resolveEval = <T>(value: any[]): T | undefined => value?.find((res) => !!res);
static resolveEval = <T>(value: T[]) => value?.find((res) => !!res) as RemoveMethods<T> | undefined;

/**
* Fetches a guild by its ID from the cache.
Expand All @@ -127,9 +127,9 @@ export default abstract class SuperClient extends Client {
const fetch = await this.cluster.broadcastEval(
(client, guildID) => client.guilds.cache.get(guildID),
{ context: guildId },
);
) as Guild[];

return fetch ? this.resolveEval(fetch) : undefined;
return fetch ? SuperClient.resolveEval(fetch) : undefined;
}

async getUserLocale(userId: Snowflake): Promise<string> {
Expand Down
67 changes: 13 additions & 54 deletions src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,17 @@
import http from 'http';
import { node } from '@tensorflow/tfjs-node';
import { load } from 'nsfwjs';
import { VoteManager } from '../managers/VoteManager.js';
import Logger from '../utils/Logger.js';
import { captureException } from '@sentry/node';
import express from 'express';
import dblRoute from './routes/dbl.js';
import nsfwRouter from './routes/nsfw.js';

const model = await load();
const port = 3000;
// to start the server
export const startApi = (data: { voteManager: VoteManager }) => {
const app = express();

export default function start() {
const server = http.createServer(async (req, res) => {
if (req.method === 'GET' && req.url?.startsWith('/nsfw')) {
const url = new URL(req.url, `http://${req.headers.host}`);
const imageUrl = url.searchParams.get('url');
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(nsfwRouter);
if (data.voteManager) app.use(dblRoute(data.voteManager));

if (!imageUrl || typeof imageUrl !== 'string') {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Missing url query parameter.' }));
return;
}

const regex = /\bhttps?:\/\/\S+?\.(?:png|jpe?g)(?:\?\S+)?\b/;
if (!regex.test(imageUrl)) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
error: 'Invalid url parameter. Must be a valid PNG, JPG, or JPEG image URL.',
}),
);
return;
}

try {
const imageBuffer = await (await fetch(imageUrl)).arrayBuffer();
const imageTensor = (await node.decodeImage(Buffer.from(imageBuffer), 3)) as any; // eslint-disable-line @typescript-eslint/no-explicit-any
const predictions = await model.classify(imageTensor);
imageTensor.dispose();

res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(predictions));
}
catch (error) {
Logger.error(error);
captureException(error);
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('500 Internal Server Error');
}
}
else {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Why the hell are we even here? This is a 404.');
}
});

server.listen(port, () => {
Logger.info(`API listening on port http://localhost:${port}.`);
});
}
app.listen(443, () => Logger.info('API listening on port http://localhost:443.'));
};
19 changes: 19 additions & 0 deletions src/api/routes/dbl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Webhook } from '@top-gg/sdk';
import { Router } from 'express';
import { VoteManager } from '../../managers/VoteManager.js';

// NOTE: testing this against top.gg only works in the production server
// to test locally use postman or something similar to send a POST request to http://localhost:443/dbl
const router: Router = Router();
const TopggWebhook = new Webhook(process.env.TOPGG_AUTH);

export default (voteManager: VoteManager) => {
router.post(
'/dbl',
TopggWebhook.listener((vote) => {
// emit the vote event to use in other files
voteManager?.emit('vote', vote);
}),
);
return router;
};
48 changes: 48 additions & 0 deletions src/api/routes/nsfw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { load } from 'nsfwjs';
import { Router } from 'express';
import { captureException } from '@sentry/node';
import { node } from '@tensorflow/tfjs-node';
import Logger from '../../utils/Logger.js';

const nsfwModel = await load();
const router: Router = Router();

router.get('/nsfw', async (req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const imageUrl = url.searchParams.get('url');

if (!imageUrl || typeof imageUrl !== 'string') {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Missing url query parameter.' }));
return;
}

const regex = /\bhttps?:\/\/\S+?\.(?:png|jpe?g)(?:\?\S+)?\b/;
if (!regex.test(imageUrl)) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
error: 'Invalid url parameter. Must be a valid PNG, JPG, or JPEG image URL.',
}),
);
return;
}

try {
const imageBuffer = await (await fetch(imageUrl)).arrayBuffer();
const imageTensor = (await node.decodeImage(Buffer.from(imageBuffer), 3)) as any; // eslint-disable-line @typescript-eslint/no-explicit-any
const predictions = await nsfwModel.classify(imageTensor);
imageTensor.dispose();

res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(predictions));
}
catch (error) {
Logger.error(error);
captureException(error);
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('500 Internal Server Error');
}
});

export default router;
2 changes: 1 addition & 1 deletion src/commands/slash/Main/blacklist/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ export default class BlacklistCommand extends BaseCommand {
});

const choices = filteredUsers.map((user) => {
return { name: user.username, value: user.userId };
return { name: user.username ?? `Unknown User - ${user.userId}`, value: user.userId };
});
interaction.respond(choices);
break;
Expand Down
2 changes: 1 addition & 1 deletion src/commands/slash/Main/blacklist/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export default class ListBlacklists extends BlacklistCommand {
: null;

fields.push({
name: data.username,
name: `${data.username}`,
value: t(
{ phrase: 'blacklist.list.user', locale: interaction.user.locale },
{
Expand Down
2 changes: 1 addition & 1 deletion src/commands/slash/Main/blacklist/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export default class Server extends BlacklistCommand {
await interaction.followUp(
t(
{ phrase: 'blacklist.user.removed', locale: interaction.user.locale },
{ emoji: emojis.delete, username: result.username },
{ emoji: emojis.delete, username: `${result.username}` },
),
);
if (user) {
Expand Down
3 changes: 2 additions & 1 deletion src/commands/slash/Main/hub/servers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { paginate } from '../../../../utils/Pagination.js';
import db from '../../../../utils/Db.js';
import { simpleEmbed } from '../../../../utils/Utils.js';
import { t } from '../../../../utils/Locale.js';
import SuperClient from '../../../../SuperClient.js';

export default class Servers extends Hub {
async execute(interaction: ChatInputCommandInteraction) {
Expand Down Expand Up @@ -105,7 +106,7 @@ export default class Servers extends Hub {
{ context: { connection } },
);

const evalRes = interaction.client.resolveEval(evalArr);
const evalRes = SuperClient.resolveEval(evalArr);

const value = t(
{ phrase: 'hub.servers.connectionInfo', locale },
Expand Down
3 changes: 2 additions & 1 deletion src/commands/slash/Staff/purge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { emojis } from '../../../utils/Constants.js';
import { simpleEmbed, msToReadable, deleteMsgsFromDb } from '../../../utils/Utils.js';
import Logger from '../../../utils/Logger.js';
import { broadcastedMessages } from '@prisma/client';
import SuperClient from '../../../SuperClient.js';

const limitOpt: APIApplicationCommandBasicOption = {
type: ApplicationCommandOptionType.Integer,
Expand Down Expand Up @@ -236,7 +237,7 @@ export default class Purge extends BaseCommand {
{ context: { channelId: network.channelId, messagesInDb } },
);

return interaction.client.resolveEval(evalRes) || [];
return SuperClient.resolveEval(evalRes) || [];
}
catch (e) {
Logger.error(e);
Expand Down
Loading

0 comments on commit bc7b775

Please sign in to comment.