Compare commits
No commits in common. "main" and "v2" have entirely different histories.
23 changed files with 0 additions and 1979 deletions
|
@ -1,7 +0,0 @@
|
|||
# NODE_ENV=development
|
||||
HOST=0.0.0.0
|
||||
PORT=8080
|
||||
|
||||
DISCORD_TOKEN=YOUR_DISCORD_BOT_TOKEN
|
||||
DISCORD_GUILD_ID=YOUR_DISCORD_GUILD_ID
|
||||
DISCORD_USER_ID=YOUR_DISCORD_USER_ID
|
179
.gitignore
vendored
179
.gitignore
vendored
|
@ -1,179 +0,0 @@
|
|||
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||
|
||||
# Logs
|
||||
|
||||
logs
|
||||
_.log
|
||||
npm-debug.log_
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Caches
|
||||
|
||||
.cache
|
||||
|
||||
# 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/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
.stylelintcache
|
||||
|
||||
# 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 variable files
|
||||
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
|
||||
# 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
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
/.vscode
|
||||
bun.lockb
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
21
LICENCE
21
LICENCE
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024 Creation's // [creations@creations.works]
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -1,43 +0,0 @@
|
|||
import { dirname, join } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
import type{ IEnvironment } from "../src/interfaces/environment";
|
||||
|
||||
const __dirname : string = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
const environment : IEnvironment = {
|
||||
development: process.env.NODE_ENV === "development" || process.argv.includes("--dev"),
|
||||
|
||||
fastify: {
|
||||
host: process.env.HOST || "0.0.0.0",
|
||||
port: parseInt(process.env.PORT || "8080"),
|
||||
},
|
||||
|
||||
discord: {
|
||||
auth: {
|
||||
token: process.env.DISCORD_TOKEN || "",
|
||||
guildId: process.env.DISCORD_GUILD_ID || "",
|
||||
},
|
||||
watchIds: [
|
||||
(() => {
|
||||
const id = process.env.DISCORD_USER_ID;
|
||||
if (id && /^\d+$/.test(id)) {
|
||||
return BigInt(id);
|
||||
}
|
||||
return BigInt(0);
|
||||
})()
|
||||
],
|
||||
},
|
||||
|
||||
paths: {
|
||||
src: __dirname,
|
||||
www: {
|
||||
root: join(__dirname, "src", "www"),
|
||||
views: join(__dirname, "src", "www", "views"),
|
||||
public: join(__dirname, "src", "www", "public")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default environment;
|
||||
export { environment };
|
25
package.json
25
package.json
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"name": "ts_fastify_example",
|
||||
"module": "src/index.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun run --watch src/index.ts --development",
|
||||
"prod": "bun run src/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/ejs": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "latest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/static": "latest",
|
||||
"@fastify/view": "latest",
|
||||
"@fastify/websocket": "latest",
|
||||
"ejs": "latest",
|
||||
"fastify": "latest",
|
||||
"oceanic.js": "latest",
|
||||
"undici": "^6.20.1"
|
||||
}
|
||||
}
|
|
@ -1,179 +0,0 @@
|
|||
import { request as uRequest } from "undici";
|
||||
import { discordBot } from "../helpers/discord.bot";
|
||||
|
||||
import type { FastifyRequest } from "fastify";
|
||||
import type { Presence } from "oceanic.js";
|
||||
import type { IRouteInfo } from "../interfaces/routes";
|
||||
|
||||
const routeInfo: IRouteInfo = {
|
||||
enabled: true,
|
||||
path: "/status",
|
||||
method: "GET",
|
||||
websocket: true
|
||||
};
|
||||
|
||||
async function route(socket: WebSocket, _request: FastifyRequest): Promise<void> {
|
||||
discordBot.addWebSocket(socket);
|
||||
|
||||
socket.on("message", async (message: string) => {
|
||||
try {
|
||||
const data = JSON.parse(message);
|
||||
|
||||
if (data.type === "getStatus") {
|
||||
let userPresences = Array.from(discordBot.getUserPresences()).reduce((acc, [key, value]) => {
|
||||
acc[key] = value;
|
||||
return acc;
|
||||
}, {} as Record<string, Presence>);
|
||||
|
||||
for (const [userId, presence] of Object.entries(userPresences)) {
|
||||
if (presence.activities) {
|
||||
for (let index = 0; index < presence.activities.length; index++) {
|
||||
const activity = presence.activities[index];
|
||||
|
||||
if (activity && activity.type === 0 && (!activity.assets || !activity.assets.largeImage)) {
|
||||
const gameName = activity.name;
|
||||
|
||||
const iconUrl = await getSteamGameIcon(gameName);
|
||||
|
||||
if (iconUrl) {
|
||||
presence.activities[index].assets = {
|
||||
largeImage: iconUrl,
|
||||
largeText: gameName
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
socket.send(JSON.stringify({
|
||||
type: "statusUpdate",
|
||||
status: userPresences
|
||||
}));
|
||||
} else if (data.type === "getUsers") {
|
||||
const usersInfo = Array.from(discordBot.getUsersInfo()).reduce((acc, [key, member]) => {
|
||||
if (member && member.user) {
|
||||
acc[key] = {
|
||||
id: member.user.id,
|
||||
username: member.user.username,
|
||||
discriminator: member.user.discriminator,
|
||||
nick: member.nick ?? member.user.username,
|
||||
avatar: member.user.avatar
|
||||
? `https://cdn.discordapp.com/avatars/${member.user.id}/${member.user.avatar}`
|
||||
: `https://cdn.discordapp.com/embed/avatars/${parseInt(member.user.discriminator) % 5}`,
|
||||
avatarDecoration: member.user.avatarDecoration
|
||||
? `https://cdn.discordapp.com/avatar-decoration-presets/${member.user.avatarDecoration}`
|
||||
: null,
|
||||
banner: member.user.banner
|
||||
? `https://cdn.discordapp.com/banners/${member.user.id}/${member.user.banner}`
|
||||
: null,
|
||||
roles: member.roles,
|
||||
joinedAt: member.joinedAt,
|
||||
premiumSince: member.premiumSince,
|
||||
user: member.user
|
||||
};
|
||||
} else {
|
||||
console.warn(`Invalid member data for key: ${key}`, member);
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, any>);
|
||||
|
||||
socket.send(JSON.stringify({
|
||||
type: "usersUpdate",
|
||||
users: usersInfo
|
||||
}));
|
||||
} else if (data.type === "getBadges" && data.userId) {
|
||||
let badgesFull: any[] = [];
|
||||
const vencordLink = "https://badges.vencord.dev/badges.json";
|
||||
const equicordLink = "https://raw.githubusercontent.com/Equicord/Equibored/refs/heads/main/badges.json";
|
||||
|
||||
try {
|
||||
const response = await uRequest(equicordLink);
|
||||
const text = await response.body.text();
|
||||
const body: any = JSON.parse(text);
|
||||
|
||||
if (!body) return;
|
||||
|
||||
const badges = body[data.userId];
|
||||
if (badges && Array.isArray(badges)) {
|
||||
badgesFull.push(...badges);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching Equicord badges:", error);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await uRequest(vencordLink);
|
||||
const body: any = await response.body.json();
|
||||
|
||||
if (!body) return;
|
||||
|
||||
const badges = body[data.userId];
|
||||
if (badges && Array.isArray(badges)) {
|
||||
badgesFull.push(...badges);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching Vencord badges:", error);
|
||||
}
|
||||
|
||||
socket.send(JSON.stringify({
|
||||
type: "badgesUpdate",
|
||||
badges: badgesFull
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing message", error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("close", () => {
|
||||
discordBot.removeWebSocket(socket);
|
||||
});
|
||||
}
|
||||
|
||||
let steamAppListCache: { data: any; timestamp: number } | null = null;
|
||||
const CACHE_DURATION = 60 * 60 * 1000; // 1 hour in milliseconds
|
||||
|
||||
async function getSteamAppList() {
|
||||
if (steamAppListCache && (Date.now() - steamAppListCache.timestamp < CACHE_DURATION)) {
|
||||
return steamAppListCache.data;
|
||||
}
|
||||
|
||||
const response = await fetch(`https://api.steampowered.com/ISteamApps/GetAppList/v2/`);
|
||||
const data = await response.json();
|
||||
|
||||
steamAppListCache = { data, timestamp: Date.now() };
|
||||
return data;
|
||||
}
|
||||
|
||||
async function getSteamAppID(gameName: string) {
|
||||
const appList = await getSteamAppList();
|
||||
const app = appList.applist.apps.find((app: any) => app.name.toLowerCase() === gameName.toLowerCase());
|
||||
return app ? app.appid : null;
|
||||
}
|
||||
|
||||
export async function getSteamGameIcon(gameName: string) {
|
||||
const appID = await getSteamAppID(gameName);
|
||||
|
||||
if (!appID) {
|
||||
console.error("Game not found on Steam.");
|
||||
return;
|
||||
}
|
||||
|
||||
const iconHashResponse = await fetch(`https://store.steampowered.com/api/appdetails?appids=${appID}`);
|
||||
const iconHashData = await iconHashResponse.json();
|
||||
const gameData = iconHashData[appID].data;
|
||||
|
||||
const iconUrl = gameData.icon
|
||||
? `https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/${appID}/${gameData.icon}.jpg`
|
||||
: gameData.capsule_image || gameData.header_image;
|
||||
|
||||
if (iconUrl) {
|
||||
return iconUrl;
|
||||
} else {
|
||||
console.error("Icon not available.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default { routeInfo, route };
|
|
@ -1,176 +0,0 @@
|
|||
import ejs from "ejs";
|
||||
import Fastify from "fastify";
|
||||
|
||||
import { readdir, stat } from "fs/promises";
|
||||
import { IncomingMessage, Server, ServerResponse } from "http";
|
||||
import { join } from "path";
|
||||
|
||||
import { environment } from "../../config/environment";
|
||||
|
||||
// types
|
||||
import type { FastifyInstance, FastifyRequest } from "fastify";
|
||||
import type { Stats } from "fs";
|
||||
import type { AddressInfo } from "net";
|
||||
|
||||
// official plugins
|
||||
import { fastifyStatic } from "@fastify/static";
|
||||
import { fastifyView } from "@fastify/view";
|
||||
import fastifyWebSocket from '@fastify/websocket';
|
||||
|
||||
//custom plugins
|
||||
import faviconPlugin from "./plugins/favicon";
|
||||
|
||||
class FastifyManager {
|
||||
private readonly port: number = environment.fastify.port;
|
||||
private readonly server: FastifyInstance<Server, IncomingMessage, ServerResponse>;
|
||||
|
||||
constructor() {
|
||||
this.server = Fastify({
|
||||
logger: false,
|
||||
trustProxy: !environment.development,
|
||||
ignoreTrailingSlash: true
|
||||
});
|
||||
}
|
||||
private async registerPlugins(): Promise<void> {
|
||||
// official plugins
|
||||
this.server.register(fastifyView, {
|
||||
engine: {
|
||||
ejs
|
||||
},
|
||||
root: environment.paths.www.views,
|
||||
includeViewExtension: true
|
||||
});
|
||||
|
||||
this.server.register(fastifyStatic, {
|
||||
root: environment.paths.www.public,
|
||||
prefix: "/public/",
|
||||
prefixAvoidTrailingSlash: true
|
||||
});
|
||||
|
||||
await this.server.register(fastifyWebSocket, {
|
||||
options: {
|
||||
maxPayload: 1048576
|
||||
}
|
||||
});
|
||||
|
||||
// custom plugins
|
||||
this.server.register(faviconPlugin, { path: join(environment.paths.www.public, "assets", "favicon.gif") });
|
||||
}
|
||||
|
||||
// dynamic route loading
|
||||
private async loadRoutes(): Promise<void> {
|
||||
const routePaths: [string, string, boolean][] = [
|
||||
["routes", "/", false],
|
||||
["../api", "/api", true]
|
||||
];
|
||||
|
||||
for (const [routePath, prefix, recursive] of routePaths) {
|
||||
const modifiedRoutePath: string = join(environment.paths.www.root, routePath);
|
||||
|
||||
let files: string[];
|
||||
try {
|
||||
files = await this.readDirRecursive(modifiedRoutePath, recursive);
|
||||
} catch (err) {
|
||||
console.error("Failed to read route directory", modifiedRoutePath, "error:", err);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const routeModule = await import(file);
|
||||
const { default: routeData } = routeModule;
|
||||
|
||||
if (!routeData || !routeData.routeInfo || !routeData.route) {
|
||||
console.error(`Failed to load route from ${file}:`, "Route data is missing");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (routeData.routeInfo.enabled && routeData.route) {
|
||||
const { routeInfo, route } = routeData;
|
||||
|
||||
let routePath = routeInfo.path || "/";
|
||||
|
||||
if (prefix) {
|
||||
routePath = routePath === "/" ? prefix : join(prefix, routePath);
|
||||
}
|
||||
|
||||
routePath = routePath.replace(/\\/g, '/');
|
||||
|
||||
if (!routePath.startsWith('/')) {
|
||||
routePath = '/' + routePath;
|
||||
}
|
||||
|
||||
routePath = routePath.replace(/\/+/g, "/");
|
||||
const methods = Array.isArray(routeInfo.method) ? routeInfo.method : [routeInfo.method];
|
||||
|
||||
for (const method of methods) {
|
||||
if (routeInfo.websocket) {
|
||||
this.server.get(routePath, { websocket: true }, function wsHandler(socket: any, req: FastifyRequest) {
|
||||
route(socket, req);
|
||||
});
|
||||
continue;
|
||||
} else {
|
||||
this.server.route({
|
||||
method,
|
||||
url: routePath,
|
||||
handler: route
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to load route from ${file}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async readDirRecursive(dir: string, recursive: boolean): Promise<string[]> {
|
||||
let results: string[] = [];
|
||||
const list: string[] = await readdir(dir);
|
||||
|
||||
for (const file of list) {
|
||||
const filePath: string = join(dir, file);
|
||||
const statObj: Stats = await stat(filePath);
|
||||
if (statObj && statObj.isDirectory()) {
|
||||
if (recursive) {
|
||||
results = results.concat(await this.readDirRecursive(filePath, recursive));
|
||||
}
|
||||
} else {
|
||||
results.push(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
try {
|
||||
await this.registerPlugins();
|
||||
await this.loadRoutes();
|
||||
|
||||
await this.server.listen({
|
||||
port: environment.fastify.port,
|
||||
host: environment.fastify.host
|
||||
});
|
||||
|
||||
const [_address, _port, scheme]: [string, number, string] = ((): [string, number, string] => {
|
||||
const address: string | AddressInfo | null = this.server.server.address();
|
||||
const resolvedAddress: [string, number] = (address && typeof address === 'object') ? [(address.address.startsWith("::") || address.address === "0.0.0.0") ? "localhost" : address.address, address.port] : ["localhost", this.port];
|
||||
const resolvedScheme: string = resolvedAddress[0] === "localhost" ? "http" : "https";
|
||||
|
||||
return [...resolvedAddress, resolvedScheme];
|
||||
})();
|
||||
|
||||
|
||||
console.info("Started Listening on", `${scheme}://${_address}:${_port}`);
|
||||
console.info("Registered routes: ", "\n", this.server.printRoutes());
|
||||
} catch (error) {
|
||||
console.error("Failed to start Fastify server", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fastifyManager = new FastifyManager();
|
||||
export default fastifyManager;
|
||||
export { fastifyManager };
|
|
@ -1,35 +0,0 @@
|
|||
import type { FastifyInstance, FastifyPluginAsync } from "fastify";
|
||||
import type { FastifyReply } from "fastify/types/reply";
|
||||
import type { FastifyRequest } from "fastify/types/request";
|
||||
import { readFile } from "fs/promises";
|
||||
|
||||
interface FaviconOptions {
|
||||
path: string;
|
||||
}
|
||||
|
||||
const faviconPlugin: FastifyPluginAsync<FaviconOptions> = async (fastify: FastifyInstance, options: FaviconOptions): Promise<void> => {
|
||||
let faviconData: Buffer | null = null;
|
||||
|
||||
try {
|
||||
faviconData = await readFile(options.path);
|
||||
} catch (err) {
|
||||
console.error("Error reading favicon:", err);
|
||||
}
|
||||
|
||||
fastify.get("/favicon.ico", async (_request: FastifyRequest, reply: FastifyReply): Promise<void> => {
|
||||
if (faviconData) {
|
||||
const contentType = options.path.endsWith(".gif") ? "image/gif" : "image/x-icon";
|
||||
reply.header("Content-Type", contentType)
|
||||
.header("Cache-Control", "public, max-age=86400") // 1 day
|
||||
.send(faviconData);
|
||||
} else {
|
||||
reply.status(404).send({
|
||||
code: 404,
|
||||
error: "FILE_NOT_FOUND",
|
||||
message: "Favicon not found"
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default faviconPlugin as FastifyPluginAsync<FaviconOptions>;
|
|
@ -1,212 +0,0 @@
|
|||
import { Client, Guild, Member, type JSONMember, type Presence, type Uncached } from "oceanic.js";
|
||||
import { environment } from "../../config/environment";
|
||||
import { getSteamGameIcon } from "../api/status";
|
||||
|
||||
class DiscordBot {
|
||||
private client: Client;
|
||||
private watchIds: string[] = environment.discord.watchIds.map((id) => id.toString());
|
||||
private userPresences: Map<string, Presence>;
|
||||
private connectedSockets: Set<WebSocket>;
|
||||
private usersInfo: Map<string, any>;
|
||||
|
||||
constructor() {
|
||||
this.client = new Client({
|
||||
auth: `Bot ${environment.discord.auth.token}`,
|
||||
gateway: {
|
||||
intents: ["GUILDS", "GUILD_PRESENCES"]
|
||||
}
|
||||
});
|
||||
|
||||
this.userPresences = new Map();
|
||||
this.usersInfo = new Map();
|
||||
this.connectedSockets = new Set();
|
||||
this.watchIds = environment.discord.watchIds.map((id) => id.toString());
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
try {
|
||||
await this.client.connect();
|
||||
await this.setupListeners();
|
||||
} catch (error) {
|
||||
console.error("Failed to connect to Discord: ", error);
|
||||
}
|
||||
}
|
||||
|
||||
public async stop(): Promise<void> {
|
||||
try {
|
||||
this.client.disconnect();
|
||||
} catch (error) {
|
||||
console.error("Failed to disconnect from Discord: ", error);
|
||||
}
|
||||
}
|
||||
|
||||
public getClient(): Client {
|
||||
return this.client;
|
||||
}
|
||||
|
||||
public getUserPresences(): Map<string, Presence> {
|
||||
return this.userPresences;
|
||||
}
|
||||
|
||||
public getUsersInfo(): Map<string, any> {
|
||||
return this.usersInfo;
|
||||
}
|
||||
|
||||
public addWebSocket(socket: WebSocket): void {
|
||||
this.connectedSockets.add(socket);
|
||||
}
|
||||
|
||||
public removeWebSocket(socket: WebSocket): void {
|
||||
this.connectedSockets.delete(socket);
|
||||
}
|
||||
|
||||
private async broadcastPresenceUpdate(userId: string, presence: Presence): Promise<void> {
|
||||
const updatedActivities = presence.activities
|
||||
? await Promise.all(
|
||||
presence.activities.map(async (activity) => {
|
||||
if (activity && activity.type === 0 && (!activity.assets || !activity.assets.largeImage)) {
|
||||
const gameName = activity.name;
|
||||
// should use what discord uses but i dont care enough to switch it out
|
||||
const iconUrl = await getSteamGameIcon(gameName);
|
||||
|
||||
if (iconUrl) {
|
||||
return {
|
||||
...activity,
|
||||
assets: {
|
||||
largeImage: iconUrl,
|
||||
largeText: gameName
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
return activity;
|
||||
})
|
||||
)
|
||||
: [];
|
||||
|
||||
const updateMessage = JSON.stringify({
|
||||
type: "presenceUpdate",
|
||||
userId,
|
||||
presence: {
|
||||
status: presence.status,
|
||||
activities: updatedActivities
|
||||
}
|
||||
});
|
||||
|
||||
this.broadcastToWebSockets(updateMessage);
|
||||
}
|
||||
|
||||
private async fetchUsersInfo(): Promise<void> {
|
||||
const guild = this.client.guilds.get(environment.discord.auth.guildId);
|
||||
|
||||
if (!guild) return;
|
||||
|
||||
for (const id of this.watchIds) {
|
||||
try {
|
||||
const member = await guild.getMember(id);
|
||||
if (member) {
|
||||
const memberData = this.serializeMember(member);
|
||||
this.usersInfo.set(member.user.id, memberData);
|
||||
|
||||
const updateMessage = JSON.stringify({
|
||||
type: "memberUpdate",
|
||||
user: memberData
|
||||
});
|
||||
|
||||
this.broadcastToWebSockets(updateMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch member with ID ${id}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchUserPresences(): Promise<void> {
|
||||
const guild = this.client.guilds.get(environment.discord.auth.guildId);
|
||||
|
||||
if (!guild) return;
|
||||
|
||||
this.watchIds.forEach((id) => {
|
||||
const member = guild.members.get(id);
|
||||
if (member && member.presence) {
|
||||
this.userPresences.set(member.user.id, member.presence);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async setupListeners(): Promise<void> {
|
||||
this.client.on("ready", async () => {
|
||||
const user = this.client.user;
|
||||
const guild = this.client.guilds.get(environment.discord.auth.guildId);
|
||||
|
||||
if (user && guild) {
|
||||
await this.fetchUserPresences();
|
||||
await this.fetchUsersInfo();
|
||||
}
|
||||
});
|
||||
|
||||
this.client.on("presenceUpdate", (guild: Guild | Uncached, member: Member | Uncached, presence: Presence, oldPresence: Presence | null) => {
|
||||
if (!(member instanceof Member)) return;
|
||||
|
||||
const userId = member.user.id.toString();
|
||||
if (!this.watchIds.includes(userId)) return;
|
||||
|
||||
const oldStatus = oldPresence;
|
||||
const newStatus = presence;
|
||||
|
||||
if (oldStatus !== newStatus) {
|
||||
this.userPresences.set(userId, presence);
|
||||
this.broadcastPresenceUpdate(userId, presence);
|
||||
}
|
||||
});
|
||||
|
||||
this.client.on("guildMemberUpdate", (member: Member, oldMember: JSONMember | null) => {
|
||||
const userId = member.user.id.toString();
|
||||
|
||||
if (!this.watchIds.includes(userId)) return;
|
||||
|
||||
const memberData = this.serializeMember(member);
|
||||
this.usersInfo.set(userId, memberData);
|
||||
|
||||
const updateMessage = JSON.stringify({
|
||||
type: "memberUpdate",
|
||||
user: memberData
|
||||
});
|
||||
|
||||
this.broadcastToWebSockets(updateMessage);
|
||||
});
|
||||
}
|
||||
|
||||
private serializeMember(member: Member): object {
|
||||
const user = member.user;
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
discriminator: user.discriminator,
|
||||
nick: member.nick ?? user.username,
|
||||
avatar: user.avatar
|
||||
? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}`
|
||||
: `https://cdn.discordapp.com/embed/avatars/${parseInt(user.discriminator) % 5}`,
|
||||
avatarDecoration: user.avatarDecorationData?.asset
|
||||
? `https://cdn.discordapp.com/avatar-decoration-presets/${user.avatarDecorationData.asset}`
|
||||
: null,
|
||||
roles: member.roles,
|
||||
joinedAt: member.joinedAt,
|
||||
premiumSince: member.premiumSince,
|
||||
user: user
|
||||
};
|
||||
}
|
||||
|
||||
private broadcastToWebSockets(message: string): void {
|
||||
for (const socket of this.connectedSockets) {
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const discordBot = new DiscordBot();
|
||||
|
||||
export { discordBot };
|
||||
export default discordBot;
|
16
src/index.ts
16
src/index.ts
|
@ -1,16 +0,0 @@
|
|||
import { fastifyManager } from "./fastify/manager";
|
||||
import { discordBot } from "./helpers/discord.bot";
|
||||
|
||||
class Index {
|
||||
public static async main() : Promise<void> {
|
||||
const main = new Index();
|
||||
await main.start();
|
||||
};
|
||||
|
||||
public async start() : Promise<void> {
|
||||
await discordBot.start();
|
||||
await fastifyManager.start();
|
||||
}
|
||||
}
|
||||
|
||||
const index : Promise<void> = Index.main();
|
|
@ -1,25 +0,0 @@
|
|||
export interface IEnvironment {
|
||||
development: boolean;
|
||||
|
||||
fastify: {
|
||||
host: string;
|
||||
port: number;
|
||||
};
|
||||
|
||||
discord: {
|
||||
auth: {
|
||||
token: string;
|
||||
guildId: string;
|
||||
};
|
||||
watchIds: bigint[];
|
||||
};
|
||||
|
||||
paths: {
|
||||
src: string;
|
||||
www: {
|
||||
root: string;
|
||||
views: string;
|
||||
public: string;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import type { TMethod } from "../types/routes";
|
||||
|
||||
export interface IRouteInfo {
|
||||
enabled: boolean;
|
||||
path: string;
|
||||
method: TMethod | TMethod[];
|
||||
websocket?: boolean;
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export type TMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS" | "HEAD";
|
Binary file not shown.
Before Width: | Height: | Size: 1.4 MiB |
Binary file not shown.
|
@ -1,4 +0,0 @@
|
|||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path fill="#43b581" fill-rule="evenodd" d="M20.97 4.06c0 .18.08.35.24.43.55.28.9.82 1.04 1.42.3 1.24.75 3.7.75 7.09v4.91a3.09 3.09 0 0 1-5.85 1.38l-1.76-3.51a1.09 1.09 0 0 0-1.23-.55c-.57.13-1.36.27-2.16.27s-1.6-.14-2.16-.27c-.49-.11-1 .1-1.23.55l-1.76 3.51A3.09 3.09 0 0 1 1 17.91V13c0-3.38.46-5.85.75-7.1.15-.6.49-1.13 1.04-1.4a.47.47 0 0 0 .24-.44c0-.7.48-1.32 1.2-1.47l2.93-.62c.5-.1 1 .06 1.36.4.35.34.78.71 1.28.68a42.4 42.4 0 0 1 4.4 0c.5.03.93-.34 1.28-.69.35-.33.86-.5 1.36-.39l2.94.62c.7.15 1.19.78 1.19 1.47ZM20 7.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM15.5 12a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM5 7a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2H7v1a1 1 0 1 1-2 0v-1H4a1 1 0 1 1 0-2h1V7Z" clip-rule="evenodd" class=""></path>
|
||||
</svg>
|
Before Width: | Height: | Size: 799 B |
|
@ -1,37 +0,0 @@
|
|||
@font-face {
|
||||
font-family: 'FantasqueSansMNerdFont';
|
||||
src: url('/public/assets/fonts/FantasqueSansMNerdFont-Regular.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
--background-color: rgb(255, 255, 255);
|
||||
--text-color: #333;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--background-color: rgb(0, 0, 0);
|
||||
--text-color: #f5f5f5;
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
font-family: 'FantasqueSansMNerdFont', sans-serif;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.snowflake {
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
|
@ -1,413 +0,0 @@
|
|||
#user-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
#user-info {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
max-width: 580px;
|
||||
margin: 0 0 0 0;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background-color: rgb(17, 17, 19);
|
||||
margin: 5em 0 2em 0;
|
||||
border: 3px solid rgb(52, 52, 57);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
position: relative;
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
display: inline-block;
|
||||
margin-top: 110px;
|
||||
background-color: rgb(17, 17, 19);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
#avatar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 6px solid rgb(17, 17, 19);
|
||||
}
|
||||
|
||||
#avatar-decoration {
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
left: -7px;
|
||||
width: 120%;
|
||||
height: 120%;
|
||||
object-fit: cover;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#display-name {
|
||||
margin: 0 0px -5px 0;
|
||||
color: white;
|
||||
font-size: 25px;
|
||||
font-weight: 800;
|
||||
font-family: 'Whitney', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
#name-pronouns-badges {
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
margin: 0 0 0 0;
|
||||
}
|
||||
|
||||
#pronouns-badges {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
#name-pronouns {
|
||||
margin: 0 0 2px 0;
|
||||
color: white;
|
||||
font-family: 'Whitney', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.status-circle {
|
||||
position: absolute;
|
||||
bottom: -8px;
|
||||
right: -8px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
border: 6px solid #202225;
|
||||
}
|
||||
|
||||
.online {
|
||||
background-color: #43b581;
|
||||
}
|
||||
|
||||
.idle {
|
||||
background-color: #faa61a;
|
||||
}
|
||||
|
||||
.dnd {
|
||||
background-color: #f04747;
|
||||
}
|
||||
|
||||
.offline {
|
||||
background-color: #747f8d;
|
||||
}
|
||||
|
||||
#activity-list {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background-color: transparent;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#activity-list.no-activity {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.activity {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: rgb(6, 6, 6);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.activity:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.activity-type-container {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.large-image {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.asset-large-image {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.asset-small-image {
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
right: -5px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgb(17, 17, 19);
|
||||
}
|
||||
|
||||
.activity-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.activity-type,
|
||||
.activity-info .activity-name {
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.activity-type {
|
||||
margin-bottom: 2px;
|
||||
font-size: 12px;
|
||||
color: #b9bbbe;
|
||||
}
|
||||
|
||||
.activity-info .activity-state,
|
||||
.activity-info .activity-large-text,
|
||||
.activity-info .activity-details {
|
||||
color: #b9bbbe;
|
||||
font-size: 14px;
|
||||
margin: 0 0 1px 0;
|
||||
}
|
||||
|
||||
.activity-info .activity-timestamp {
|
||||
color: #43b581;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 5px 0 0 0;
|
||||
}
|
||||
|
||||
.activity-info .activity-timestamp img {
|
||||
margin-right: 5px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.avatar-custom-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
display: flex;
|
||||
margin: 0 0 2em 0;
|
||||
}
|
||||
|
||||
.custom-status-text {
|
||||
background-color: rgb(7, 12, 23);
|
||||
border-radius: 12px;
|
||||
padding: 8px 0 5px 15px;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
max-width: 200px;
|
||||
line-height: 1.2;
|
||||
position: absolute;
|
||||
outline: 1px solid #2b2e35;
|
||||
left: 160px;
|
||||
top: 76%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.custom-status-text::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -11px;
|
||||
left: 10px;
|
||||
width: 20px;
|
||||
height: 10px;
|
||||
background-color: rgb(7, 12, 23);
|
||||
border-radius: 10px 10px 0 0;
|
||||
border: 1px solid #2b2e35;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.custom-status-text::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -18px;
|
||||
left: -2px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background-color: rgb(7, 12, 23);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(-50%);
|
||||
border: 1px solid #2b2e35;
|
||||
}
|
||||
|
||||
#badge-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 5px;
|
||||
|
||||
background-color: rgb(17, 17, 19);
|
||||
border-radius: 4px;
|
||||
border: 1px solid #2b2e35;
|
||||
padding: 2px;
|
||||
font-size: 14px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.badge {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
#banner {
|
||||
width: calc(100% + 40px);
|
||||
height: 210px;
|
||||
object-fit: fill;
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
left: -20px;
|
||||
z-index: -1;
|
||||
padding: 0;
|
||||
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
|
||||
#user-tabs-container {
|
||||
margin-top: 15px;
|
||||
padding: 15px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid rgb(45, 45, 48);
|
||||
border-radius: 8px;
|
||||
background-color: rgb(10, 10, 10);
|
||||
}
|
||||
|
||||
#user-tabs-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 20px;
|
||||
margin: 0 0 10px 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#user-tabs-buttons button {
|
||||
background-color: transparent;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#user-tabs-buttons button.active {
|
||||
text-decoration: underline;
|
||||
text-underline-position: under;
|
||||
text-underline-offset: 9px;
|
||||
text-decoration-thickness: 1px;
|
||||
}
|
||||
|
||||
.tab-content.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
border-top: .5px solid rgb(45, 45, 48);
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
#about-me-content {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
#about-me-content a {
|
||||
text-decoration: none;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#about-me-content a:hover {
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 2px;
|
||||
}
|
||||
|
||||
#about-me-content a:visited {
|
||||
color: rgb(59, 131, 194);
|
||||
}
|
||||
|
||||
#about-me-content #socials {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
align-items: flex-start;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#about-me-content #socials h3 {
|
||||
margin: 0;
|
||||
color: #ffffff;
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#about-me-content #socials ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#about-me-content #socials ul li {
|
||||
margin: 0;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#about-me-content #socials ul li img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
#about-me-content #socials ul li a {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
#about-me-content #socials ul li a.visited {
|
||||
color: rgb(59, 131, 194) !important;
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const snowContainer = document.createElement('div');
|
||||
snowContainer.style.position = 'fixed';
|
||||
snowContainer.style.top = '0';
|
||||
snowContainer.style.left = '0';
|
||||
snowContainer.style.width = '100vw';
|
||||
snowContainer.style.height = '100vh';
|
||||
snowContainer.style.pointerEvents = 'none';
|
||||
document.body.appendChild(snowContainer);
|
||||
|
||||
const maxSnowflakes = 60;
|
||||
const snowflakes = [];
|
||||
const mouse = { x: -100, y: -100 };
|
||||
|
||||
document.addEventListener('mousemove', function (e) {
|
||||
mouse.x = e.clientX;
|
||||
mouse.y = e.clientY;
|
||||
});
|
||||
|
||||
const createSnowflake = () => {
|
||||
if (snowflakes.length >= maxSnowflakes) {
|
||||
const oldestSnowflake = snowflakes.shift();
|
||||
snowContainer.removeChild(oldestSnowflake);
|
||||
}
|
||||
|
||||
const snowflake = document.createElement('div');
|
||||
snowflake.classList.add('snowflake');
|
||||
snowflake.style.position = 'absolute';
|
||||
snowflake.style.width = `${Math.random() * 3 + 2}px`;
|
||||
snowflake.style.height = snowflake.style.width;
|
||||
snowflake.style.background = 'white';
|
||||
snowflake.style.borderRadius = '50%';
|
||||
snowflake.style.opacity = Math.random();
|
||||
snowflake.style.left = `${Math.random() * window.innerWidth}px`;
|
||||
snowflake.style.top = `-${snowflake.style.height}`;
|
||||
snowflake.speed = Math.random() * 3 + 2;
|
||||
snowflake.directionX = (Math.random() - 0.5) * 0.5;
|
||||
snowflake.directionY = Math.random() * 0.5 + 0.5;
|
||||
|
||||
snowflakes.push(snowflake);
|
||||
snowContainer.appendChild(snowflake);
|
||||
};
|
||||
|
||||
setInterval(createSnowflake, 80);
|
||||
|
||||
function updateSnowflakes() {
|
||||
snowflakes.forEach((snowflake, index) => {
|
||||
let rect = snowflake.getBoundingClientRect();
|
||||
|
||||
let dx = rect.left + rect.width / 2 - mouse.x;
|
||||
let dy = rect.top + rect.height / 2 - mouse.y;
|
||||
let distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < 30) {
|
||||
snowflake.directionX += (dx / distance) * 0.02;
|
||||
snowflake.directionY += (dy / distance) * 0.02;
|
||||
} else {
|
||||
snowflake.directionX += (Math.random() - 0.5) * 0.01;
|
||||
snowflake.directionY += (Math.random() - 0.5) * 0.01;
|
||||
}
|
||||
|
||||
snowflake.style.left = `${rect.left + snowflake.directionX * snowflake.speed}px`;
|
||||
snowflake.style.top = `${rect.top + snowflake.directionY * snowflake.speed}px`;
|
||||
|
||||
if (rect.top + rect.height >= window.innerHeight) {
|
||||
snowContainer.removeChild(snowflake);
|
||||
snowflakes.splice(index, 1);
|
||||
}
|
||||
|
||||
if (rect.left > window.innerWidth || rect.top > window.innerHeight || rect.left < 0) {
|
||||
snowflake.style.left = `${Math.random() * window.innerWidth}px`;
|
||||
snowflake.style.top = `-${snowflake.style.height}`;
|
||||
}
|
||||
});
|
||||
|
||||
requestAnimationFrame(updateSnowflakes);
|
||||
}
|
||||
|
||||
updateSnowflakes();
|
||||
});
|
|
@ -1,397 +0,0 @@
|
|||
const ws = new WebSocket(`/api/status`);
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
ws.addEventListener("open", () => {
|
||||
ws.send(JSON.stringify({ type: "getStatus" }));
|
||||
ws.send(JSON.stringify({ type: "getUsers" }));
|
||||
});
|
||||
|
||||
ws.addEventListener("message", (event) => {
|
||||
handleMessage(event);
|
||||
});
|
||||
|
||||
ws.addEventListener("close", () => {
|
||||
// console.log("WebSocket connection closed.");
|
||||
});
|
||||
|
||||
ws.addEventListener("error", (error) => {
|
||||
console.error("WebSocket error:", error);
|
||||
});
|
||||
|
||||
const bannerElem = document.getElementById("banner");
|
||||
const avatar_custom_container = document.getElementById("avatar-custom-container",);
|
||||
const avatarElem = document.getElementById("avatar");
|
||||
const displayNameElem = document.getElementById("display-name");
|
||||
const decorationElem = document.getElementById("avatar-decoration");
|
||||
const usernameElem = document.getElementById("username");
|
||||
const statusCircle = document.getElementById("status-circle");
|
||||
const badgeContainer = document.getElementById("badge-container");
|
||||
|
||||
const about_me_tab = document.getElementById("about-me-content");
|
||||
const about_me_tab_button = document.getElementById("about-me");
|
||||
const activity_tab = document.getElementById("activity-content");
|
||||
const activity_tab_button = document.getElementById("activity");
|
||||
|
||||
// const placeholderAvatar = "/public/images/avatar-placeholder.png";
|
||||
const placeholderUsername = "Loading...";
|
||||
|
||||
// avatarElem.src = placeholderAvatar;
|
||||
usernameElem.textContent = placeholderUsername;
|
||||
decorationElem.style.display = "none";
|
||||
|
||||
function handleMessage(event) {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
if (data.type === "statusUpdate") {
|
||||
handleStatusUpdate(data.status);
|
||||
} else if (data.type === "presenceUpdate") {
|
||||
handlePresenceUpdate(data.presence);
|
||||
} else if (data.type === "usersUpdate") {
|
||||
handleUsersUpdate(data.users);
|
||||
} else if (data.type === "badgesUpdate") {
|
||||
handleBadges(data.badges);
|
||||
}
|
||||
}
|
||||
|
||||
function updateStatusCircle(status) {
|
||||
if (!statusCircle || !status) return;
|
||||
|
||||
statusCircle.classList.remove("online", "idle", "dnd", "offline");
|
||||
statusCircle.classList.add(status);
|
||||
}
|
||||
|
||||
function typeText(type) {
|
||||
switch (type) {
|
||||
case 0:
|
||||
return "Playing";
|
||||
case 1:
|
||||
return "Streaming";
|
||||
case 2:
|
||||
return "Listening to";
|
||||
case 3:
|
||||
return "Watching";
|
||||
case 4:
|
||||
return "Custom Status";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
const tabButtons = document.querySelectorAll(".tab-button");
|
||||
|
||||
tabButtons.forEach((button) => {
|
||||
const type = button.getAttribute("data-type");
|
||||
|
||||
button.addEventListener("click", () => {
|
||||
tabSwitch(type);
|
||||
});
|
||||
});
|
||||
|
||||
function tabSwitch(tab) {
|
||||
if (tab === "activity") {
|
||||
about_me_tab.classList.remove("active");
|
||||
about_me_tab_button.classList.remove("active");
|
||||
about_me_tab.classList.add("hidden");
|
||||
|
||||
activity_tab.classList.add("active");
|
||||
activity_tab_button.classList.add("active");
|
||||
activity_tab.classList.remove("hidden");
|
||||
} else {
|
||||
activity_tab.classList.remove("active");
|
||||
activity_tab_button.classList.remove("active");
|
||||
activity_tab.classList.add("hidden");
|
||||
|
||||
about_me_tab.classList.add("active");
|
||||
about_me_tab_button.classList.add("active");
|
||||
about_me_tab.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function updateActivities(activities) {
|
||||
const activityList = document.createElement("div");
|
||||
const customStatusText = document.getElementById("custom-status-text");
|
||||
activityList.id = "activity-list";
|
||||
|
||||
const shouldShowNoActivity = activities.length === 0 || (activities.length === 1 && activities[0]?.type === 4);
|
||||
if (shouldShowNoActivity) {
|
||||
const noActivity = document.createElement("p");
|
||||
noActivity.classList.add("no-activity");
|
||||
noActivity.textContent = "No activity(s) to display";
|
||||
activityList.appendChild(noActivity);
|
||||
|
||||
tabSwitch("about-me");
|
||||
}
|
||||
|
||||
const existingActivities = document.getElementById("activity-list");
|
||||
|
||||
if (existingActivities) existingActivities.remove();
|
||||
if (customStatusText) customStatusText.remove();
|
||||
|
||||
activities.forEach((activity) => {
|
||||
if (activity.type === 4) {
|
||||
const customStatusDiv = document.createElement("div");
|
||||
customStatusDiv.classList.add("custom-status");
|
||||
|
||||
const customStatus = document.createElement("p");
|
||||
customStatus.classList.add("custom-status-text");
|
||||
customStatus.id = "custom-status-text";
|
||||
customStatus.textContent = activity.state;
|
||||
|
||||
customStatusDiv.appendChild(customStatus);
|
||||
avatar_custom_container.appendChild(customStatusDiv);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const activityDiv = document.createElement("div");
|
||||
activityDiv.classList.add("activity");
|
||||
|
||||
let activityLargeText;
|
||||
|
||||
const activityTypeDiv = document.createElement("div");
|
||||
activityTypeDiv.classList.add("activity-type-container");
|
||||
|
||||
const activityType = document.createElement("p");
|
||||
activityType.classList.add("activity-type");
|
||||
|
||||
if (activity.type === 2) {
|
||||
activityType.textContent = `${typeText(activity.type)} ${activity.name}`;
|
||||
} else {
|
||||
activityType.textContent = `${typeText(activity.type)}`;
|
||||
}
|
||||
|
||||
activityTypeDiv.appendChild(activityType);
|
||||
activityDiv.appendChild(activityTypeDiv);
|
||||
|
||||
const activityContentDiv = document.createElement("div");
|
||||
activityContentDiv.classList.add("activity-content");
|
||||
|
||||
if (activity.assets?.largeImage) {
|
||||
const largeImageDiv = document.createElement("div");
|
||||
largeImageDiv.classList.add("large-image");
|
||||
|
||||
if (activity.assets.largeText && activity.type === 2)
|
||||
activityLargeText = activity.assets.largeText;
|
||||
|
||||
const largeImage = document.createElement("img");
|
||||
largeImage.src = parseDiscordImage(
|
||||
activity.assets.largeImage,
|
||||
activity.applicationID,
|
||||
);
|
||||
largeImage.alt = "Large Image";
|
||||
largeImage.classList.add("asset-large-image");
|
||||
largeImageDiv.appendChild(largeImage);
|
||||
|
||||
if (activity.assets?.smallImage) {
|
||||
const smallImageDiv = document.createElement("div");
|
||||
smallImageDiv.classList.add("small-image");
|
||||
|
||||
const smallImage = document.createElement("img");
|
||||
smallImage.src = parseDiscordImage(
|
||||
activity.assets.smallImage,
|
||||
activity.applicationID,
|
||||
);
|
||||
smallImage.alt = "Small Image";
|
||||
smallImage.classList.add("asset-small-image");
|
||||
smallImageDiv.appendChild(smallImage);
|
||||
|
||||
largeImageDiv.appendChild(smallImageDiv);
|
||||
}
|
||||
|
||||
activityContentDiv.appendChild(largeImageDiv);
|
||||
}
|
||||
|
||||
const activityInfoDiv = document.createElement("div");
|
||||
activityInfoDiv.classList.add("activity-info");
|
||||
|
||||
const name = document.createElement("p");
|
||||
name.classList.add("activity-name");
|
||||
if (activity.type === 2) {
|
||||
name.textContent = `${activity.details}`;
|
||||
} else {
|
||||
name.textContent = `${activity.name}`;
|
||||
}
|
||||
activityInfoDiv.appendChild(name);
|
||||
|
||||
if (activity.details && activity.type !== 2) {
|
||||
const details = document.createElement("p");
|
||||
details.classList.add("activity-details");
|
||||
details.textContent = `${activity.details}`;
|
||||
activityInfoDiv.appendChild(details);
|
||||
}
|
||||
|
||||
if (activity.state) {
|
||||
const state = document.createElement("p");
|
||||
state.classList.add("activity-state");
|
||||
state.textContent = `${activity.state}`;
|
||||
activityInfoDiv.appendChild(state);
|
||||
}
|
||||
|
||||
if (activityLargeText) {
|
||||
const largeText = document.createElement("p");
|
||||
largeText.classList.add("activity-large-text");
|
||||
largeText.textContent = `${activityLargeText}`;
|
||||
activityInfoDiv.appendChild(largeText);
|
||||
}
|
||||
|
||||
if (activity.timestamps?.start) {
|
||||
const timestamp = document.createElement("p");
|
||||
timestamp.classList.add("activity-timestamp");
|
||||
const startTime = activity.timestamps.start;
|
||||
|
||||
function updateElapsedTime() {
|
||||
const elapsedTime = getElapsedTime(startTime);
|
||||
timestamp.innerHTML = `<img src="/public/assets/game_icon.svg" alt="Time icon"> ${elapsedTime}`;
|
||||
}
|
||||
|
||||
updateElapsedTime();
|
||||
|
||||
setInterval(updateElapsedTime, 1000);
|
||||
|
||||
activityInfoDiv.appendChild(timestamp);
|
||||
}
|
||||
|
||||
activityContentDiv.appendChild(activityInfoDiv);
|
||||
activityDiv.appendChild(activityContentDiv);
|
||||
|
||||
activityList.appendChild(activityDiv);
|
||||
});
|
||||
|
||||
activity_tab.appendChild(activityList);
|
||||
}
|
||||
|
||||
function getElapsedTime(startTimestamp) {
|
||||
const now = Date.now();
|
||||
const elapsed = now - startTimestamp;
|
||||
|
||||
const seconds = Math.floor(elapsed / 1000) % 60;
|
||||
const minutes = Math.floor(elapsed / (1000 * 60)) % 60;
|
||||
const hours = Math.floor(elapsed / (1000 * 60 * 60)) % 24;
|
||||
const days = Math.floor(elapsed / (1000 * 60 * 60 * 24));
|
||||
|
||||
let formattedTime = "";
|
||||
|
||||
if (days > 0) {
|
||||
formattedTime += `${days}:`;
|
||||
}
|
||||
if (hours > 0 || days > 0) {
|
||||
formattedTime += `${pad(hours)}:`;
|
||||
}
|
||||
if (minutes > 0 || hours > 0 || days > 0) {
|
||||
formattedTime += `${pad(minutes)}:`;
|
||||
}
|
||||
|
||||
formattedTime += `${pad(seconds)}`;
|
||||
|
||||
return formattedTime;
|
||||
}
|
||||
|
||||
function pad(value) {
|
||||
return String(value).padStart(2, "0");
|
||||
}
|
||||
|
||||
function handleStatusUpdate(status) {
|
||||
const firstUser = Object.values(status)[0];
|
||||
const userStatus = firstUser?.status;
|
||||
const activities = firstUser?.activities || [];
|
||||
|
||||
updateStatusCircle(userStatus);
|
||||
updateActivities(activities);
|
||||
}
|
||||
|
||||
function handlePresenceUpdate(presence) {
|
||||
const userPresence = presence?.status;
|
||||
const activities = presence?.activities || [];
|
||||
|
||||
updateStatusCircle(userPresence);
|
||||
updateActivities(activities);
|
||||
}
|
||||
|
||||
function parseDiscordImage(imageUrl, activityID) {
|
||||
if (imageUrl.startsWith("mp:external/")) {
|
||||
const httpsIndex = imageUrl.indexOf("https/");
|
||||
if (httpsIndex !== -1) {
|
||||
return "https://" + imageUrl.slice(httpsIndex + "https/".length);
|
||||
}
|
||||
} else if (/^\d+$/.test(imageUrl)) {
|
||||
return `https://cdn.discordapp.com/app-assets/${activityID}/${imageUrl}`;
|
||||
}
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
function handleUsersUpdate(users) {
|
||||
const firstUser = Object.values(users)[0];
|
||||
|
||||
ws.send(JSON.stringify({ type: "getBadges", userId: firstUser.id }));
|
||||
if (!firstUser || !firstUser.user) return;
|
||||
|
||||
const { avatar, avatarDecoration, username, banner } = firstUser;
|
||||
const displayName = firstUser.user.globalName || firstUser.user.username;
|
||||
|
||||
updateUserInfo(avatar, avatarDecoration, username, displayName, banner);
|
||||
}
|
||||
|
||||
function updateUserInfo(
|
||||
avatarUrl,
|
||||
decorationUrl,
|
||||
username,
|
||||
displayName,
|
||||
banner,
|
||||
) {
|
||||
if (bannerElem) {
|
||||
bannerElem.src = banner + "?size=2048" || "";
|
||||
}
|
||||
|
||||
if (avatarElem) {
|
||||
avatarElem.src = avatarUrl; //|| placeholderAvatar;
|
||||
}
|
||||
|
||||
if (decorationElem) {
|
||||
if (decorationUrl) {
|
||||
decorationElem.src = decorationUrl;
|
||||
decorationElem.style.display = "block";
|
||||
} else {
|
||||
decorationElem.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
if (usernameElem) {
|
||||
usernameElem.textContent = username || placeholderUsername;
|
||||
}
|
||||
|
||||
if (displayNameElem) {
|
||||
displayNameElem.textContent = displayName || placeholderUsername;
|
||||
}
|
||||
}
|
||||
|
||||
function handleBadges(badges) {
|
||||
|
||||
// known as
|
||||
badges.push({
|
||||
badge: "https://cdn.discordapp.com/badge-icons/6de6d34650760ba5551a79732e98ed60.png",
|
||||
tooltip: "Originally known as Creation's#0001",
|
||||
});
|
||||
|
||||
//hypesquad
|
||||
badges.push({
|
||||
badge: "https://cdn.discordapp.com/badge-icons/3aa41de486fa12454c3761e8e223442e.png",
|
||||
tooltip: "HypeSquad Balance",
|
||||
});
|
||||
|
||||
badges.push({
|
||||
badge: "https://cdn.discordapp.com/badge-icons/6bdc42827a38498929a4920da12695d9.png",
|
||||
tooltip: "Active Developer",
|
||||
});
|
||||
|
||||
for (const badge of badges) {
|
||||
if ((badge && !badge.badge) || !badge.tooltip) continue;
|
||||
|
||||
const badgeImg = document.createElement("img");
|
||||
badgeImg.src = badge.badge;
|
||||
badgeImg.alt = badge.tooltip;
|
||||
badgeImg.title = badge.tooltip;
|
||||
badgeImg.classList.add("badge");
|
||||
badgeContainer.appendChild(badgeImg);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,20 +0,0 @@
|
|||
import type { FastifyReply, FastifyRequest } from "fastify";
|
||||
import type { IRouteInfo } from "../../interfaces/routes";
|
||||
|
||||
const routeInfo: IRouteInfo = {
|
||||
enabled: true,
|
||||
path: "/",
|
||||
method: "GET"
|
||||
};
|
||||
|
||||
async function route(request : FastifyRequest, reply : FastifyReply) : Promise<string> {
|
||||
const siteData = {
|
||||
title: "Home",
|
||||
};
|
||||
|
||||
return reply.viewAsync("index", {
|
||||
...siteData,
|
||||
});
|
||||
}
|
||||
|
||||
export default { routeInfo, route }
|
|
@ -1,74 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="dark">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>creations.works</title>
|
||||
|
||||
<link rel="stylesheet" href="/public/css/style.css" />
|
||||
<link rel="stylesheet" href="/public/css/user.style.css" />
|
||||
|
||||
<script src="/public/js/status.ws.js" defer></script>
|
||||
<script src="/public/js/snow.js" defer></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-views"></div>
|
||||
<div id="user-container">
|
||||
<div id="user-info">
|
||||
<div class="avatar-custom-container" id="avatar-custom-container">
|
||||
<img id="banner" />
|
||||
<div class="avatar-container">
|
||||
<img id="avatar" />
|
||||
<img id="avatar-decoration" src="" />
|
||||
<div id="status-circle" class="status-circle offline"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="name-pronouns-badges">
|
||||
<h2 id="display-name">User Name</h2>
|
||||
<div id="pronouns-badges">
|
||||
<div id="name-pronouns">
|
||||
<span id="username">User Name</span>
|
||||
<span id="dot">•</span>
|
||||
<span id="pronouns">creations.works</span>
|
||||
</div>
|
||||
<div id="badge-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="user-tabs-container">
|
||||
<div id="user-tabs-buttons">
|
||||
<button id="about-me" class="tab-button" data-type='about-me'>About Me</button>
|
||||
<button id="activity" class="tab-button active" data-type='activity'>Activity</button>
|
||||
</div>
|
||||
<div id="tab-content">
|
||||
<div id="about-me-content" class="tab-content hidden">
|
||||
<a href="https://atums.world" target="_blank">https://atums.world</a>
|
||||
<div id="socials">
|
||||
<h3>Find me here</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<img src="https://external-content.duckduckgo.com/iu/?u=http%3A%2F%2Fsudhop.com%2Fbilder%2Fmail.png&f=1&nofb=1&ipt=1ae00a3202725283cc556e3feefdb72750c50afa5a44f428ea3c15d7babf1d70&ipo=images" />
|
||||
<a href="mailto:creations@creations.works">Email, creations@creations.works</a>
|
||||
</li>
|
||||
<li>
|
||||
<img src="https://cdn.prod.website-files.com/6257adef93867e50d84d30e2/636e0a69f118df70ad7828d4_icon_clyde_blurple_RGB.svg" />
|
||||
<a href="https://discord.gg/DxFhhbr2pm" target="_blank">Discord</a>
|
||||
</li>
|
||||
<li>
|
||||
<img src="https://git.creations.works/assets/img/logo.svg" alt="Forgejo Icon">
|
||||
<a href="https://git.creations.works/creations" target="_blank">Forgejo</a>
|
||||
</li>
|
||||
<li>
|
||||
<img src="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fportalrepository.com%2Fwp-content%2Fuploads%2F2019%2F07%2F1024px-Steam_icon_logo.svg.png&f=1&nofb=1&ipt=42fde64bfc0c2aa3cab5944e37a1494d95b368514950c98317460883a1a94bf6&ipo=images" />
|
||||
<a href="https://steamcommunity.com/id/creations_works/" target="_blank">Steam</a>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div id="activity-content" class="tab-content active"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,27 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
// Enable latest features
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue