refactor(eslint)

This commit is contained in:
xhyrom 2022-01-02 18:54:19 +01:00
parent 85dbdfe907
commit a93f35b2ef
14 changed files with 2152 additions and 215 deletions

4
.eslintignore Normal file
View file

@ -0,0 +1,4 @@
dist/
tests/
src/web/
webpack.config.js

33
.eslintrc.json Normal file
View file

@ -0,0 +1,33 @@
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12
},
"plugins": [
"@typescript-eslint"
],
"rules": {
"indent": [
"error",
"tab"
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
],
"prefer-const": "error",
"@typescript-eslint/no-explicit-any": "off"
}
}

1909
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -13,7 +13,9 @@
"@cloudflare/workers-types": "^3.3.0", "@cloudflare/workers-types": "^3.3.0",
"@types/jest": "^27.0.3", "@types/jest": "^27.0.3",
"@types/service-worker-mock": "^2.0.1", "@types/service-worker-mock": "^2.0.1",
"@typescript-eslint/eslint-plugin": "^5.8.1",
"discord-api-types": "^0.25.2", "discord-api-types": "^0.25.2",
"eslint": "^8.6.0",
"service-worker-mock": "^2.0.5", "service-worker-mock": "^2.0.5",
"ts-loader": "^9.2.6", "ts-loader": "^9.2.6",
"typescript": "^4.5.4", "typescript": "^4.5.4",

View file

@ -3,138 +3,139 @@ import { isJSON } from './isJson';
import { isSnowflake } from './snowflakeUtils'; import { isSnowflake } from './snowflakeUtils';
import { verify } from './verify'; import { verify } from './verify';
const respond = (response: APIInteractionResponse) => new Response(JSON.stringify(response), {headers: {'content-type': 'application/json'}}) const respond = (response: APIInteractionResponse) => new Response(JSON.stringify(response), {headers: {'content-type': 'application/json'}});
const badFormatting = (rolesMax?: boolean) => { const badFormatting = (rolesMax?: boolean) => {
return respond({ return respond({
type: InteractionResponseType.ChannelMessageWithSource, type: InteractionResponseType.ChannelMessageWithSource,
data: { data: {
flags: 64, flags: 64,
content: `${rolesMax ? 'You can have maximum 25 buttons. (5x5)' : 'Bad formatting, generate [here](https://xhyrom.github.io/roles-bot)'}` content: `${rolesMax ? 'You can have maximum 25 buttons. (5x5)' : 'Bad formatting, generate [here](https://xhyrom.github.io/roles-bot)'}`
} }
}) });
} };
export const handleRequest = async(request: Request): Promise<Response> => { export const handleRequest = async(request: Request): Promise<Response> => {
if (!request.headers.get('X-Signature-Ed25519') || !request.headers.get('X-Signature-Timestamp')) return Response.redirect('https://www.youtube.com/watch?v=dQw4w9WgXcQ') if (!request.headers.get('X-Signature-Ed25519') || !request.headers.get('X-Signature-Timestamp')) return Response.redirect('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
if (!await verify(request)) return new Response('', { status: 401 }) if (!await verify(request)) return new Response('', { status: 401 });
const interaction = await request.json() as APIPingInteraction | APIApplicationCommandInteraction | APIMessageComponentInteraction; const interaction = await request.json() as APIPingInteraction | APIApplicationCommandInteraction | APIMessageComponentInteraction;
if (interaction.type === InteractionType.Ping) if (interaction.type === InteractionType.Ping)
return respond({ return respond({
type: InteractionResponseType.Pong type: InteractionResponseType.Pong
}) });
if (interaction.type === InteractionType.ApplicationCommand && interaction.data.name === 'setup') { if (interaction.type === InteractionType.ApplicationCommand && interaction.data.name === 'setup') {
// @ts-ignore
if ((interaction.member?.permissions & 0x10) !== 0x10) return respond({
type: InteractionResponseType.ChannelMessageWithSource,
data: {
flags: 64,
content: `Required permissions: \`MANAGE_ROLES\``
}
})
// @ts-ignore if ((Number(interaction.member?.permissions) & 0x10) !== 0x10) return respond({
const json = isJSON(interaction.data.options[0].value) ? JSON.parse(interaction.data.options[0].value) : null; type: InteractionResponseType.ChannelMessageWithSource,
data: {
flags: 64,
content: 'Required permissions: `MANAGE_ROLES`'
}
});
if (!json) return badFormatting(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const json = isJSON(interaction.data.options[0].value) ? JSON.parse(interaction.data.options[0].value) : null;
const channelId = json.channel; if (!json) return badFormatting();
const message = json.message?.toString();
let roles = json.roles;
if (!channelId) return badFormatting(); const channelId = json.channel;
if (!message) return badFormatting(); const message = json.message?.toString();
if (!roles || Object.values(json.roles).filter((role: any) => role.id && role.label).length === 0 || roles.length === 0 || roles.length > 25) return badFormatting(roles.length > 25); let roles = json.roles;
roles = roles.map((r: any) => { if (!channelId) return badFormatting();
let o: any = { if (!message) return badFormatting();
type: 2, if (!roles || Object.values(json.roles).filter((role: any) => role.id && role.label).length === 0 || roles.length === 0 || roles.length > 25) return badFormatting(roles.length > 25);
style: r.style || 2,
label: r.label,
custom_id: r.id
}
if (r.emoji) { roles = roles.map((r: any) => {
if (isSnowflake(r.emoji)) o.emoji = { id: r.emoji, name: null }; const o: any = {
else o.emoji = { id: null, name: r.emoji }; type: 2,
} style: r.style || 2,
label: r.label,
custom_id: r.id
};
return o; if (r.emoji) {
}) if (isSnowflake(r.emoji)) o.emoji = { id: r.emoji, name: null };
else o.emoji = { id: null, name: r.emoji };
}
const finalComponents = []; return o;
for (let i = 0; i <= roles.length; i += 5) { });
const row: any = {
type: 1,
components: []
}
const btnslice: any = roles.slice(i, i + 5); const finalComponents = [];
for (let i = 0; i <= roles.length; i += 5) {
const row: any = {
type: 1,
components: []
};
for (let y: number = 0; y < btnslice.length; y++) row.components.push(btnslice[y]); const btnslice: any = roles.slice(i, i + 5);
for (let y = 0; y < btnslice.length; y++) row.components.push(btnslice[y]);
finalComponents.push(row); finalComponents.push(row);
} }
await fetch(`${RouteBases.api}/channels/${channelId}/messages`, { await fetch(`${RouteBases.api}/channels/${channelId}/messages`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Authorization': `Bot ${CLIENT_TOKEN}`, 'Authorization': `Bot ${CLIENT_TOKEN}`,
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
content: message, content: message,
components: finalComponents components: finalComponents
}) })
}).catch(e => e) }).catch(e => e);
return respond({ return respond({
type: InteractionResponseType.ChannelMessageWithSource, type: InteractionResponseType.ChannelMessageWithSource,
data: { data: {
flags: 64, flags: 64,
content: 'Done!' content: 'Done!'
} }
}) });
} else if (interaction.type === InteractionType.MessageComponent) { } else if (interaction.type === InteractionType.MessageComponent) {
const roleId = interaction.data.custom_id; const roleId = interaction.data.custom_id;
const url = `${RouteBases.api}${Routes.guildMemberRole(interaction.guild_id || '', interaction.member?.user.id || '', roleId)}`; const url = `${RouteBases.api}${Routes.guildMemberRole(interaction.guild_id || '', interaction.member?.user.id || '', roleId)}`;
let method = ""; let method = '';
let content = ""; let content = '';
if (!interaction?.member?.roles?.includes(roleId)) { if (!interaction?.member?.roles?.includes(roleId)) {
content = `Gave the <@&${roleId}> role!`; content = `Gave the <@&${roleId}> role!`;
method = 'PUT'; method = 'PUT';
} else { } else {
content = `Removed the <@&${roleId}> role!`; content = `Removed the <@&${roleId}> role!`;
method = 'DELETE'; method = 'DELETE';
} }
await fetch(url, { await fetch(url, {
method: method, method: method,
headers: { headers: {
'Authorization': `Bot ${CLIENT_TOKEN}` 'Authorization': `Bot ${CLIENT_TOKEN}`
} }
}).catch(e => e); }).catch(e => e);
return respond({ return respond({
type: InteractionResponseType.ChannelMessageWithSource, type: InteractionResponseType.ChannelMessageWithSource,
data: { data: {
flags: MessageFlags.Ephemeral, flags: MessageFlags.Ephemeral,
content: content, content: content,
allowed_mentions: { parse: [] } allowed_mentions: { parse: [] }
} }
}) });
} }
return respond({ return respond({
type: InteractionResponseType.ChannelMessageWithSource, type: InteractionResponseType.ChannelMessageWithSource,
data: { data: {
flags: MessageFlags.Ephemeral, flags: MessageFlags.Ephemeral,
content: 'Beep boop, boop beep?' content: 'Beep boop, boop beep?'
} }
}) });
} };

View file

@ -1,5 +1,5 @@
import { handleRequest } from './bot' import { handleRequest } from './bot';
addEventListener('fetch', (event) => { addEventListener('fetch', (event) => {
event.respondWith(handleRequest(event.request)) event.respondWith(handleRequest(event.request));
}) });

View file

@ -1,11 +1,11 @@
export const isJSON = (data: any): boolean => { export const isJSON = (data: any): boolean => {
if (typeof data !== 'string') return false; if (typeof data !== 'string') return false;
try { try {
const result = JSON.parse(data); const result = JSON.parse(data);
const type = result.toString(); const type = result.toString();
return type === '[object Object]' || type === '[object Array]'; return type === '[object Object]' || type === '[object Array]';
} catch (err) { } catch (err) {
return false; return false;
} }
} };

View file

@ -1,6 +1,6 @@
export const toSnowflake = (snowflake: number, epoch = DISCORD_EPOCH) => { export const toSnowflake = (snowflake: number, epoch = DISCORD_EPOCH) => {
return new Date(snowflake / 4194304 + epoch) return new Date(snowflake / 4194304 + epoch);
} };
export const DISCORD_EPOCH = 1420070400000; export const DISCORD_EPOCH = 1420070400000;
@ -12,4 +12,4 @@ export const isSnowflake = (snowflake: number, epoch?: number) => {
if (isNaN(timestamp.getTime())) return false; if (isNaN(timestamp.getTime())) return false;
return true; return true;
} };

4
src/bot/types.d.ts vendored
View file

@ -1,2 +1,2 @@
declare const CLIENT_PUBLIC_KEY: string declare const CLIENT_PUBLIC_KEY: string;
declare const CLIENT_TOKEN: string declare const CLIENT_TOKEN: string;

View file

@ -3,35 +3,35 @@
'use strict'; 'use strict';
function hex2bin(hex: string) { function hex2bin(hex: string) {
const buf = new Uint8Array(Math.ceil(hex.length / 2)); const buf = new Uint8Array(Math.ceil(hex.length / 2));
for (var i = 0; i < buf.length; i++) { for (let i = 0; i < buf.length; i++) {
buf[i] = parseInt(hex.substr(i * 2, 2), 16); buf[i] = parseInt(hex.substr(i * 2, 2), 16);
} }
return buf; return buf;
} }
const PUBLIC_KEY = crypto.subtle.importKey( const PUBLIC_KEY = crypto.subtle.importKey(
'raw', 'raw',
hex2bin(CLIENT_PUBLIC_KEY || ''), hex2bin(CLIENT_PUBLIC_KEY || ''),
{ {
name: 'NODE-ED25519', name: 'NODE-ED25519',
namedCurve: 'NODE-ED25519', namedCurve: 'NODE-ED25519',
}, },
true, true,
['verify'], ['verify'],
); );
const encoder = new TextEncoder(); const encoder = new TextEncoder();
export async function verify(request: Request) { export async function verify(request: Request) {
const signature = hex2bin(request.headers.get('X-Signature-Ed25519')!); const signature = hex2bin(request.headers.get('X-Signature-Ed25519') || '');
const timestamp = request.headers.get('X-Signature-Timestamp'); const timestamp = request.headers.get('X-Signature-Timestamp');
const unknown = await request.clone().text(); const unknown = await request.clone().text();
return await crypto.subtle.verify( return await crypto.subtle.verify(
'NODE-ED25519', 'NODE-ED25519',
await PUBLIC_KEY, await PUBLIC_KEY,
signature, signature,
encoder.encode(timestamp + unknown), encoder.encode(timestamp + unknown),
); );
} }

View file

@ -2,5 +2,5 @@ import '../styles/css/style.css';
import type { AppProps } from 'next/app'; import type { AppProps } from 'next/app';
export default function MyApp({ Component, pageProps }: AppProps) { export default function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} /> return <Component {...pageProps} />;
} }

View file

@ -1,34 +1,34 @@
import Head from "next/head"; import Head from 'next/head';
import Script from "next/script"; import Script from 'next/script';
export default function Home() { export default function Home() {
return ( return (
<div> <div>
<Head> <Head>
<title>Roles Bot</title> <title>Roles Bot</title>
<link rel="icon" href="logo.ico" /> <link rel="icon" href="logo.ico" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.3.1/styles/default.min.css" /> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.3.1/styles/default.min.css" />
<script src="https://kit.fontawesome.com/5acf4d9e80.js" crossOrigin="anonymous"></script> <script src="https://kit.fontawesome.com/5acf4d9e80.js" crossOrigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.3.1/highlight.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.3.1/highlight.min.js"></script>
</Head> </Head>
<section className="flex-container"> <section className="flex-container">
<div className="container animate__animated animate__fadeIn"> <div className="container animate__animated animate__fadeIn">
<h1>Generate</h1> <h1>Generate</h1>
<form> <form>
<input placeholder="Your Message" name="message" id="message"/><br /> <input placeholder="Your Message" name="message" id="message"/><br />
<input placeholder="Channel Id" name="channel" id="channel"/> <input placeholder="Channel Id" name="channel" id="channel"/>
</form> </form>
<button id="addRole">Add Role</button> <button id="addRole">Add Role</button>
<button id="buttonCopy">Copy</button> <button id="buttonCopy">Copy</button>
<pre className={`hljs language-json copy`} id="jsonPre"><code id="json" className="code"></code></pre> <pre className={'hljs language-json copy'} id="jsonPre"><code id="json" className="code"></code></pre>
</div> </div>
</section> </section>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<Script src="script.js"></Script> <Script src="script.js"></Script>
<Script id='hljs'>hljs.initHighlightingOnLoad();</Script> <Script id='hljs'>hljs.initHighlightingOnLoad();</Script>
</div> </div>
) );
} }

View file

@ -1,8 +1,8 @@
module.exports = { module.exports = {
"message": "lol", 'message': 'lol',
"channel": "862700556438732851", 'channel': '862700556438732851',
"roles": [ 'roles': [
{"id": "777805077825060867","label":"Bots","emoji":"😦"}, {'id': '777805077825060867','label':'Bots','emoji':'😦'},
{"id":"922762668841009152","label":"Ping","emoji":"🤩"} {'id':'922762668841009152','label':'Ping','emoji':'🤩'}
] ]
} };

View file

@ -3,25 +3,25 @@ const path = require('path');
const mode = process.env.NODE_ENV || 'production'; const mode = process.env.NODE_ENV || 'production';
module.exports = { module.exports = {
output: { output: {
filename: `worker.${mode}.js`, filename: `worker.${mode}.js`,
path: path.join(__dirname, 'dist'), path: path.join(__dirname, 'dist'),
}, },
mode, mode,
resolve: { resolve: {
extensions: ['.ts', '.tsx', '.js'], extensions: ['.ts', '.tsx', '.js'],
plugins: [], plugins: [],
fallback: { util: false } fallback: { util: false }
}, },
module: { module: {
rules: [ rules: [
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
loader: 'ts-loader', loader: 'ts-loader',
options: { options: {
transpileOnly: true, transpileOnly: true,
}, },
}, },
], ],
}, },
} };