Compare commits

..

1 Commits

Author SHA1 Message Date
liushuyu
3c1019e817
package: use parcel bundler ...
* Significantly reduced deployment size
* Bot starts up much more faster
2021-10-22 21:05:47 -06:00
34 changed files with 4987 additions and 1928 deletions

View File

@ -1,12 +1,6 @@
{
"extends": ["eslint:recommended", "standard",
"plugin:@typescript-eslint/recommended"
],
"plugins": ["@typescript-eslint"],
"extends": ["eslint:recommended", "standard"],
"rules": {
"semi": [2, "always"]
},
"parserOptions": {
"project": ["tsconfig.json"]
}
}

View File

@ -2,7 +2,7 @@ name: Node.js CI
on:
push:
branches: [ '*' ]
branches: [ "*" ]
pull_request:
branches: [ master ]
@ -13,12 +13,12 @@ jobs:
strategy:
matrix:
node-version: [20.x]
node-version: [16.x]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: yarn
@ -29,15 +29,15 @@ jobs:
runs-on: ubuntu-latest
if: (github.ref == 'refs/heads/master') && (github.repository == 'citra-emu/discord-bot')
steps:
- uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v2
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
name: Setup Docker BuildX system
- name: Login to DockerHub
uses: docker/login-action@v2
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: docker/build-push-action@v3
- uses: docker/build-push-action@v2
name: Deploy the image
with:
push: true

4
.gitignore vendored
View File

@ -51,5 +51,5 @@ config/development.json
CMakeLists.txt.user*
/dist
_.ts
# Parcel's cache
/.parcel-cache

4
.parcelrc Normal file
View File

@ -0,0 +1,4 @@
{
"extends": "@parcel/config-default",
"resolvers": ["@parcel/resolver-glob", "..."]
}

View File

@ -1,15 +1,16 @@
FROM node:20-alpine AS build
FROM node:16-alpine AS build
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies and add source files
COPY package.json env.json yarn.lock tsconfig.json bundle.sh *.js *.mjs ./
COPY ./src ./src
RUN yarn install --frozen-lockfile && sh -e ./bundle.sh
COPY package.json yarn.lock env.json bundle.sh .parcelrc ./
COPY src/ ./src
COPY patches/ ./patches
RUN apk add python3 build-base && yarn install && sh bundle.sh
# Second stage
FROM node:20-alpine
FROM node:16-alpine
WORKDIR /usr/src/app

View File

@ -1,13 +1,27 @@
#!/bin/bash -e
yarn
[ -d "dist" ] && rm -rf dist
yarn run build
echo "[+] Applying patches to accommodate bundler ..."
for i in patches/*.patch; do
echo "Applying $i ..."
patch -Np1 --no-backup-if-mismatch -i "$i"
done
yarn run parcel build
echo "[+] Installing non-bundle-able packages ..."
# DISCORD_JS="$(grep discord.js package.json | sed 's|,||')"
cd "dist"
echo "{\"name\": \"citra-discord-bot\",\"license\": \"GPL-2.0+\",\"dependencies\": {}}" > package.json
yarn install
echo '{"name": "citra-discord-bot","license": "GPL-2.0+"}' > package.json
yarn add discord.js@^13
cd ..
echo "[+] Reversing patches ..."
for i in patches/*.patch; do
echo "Reversing $i ..."
patch -Np1 -R -i "$i"
done
echo "[+] Removing patch backup files ..."
find . -name "*.orig" -print -delete

View File

@ -33,7 +33,6 @@
"description": "The unique ID for the role that the bot will *remove* when the user accepts the rules."
},
"DISCORD_DEVELOPER_ROLE": true,
"DISCORD_TESTER_ROLE": true,
"DISCORD_LOGIN_TOKEN": {
"description": "The login token of the bot."
},

View File

@ -1,42 +0,0 @@
const fs = require('fs');
const path = require('path');
function collectModules(dir, extname) {
let modules = [];
fs.readdirSync(`./src/${dir}/`).forEach(function (file) {
// Load the module if it's a script.
if (path.extname(file) === extname) {
if (file.includes(".disabled")) {
console.info(`Did not load disabled module: ${file}`);
} else {
const moduleName = path.basename(file, extname);
if (moduleName === "_") return;
modules.push(moduleName);
console.info(`Scanning ${moduleName} from ${file} ...`);
}
}
});
return modules;
}
const header = '// GENERATED FILE. DO NOT EDIT!\n// See generateExports.js for details.\n'
console.info('Generating module loader ...');
let modules = collectModules('commands', '.ts');
let loader_content = header;
for (let mod of modules) {
loader_content += `import * as ${mod} from './${mod}';\n`;
}
let loader_map = modules.map((moduleName) => moduleName.toLowerCase() === moduleName ? moduleName : `${moduleName.toLowerCase()}: ${moduleName}`).join(', ');
loader_content += `\nexport default { ${loader_map} };\n`;
fs.writeFileSync("./src/commands/_.ts", loader_content);
let triggers = collectModules('triggers', '.ts');
loader_content = header;
for (let mod of triggers) {
loader_content += `import * as ${mod} from "./${mod}";\n`;
}
loader_map = triggers.join(', ');
loader_content += `\nexport default [ ${loader_map} ];\n`;
fs.writeFileSync("./src/triggers/_.ts", loader_content);

View File

@ -3,44 +3,57 @@
"version": "2.0.0",
"description": "Citra bot for Discord",
"author": "chris062689 <chris062689@gmail.com>",
"preferGlobal": true,
"private": true,
"subdomain": "citra-emu",
"analyze": true,
"license": "GPL-2.0+",
"source": "src/server.ts",
"main": "dist/server.js",
"engines": {
"node": ">=20.0.0"
"node": ">= 16"
},
"targets": {
"main": {
"includeNodeModules": {
"discord.js": false
},
"context": "node",
"optimize": true
}
},
"dependencies": {
"checkenv": "^1.2.2",
"discord.js": "^14.14.1",
"ip": "^2.0.1",
"logdna": "^3.5.3",
"discord.js": "^13.2.0",
"ip": "^1.1.5",
"logdna": "^3.5.2",
"logdna-winston": "^4.0.1",
"node-fetch": "^3",
"string-similarity": "^4.0.4",
"typescript": "^5.3.3",
"winston": "^3.11.0"
"winston": "^3.3.3"
},
"devDependencies": {
"@tsconfig/node20": "^20.1.2",
"@types/ip": "^1.1.3",
"@types/node": "^20.11.21",
"@types/string-similarity": "^4.0.2",
"@types/ws": "^8.5.10",
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.0",
"esbuild": "^0.20.1",
"eslint": "^8.57.0",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-n": "^16.6.2",
"eslint-plugin-promise": "^6.1.1",
"ts-node": "^10.9.2"
"@parcel/resolver-glob": "^2.0.0",
"@tsconfig/node14": "^1.0.1",
"@types/ip": "^1.1.0",
"@types/node": "^16.11.2",
"@types/node-fetch": "^3",
"@types/string-similarity": "^4.0.0",
"@types/ws": "^8.2.0",
"eslint": "^8.0.1",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.1",
"eslint-plugin-standard": "^4.1.0",
"parcel": "^2.0.0",
"ts-node": "^10.3.1",
"typescript": "^4.4.4"
},
"scripts": {
"postinstall": "node generateExports.js",
"build": "yarn run esbuild --bundle src/server.ts --platform=node --target=node20 --outdir=dist/ --minify-whitespace",
"check": "yarn run tsc --noEmit",
"bundle": "./bundle.sh",
"build": "yarn run tsc",
"dist": "./bundle.sh",
"serve": "yarn run ts-node ./src/server.ts"
}
}

View File

@ -0,0 +1,95 @@
From 2ae094e7ad43d9137fafe84a36eac3e9bf0a8245 Mon Sep 17 00:00:00 2001
From: liushuyu <liushuyu011@gmail.com>
Date: Wed, 15 Sep 2021 22:49:40 -0600
Subject: [PATCH] server: make it bundler friendly
---
src/server.ts | 49 ++++++++++++++++++++-----------------------------
1 file changed, 20 insertions(+), 29 deletions(-)
diff --git a/src/server.ts b/src/server.ts
index bc63ae7..7cf8825 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -1,5 +1,7 @@
// Check for environmental variables.
-require('checkenv').check();
+const checkenv = require('checkenv');
+checkenv.setConfig(require('../env.json'));
+checkenv.check();
import discord = require('discord.js');
import path = require('path');
@@ -9,6 +11,10 @@ import logger from './logging';
import state from './state';
import * as data from './data';
import { IModule, ITrigger } from './models/interfaces';
+// Parcel glob imports
+import commands from './commands/*.ts';
+import triggers from './triggers/*.ts';
+import responses from './responses/*.json';
interface IModuleMap {
[name: string]: IModule;
@@ -233,37 +239,16 @@ client.on('message', message => {
});
// Cache all command modules.
-cachedModules = {};
-fs.readdirSync('./commands/').forEach(function (file) {
- // Load the module if it's a script.
- if (path.extname(file) === '.js') {
- if (file.includes('.disabled')) {
- logger.info(`Did not load disabled module: ${file}`);
- } else {
- const moduleName = path.basename(file, '.js').toLowerCase();
- logger.info(`Loaded module: ${moduleName} from ${file}`);
- cachedModules[moduleName] = require(`./commands/${file}`);
- }
- }
+cachedModules = commands;
+Object.entries(commands).forEach(function (command) {
+ logger.info(`Loaded command: ${command[0]}`);
});
// Cache all triggers.
cachedTriggers = [];
-fs.readdirSync('./triggers/').forEach(function (file) {
- // Load the module if it's a script.
- if (path.extname(file) === '.js') {
- if (file.includes('.disabled')) {
- logger.info(`Did not load disabled trigger: ${file}`);
- } else {
- const moduleName = path.basename(file, '.js').toLowerCase();
- logger.info(`Loaded trigger: ${moduleName} from ${file}`);
- try {
- cachedTriggers.push(require(`./triggers/${file}`));
- } catch (e) {
- logger.error(`Could not load trigger ${moduleName}: ${e}`);
- }
- }
- }
+Object.entries(triggers).forEach((trigger: [string, ITrigger]) => {
+ cachedTriggers.push(trigger[1]);
+ logger.info(`Loaded trigger: ${trigger[0]}`);
});
data.readWarnings();
@@ -271,7 +256,13 @@ data.readBans();
// Load custom responses
if (process.env.DATA_CUSTOM_RESPONSES) {
- data.readCustomResponses();
+ // Load the responses file into the responses variable.
+ state.responses = responses[process.env.TENANT];
+ if (!state.responses) {
+ logger.error(`Failed to load ${process.env.TENANT} from cache! Custom responses are disabled.`);
+ } else {
+ logger.debug(`Loaded ${process.env.TENANT} responses from cache.`);
+ }
}
client.login(process.env.DISCORD_LOGIN_TOKEN);
--
2.33.0

View File

@ -0,0 +1,11 @@
--- a/node_modules/winston/dist/winston.js 2021-08-26 22:26:24.296000000 -0600
+++ b/node_modules/winston/dist/winston.js 2021-09-15 23:35:13.136034453 -0600
@@ -6,7 +6,7 @@
*/
'use strict';
-var logform = require('logform');
+import * as logform from 'logform/dist/browser.js';
var _require = require('./winston/common'),
warn = _require.warn;

View File

@ -1,7 +1,9 @@
import { ban } from '../common';
import * as discord from 'discord.js';
import discord = require('discord.js');
export const roles = ['Admins', 'Moderators', 'CitraBot'];
export async function command (message: discord.Message) {
return Promise.all(message.mentions.users.map(async (user) => ban(user, message.author, message.guild)));
}
export function command (message: discord.Message) {
message.mentions.users.map((user) => {
ban(user, message.author, message.guild);
});
};

View File

@ -1,21 +1,21 @@
import state from '../state';
import * as data from '../data';
import logger from '../logging';
import * as discord from 'discord.js';
import discord = require('discord.js');
export const roles = ['Admins', 'Moderators'];
export async function command (message: discord.Message) {
return Promise.all(message.mentions.users.map(async (user) => {
export function command (message: discord.Message) {
message.mentions.users.map((user) => {
const count = state.warnings.filter(x => x.id === user.id && !x.cleared);
if (count != null && count.length > 0) {
count.forEach(warning => { warning.cleared = true; });
data.flushWarnings();
await message.channel.send(`${user.toString()}, your warnings have been cleared.`);
message.channel.send(`${user.toString()}, your warnings have been cleared.`);
} else {
await message.channel.send(`${user.toString()}, you have no warnings to clear.`);
message.channel.send(`${user.toString()}, you have no warnings to clear.`);
}
logger.info(`${message.author.username} has cleared all warnings for ${user.toString()} ${user.username} [${count?.length}].`);
await state.logChannel?.send(`${message.author.toString()} has cleared all warnings for ${user.toString()} [${count?.length}].`);
}));
}
logger.info(`${message.author.username} has cleared all warnings for ${user} ${user.username} [${count?.length}].`);
state.logChannel?.send(`${message.author.toString()} has cleared all warnings for ${user.toString()} [${count?.length}].`);
});
};

View File

@ -1,5 +1,6 @@
import * as discord from 'discord.js';
import * as stringSimilarity from 'string-similarity';
import fetch from 'node-fetch';
import discord = require('discord.js');
import stringSimilarity = require('string-similarity');
import logger from '../logging';
import state from '../state';
@ -15,21 +16,21 @@ const compatStrings: ICompatList = {
1: { key: '1', name: 'Great', color: '#47d35c', description: 'Game functions with minor graphical or audio glitches and is playable from start to finish. May require some workarounds.' },
2: { key: '2', name: 'Okay', color: '#94b242', description: 'Game functions with major graphical or audio glitches, but game is playable from start to finish with workarounds.' },
3: { key: '3', name: 'Bad', color: '#f2d624', description: 'Game functions, but with major graphical or audio glitches. Unable to progress in specific areas due to glitches even with workarounds.' },
4: { key: '4', name: 'Intro/Menu', color: 'Red', description: 'Game is completely unplayable due to major graphical or audio glitches. Unable to progress past the Start Screen.' },
4: { key: '4', name: 'Intro/Menu', color: 'RED', description: 'Game is completely unplayable due to major graphical or audio glitches. Unable to progress past the Start Screen.' },
5: { key: '5', name: "Won't Boot", color: '#828282', description: 'The game crashes when attempting to startup.' },
99: { key: '99', name: 'Not Tested', color: 'DarkButNotBlack', description: 'The game has not yet been tested.' }
99: { key: '99', name: 'Not Tested', color: 'DARK_BUT_NOT_BLACK', description: 'The game has not yet been tested.' }
};
async function updateDatabase () {
let body: IGameDBEntry[];
let body: any;
if (!targetServer) {
logger.error('Unable to download latest games list!');
return;
}
try {
const response = await fetch(targetServer);
body = (await response.json()) as IGameDBEntry[];
let response = await fetch(targetServer);
body = await response.json();
} catch (e) {
logger.error('Unable to download latest games list!');
throw e;
@ -61,15 +62,15 @@ export async function command (message: discord.Message) {
try {
await state.gameDBPromise;
} catch (e) {
await message.channel.send('Game compatibility feed temporarily unavailable.');
message.channel.send('Game compatibility feed temporarily unavailable.');
throw e;
} finally {
// We don't need this message anymore
await waitMessage.then(async waitMessageResult => await waitMessageResult.delete());
waitMessage.then(waitMessageResult => waitMessageResult.delete());
}
}
const game = message.content.substring(message.content.indexOf(' ') + 1);
const game = message.content.substr(message.content.indexOf(' ') + 1);
// Search all games. This is only linear time, so /shrug?
let bestGame: IGameDBEntry | null = null;
@ -86,7 +87,7 @@ export async function command (message: discord.Message) {
}
if (!bestGame) {
await message.channel.send('Game could not be found.');
message.channel.send('Game could not be found.');
return;
}
@ -95,13 +96,13 @@ export async function command (message: discord.Message) {
const compat = compatStrings[bestGame.compatibility];
const embed = new discord.EmbedBuilder()
.addFields({ name: 'Status', value: compat.name, inline: true })
const embed = new discord.MessageEmbed()
.addField('Status', compat.name, true)
.setTitle(bestGame.title)
.setColor(compat.color)
.setDescription(compat.description)
.setURL(url)
.setThumbnail(screenshot);
await message.channel.send({ embeds: [embed] });
message.channel.send({embeds: [embed]});
}

View File

@ -1,20 +1,38 @@
import state from '../state';
import logger from '../logging';
import * as discord from 'discord.js';
import { grantRole } from '../common';
import discord = require('discord.js');
export const roles = ['Admins', 'Moderators', 'CitraBot'];
export async function command (message: discord.Message) {
export function command (message: discord.Message) {
const role = process.env.DISCORD_DEVELOPER_ROLE;
if (!role) {
logger.error('DISCORD_DEVELOPER_ROLE suddenly became undefined?!');
return Promise.resolve([]);
return;
}
return Promise.all(message.mentions.users.map(async (user) => {
return message.guild?.members.fetch(user).then((member) => grantRole(member, role, message.channel))
.catch(async () => {
await message.channel.send(`User ${user.toString()} was not found in the channel.`);
});
}));
message.mentions.users.map((user) => {
message.guild?.members.fetch(user).then((member) => {
const alreadyJoined = member.roles.cache.has(role);
if (alreadyJoined) {
member.roles.remove(role).then(() => {
message.channel.send(`${user.toString()}'s speech has been revoked in the #development channel.`);
}).catch(() => {
state.logChannel?.send(`Error revoking ${user.toString()}'s developer speech...`);
logger.error(`Error revoking ${user} ${user.username}'s developer speech...`);
});
} else {
member.roles.add(role).then(() => {
message.channel.send(`${user.toString()} has been granted speech in the #development channel.`);
}).catch(() => {
state.logChannel?.send(`Error granting ${user.toString()}'s developer speech...`);
logger.error(`Error granting ${user} ${user.username}'s developer speech...`);
});
}
}).catch(() => {
message.channel.send(`User ${user.toString()} was not found in the channel.`);
});
});
}

View File

@ -1,20 +0,0 @@
import logger from '../logging';
import * as discord from 'discord.js';
import { grantRole } from '../common';
const role = process.env.DISCORD_TESTER_ROLE;
export const roles = ['Admins', 'Moderators', 'CitraBot'];
export async function command (message: discord.Message) {
if (!role) {
logger.error('DISCORD_TESTER_ROLE suddenly became undefined?!');
return Promise.resolve([]);
}
return Promise.all(message.mentions.users.map(async (user) => {
return message.guild?.members.fetch(user).then((member) => grantRole(member, role, message.channel))
.catch(async () => {
await message.channel.send(`User ${user.toString()} was not found in the channel.`);
});
}));
}

View File

@ -1,20 +1,20 @@
import state from '../state';
import UserBan from '../models/UserBan';
import UserWarning from '../models/UserWarning';
import * as discord from 'discord.js';
import discord = require('discord.js');
export const roles = ['Admins', 'Moderators'];
function formatWarnings (warnings: UserWarning[]) {
return warnings.map(x => `[${x.date.toISOString()}] ${x.warnedByUsername} warned ${x.username} [${x.priorWarnings} + 1]. ${x.silent ? '(silent)' : ''} ${x.cleared ? '(cleared)' : ''}`);
return warnings.map(x => `[${x.date}] ${x.warnedByUsername} warned ${x.username} [${x.priorWarnings} + 1]. ${x.silent ? '(silent)' : ''} ${x.cleared ? '(cleared)' : ''}`);
}
function formatBans (bans: UserBan[]) {
return bans.map(x => `[${x.date.toISOString()}] ${x.warnedByUsername} banned ${x.username} [${x.priorWarnings} + 1].`);
return bans.map(x => `[${x.date}] ${x.warnedByUsername} banned ${x.username} [${x.priorWarnings} + 1].`);
}
export async function command (message: discord.Message) {
return Promise.all(message.mentions.users.map(async (user) => {
export function command (message: discord.Message) {
message.mentions.users.map((user) => {
const totalWarnings = state.warnings.filter(x => x.id === user.id && x.cleared === false).length;
const warns = state.warnings.filter(x => x.id === user.id);
const bans = state.bans.filter(x => x.id === user.id);
@ -22,6 +22,6 @@ export async function command (message: discord.Message) {
const warnsString = `Warns: \`\`\`${formatWarnings(warns).join('\n')}\`\`\``;
const bansString = `Bans: \`\`\`${formatBans(bans).join('\n')}\`\`\``;
await message.channel.send(`\`${user.username} (${totalWarnings}) information:\`${warns.length !== 0 ? warnsString : '\n<No warnings>\n'}${bans.length !== 0 ? bansString : '<Not banned>'}`);
}));
}
message.channel.send(`\`${user.username} (${totalWarnings}) information:\`${warns.length !== 0 ? warnsString : '\n<No warnings>\n'}${bans.length !== 0 ? bansString : '<Not banned>'}`);
});
};

View File

@ -1,13 +1,13 @@
import * as discord from 'discord.js';
import discord = require('discord.js');
export const roles = ['Admins', 'Moderators'];
export async function command (message: discord.Message, reply: string | undefined) {
export function command (message: discord.Message, reply: string) {
let replyMessage;
if (reply == null) {
replyMessage = message.content.substring(message.content.indexOf(' ') + 1);
replyMessage = message.content.substr(message.content.indexOf(' ') + 1);
} else {
replyMessage = `${message.mentions.users.map(user => user.toString()).join(' ')} ${reply}`;
replyMessage = `${message.mentions.users.map(user => `${user.toString()}`)} ${reply}`;
}
await message.channel.send(replyMessage);
message.channel.send(replyMessage);
}

View File

@ -1,32 +1,33 @@
import * as discord from 'discord.js';
import fetch from 'node-fetch';
import discord = require('discord.js');
const fetchOptions = {
headers: { 'User-Agent': 'Citra-Emu/CitraBot (Node.js)', Accept: 'application/vnd.github.antiope-preview+json' }
headers: { 'User-Agent': 'Citra-Emu/CitraBot (Node.js)', 'Accept': 'application/vnd.github.antiope-preview+json' }
};
const repo = process.env.GITHUB_REPOSITORY || 'citra-emu/citra';
export const roles = ['Admins', 'Moderators', 'Developer'];
export async function command (message: discord.Message) {
const prNumber = message.content.substring(message.content.indexOf(' ') + 1).replace(/\n/g, '');
const url = `https://api.github.com/repos/${repo}/pulls/${prNumber}`;
return fetch(url, fetchOptions).then(response => response.json()).then(async (pr: any) => {
export function command(message: discord.Message) {
const pr_number = message.content.substr(message.content.indexOf(' ') + 1).replace(/\n/g, '');
const url = `https://api.github.com/repos/${repo}/pulls/${pr_number}`;
fetch(url, fetchOptions).then(response => response.json()).then((pr: any) => {
if (!pr || pr.documentation_url || !pr.head) throw new Error('PR not found');
const headSHA = pr.head.sha;
// use the new GitHub checks API
return fetch(`https://api.github.com/repos/${repo}/commits/${headSHA}/check-runs`, fetchOptions).then(response => response.json()).then(async (statuses: any) => {
fetch(`https://api.github.com/repos/${repo}/commits/${headSHA}/check-runs`, fetchOptions).then(response => response.json()).then((statuses: any) => {
if (!statuses.check_runs || statuses.total_count < 1) throw new Error('No check runs');
const msg = new discord.EmbedBuilder().setTitle(`Status for PR #${prNumber}`).setURL(pr.html_url);
let color: discord.ColorResolvable = 'Green';
let msg = new discord.MessageEmbed().setTitle(`Status for PR #${pr_number}`).setURL(pr.html_url);
let color = 'GREEN' as discord.ColorResolvable;
statuses.check_runs.forEach((run: any) => {
msg.addFields({ name: run.name, value: `**[${run.status} ${run.conclusion}](${run.html_url})**` });
if (run.conclusion !== 'success') color = 'Red';
msg.addField(`${run.name}`, `**[${run.status} ${run.conclusion}](${run.html_url})**`);
if (run.conclusion !== 'success') color = 'RED';
});
msg.setColor(color);
await message.channel.send({ embeds: [msg] });
}).catch(async () => {
await message.channel.send('I wasn\'t able to get the status of that PR...');
message.channel.send({ embeds: [msg] });
}).catch(() => {
message.channel.send('I wasn\'t able to get the status of that PR...')
});
}).catch(async () => {
await message.channel.send('No such PR.');
}).catch(() => {
message.channel.send('No such PR.');
});
}

View File

@ -2,23 +2,23 @@ import state from '../state';
import * as data from '../data';
import logger from '../logging';
import UserWarning from '../models/UserWarning';
import * as discord from 'discord.js';
import discord = require('discord.js');
export const roles = ['Admins', 'Moderators'];
export async function command (message: discord.Message) {
exports.roles = ['Admins', 'Moderators'];
exports.command = function (message: discord.Message) {
const silent = message.content.includes('silent');
return Promise.all(message.mentions.users.map(async (user) => {
message.mentions.users.map((user) => {
const count = state.warnings.filter(x => x.id === user.id && !x.cleared).length || 0;
if (!silent) {
await message.channel.send(`${user.toString()} You have been warned. Additional infractions may result in a ban.`);
if (silent === false) {
message.channel.send(`${user.toString()} You have been warned. Additional infractions may result in a ban.`);
}
logger.info(`${message.author.username} ${message.author} has warned ${user.username} ${user} [${count} + 1].`);
await state.logChannel?.send(`${message.author.toString()} has warned ${user.toString()} (${user.username}) [${user}] [${count} + 1].`);
state.logChannel?.send(`${message.author.toString()} has warned ${user.toString()} (${user.username}) [${user}] [${count} + 1].`);
state.warnings.push(new UserWarning(user.id, user.username, message.author.id, message.author.username, count, silent));
data.flushWarnings();
}));
}
});
};

View File

@ -1,9 +1,9 @@
import state from '../state';
import * as discord from 'discord.js';
import discord = require('discord.js');
export async function command (message: discord.Message) {
return Promise.all(message.mentions.users.map(async (user) => {
exports.command = function (message: discord.Message) {
message.mentions.users.map((user) => {
const warnings = state.warnings.filter(x => x.id === user.id && !x.cleared);
await message.channel.send(`${user.toString()}, you have ${warnings.length} total warnings.`);
}));
}
message.channel.send(`${user.toString()}, you have ${warnings.length} total warnings.`);
});
};

View File

@ -2,42 +2,19 @@ import state from './state';
import * as data from './data';
import logger from './logging';
import UserBan from './models/UserBan';
import * as discord from 'discord.js';
import discord = require('discord.js');
export async function ban (user: discord.User, moderator: discord.User, guild: discord.Guild | null) {
const count = state.warnings.filter(x => x.id === user.id && !x.cleared).length || 0;
export function ban(user: discord.User, moderator: discord.User, guild: discord.Guild | null) {
const count = state.warnings.filter(x => x.id === user.id && !x.cleared).length || 0;
logger.info(`${moderator.toString()} has banned ${user.toString()} ${user.id} ${user.username}.`);
await state.logChannel?.send(`${moderator.toString()} has banned ${user.id} ${user.toString()} [${count}].`);
logger.info(`${moderator.toString()} has banned ${user.toString()} ${user.id} ${user.username}.`);
state.logChannel?.send(`${moderator.toString()} has banned ${user.id} ${user.toString()} [${count}].`);
state.bans.push(new UserBan(user.id, user.username, moderator.id, moderator.username, count));
guild?.members?.ban(user).catch(async function (error) {
await state.logChannel?.send(`Error banning ${user.toString()} ${user.username}`);
logger.error(`Error banning ${user.toString()} ${user.id} ${user.username}.`, error);
});
data.flushBans();
}
export async function grantRole (member: discord.GuildMember, role: string, channel: discord.TextBasedChannel) {
const user = member.user;
const roleDisplayName = member.guild.roles.cache.get(role)?.name;
const alreadyJoined = member.roles.cache.has(role);
if (alreadyJoined) {
member.roles.remove(role).then(async () => {
await channel.send(`${user.toString()}'s ${roleDisplayName} role has been revoked.`);
}).catch(async () => {
await state.logChannel?.send(`Error revoking ${user.toString()}'s ${roleDisplayName} permission...`);
logger.error(`Error revoking ${user.toString()} ${user.username}'s ${roleDisplayName} permission...`);
state.bans.push(new UserBan(user.id, user.username, moderator.id, moderator.username, count));
guild?.members?.ban(user).catch(function (error) {
state.logChannel?.send(`Error banning ${user.toString()} ${user.username}`);
logger.error(`Error banning ${user.toString()} ${user.id} ${user.username}.`, error);
});
return;
}
member.roles.add(role).then(async () => {
await channel.send(`${user.toString()} has been granted ${roleDisplayName} role.`);
}).catch(async () => {
await state.logChannel?.send(`Error granting ${user.toString()}'s ${roleDisplayName} permission...`);
logger.error(`Error granting ${user.toString()} ${user.username}'s ${roleDisplayName} permission...`);
});
data.flushBans();
}

View File

@ -1,12 +1,6 @@
import * as fs from 'fs';
import state from './state';
import logger from './logging';
import { IResponses } from './models/interfaces';
const responses: { [index: string]: IResponses } = {
citra: require('./responses/citra.json'),
yuzu: require('./responses/yuzu.json')
};
export function readWarnings () {
// Load the warnings file into the application state.
@ -38,14 +32,12 @@ export function readBans () {
export function readCustomResponses () {
// Load the responses file into the responses variable.
if (process.env.TENANT) {
state.responses = responses[process.env.TENANT];
if (state.responses) {
logger.debug(`Loaded responses file for ${process.env.TENANT} from external source.`);
return;
}
try {
state.responses = require(`./responses/${process.env.TENANT}.json`);
logger.debug(`Loaded responses file for ${process.env.TENANT} from external source.`);
} catch (e) {
logger.error(`Failed to load ${process.env.TENANT}.json! Custom responses are disabled.`);
}
logger.error(`Failed to load ${process.env.TENANT}.json! Custom responses are disabled.`);
}
export function flushWarnings () {

View File

@ -1,7 +1,6 @@
import * as winston from 'winston';
import winston = require('winston');
import * as ip from 'ip';
import * as os from 'os';
import LogdnaWinston from 'logdna-winston';
const logger = winston.createLogger({
level: 'debug',
@ -14,14 +13,15 @@ const logger = winston.createLogger({
handleExceptions: true
})
],
exitOnError: true
exitOnError: true,
});
// Setup logging for LogDNA cloud logging.
if (process.env.LOGDNA_API_KEY) {
const logdnaWinston = require('logdna-winston');
const logLevel = process.env.LOGDNA_LEVEL || 'info';
logger.add(new LogdnaWinston({
logger.add(new logdnaWinston({
level: logLevel,
app: process.env.LOGDNA_APPNAME,
index_meta: true,

View File

@ -24,7 +24,7 @@ export interface IResponses {
export interface IModule {
readonly roles?: string[],
command: (message: Message, args?: string) => Promise<void> | Promise<void[]>
command: (message: Message, args?: string) => void | Promise<void>
}
export interface ITrigger {

View File

@ -2,34 +2,26 @@
"pmReply": "Please refer to our **Frequently Asked Questions**. <https://citra-emu.org/wiki/faq/>",
"quotes": {
"faq": { "reply": "Please refer to our **Frequently Asked Questions**. <https://citra-emu.org/wiki/faq/>" },
"requirements": { "reply": "Please refer to our **Frequently Asked Questions**. The only requirements for Citra are a GPU that supports at least OpenGL 4.3 or Vulkan 1.1 and a 64-bit OS, but you definitely want a processor with the highest possible performance per core. <https://citra-emu.org/wiki/faq/>"},
"cpu": { "reply": "Citra requires powerful single-core performance. Refer to your CPU in this graph. Your experience with Citra won't be enjoyable in most games if it's below 1,800. <https://www.cpubenchmark.net/singleThread.html>" },
"requirements": { "reply": "Please refer to our **Frequently Asked Questions**. The only requirements for Citra are a GPU that supports at least OpenGL 3.3 and a 64-bit OS, but you definitely want a processor with the highest possible performance per core. <https://citra-emu.org/wiki/faq/>"},
"roms": { "reply": "Please read our __community rules__. Warez/downloading games talk is strictly prohibited. To prevent legal issues, you are not allowed to post links or refer to any kind of ROM, NAND, ISO, game, or other copyrighted material that has been illegally obtained or shared. <https://citra-emu.org/rules/>"},
"dump-game": { "reply": "Please refer to our __game dumping guides__. \nFor Cartridges: <https://citra-emu.org/wiki/dumping-game-cartridges/> \nFor Installed Titles: <https://citra-emu.org/wiki/dumping-installed-titles/> \nTo dump DLC and Updates, use this guide <https://citra-emu.org/wiki/dumping-updates-and-dlcs/> and install the results through `File -> Install CIA...`"},
"dumping": {"reply": "Dumping is the act of creating an archival copy of your own legally purchased games. This process requires a hacked console to perform the dump \nDumping is legal, however sharing your dumped archival copies with anyone else is illegal."},
"apk": { "reply": "Official Citra for Android has been released! Download the .apk (the second `citra-android-universal` link) from <https://github.com/citra-emu/citra-nightly/releases>. \nIf you have the Citra Android app from the Google Play Store installed, please uninstall it before installing from our Nightly repo. Your save data will transfer over and if you have paid for Citra Premium, that will transfer over too. \n\nPlease note that the current app is a beta version and not everything works right away. We will continue to fix issues/bugs and release updates."},
"dump-system": { "reply": "Please refer to our __system dumping guide__: <https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/>"},
"apk": { "reply": "Official Citra for Android has been released! Download it from <https://citra-emu.org/download>. \nPlease note that the current app is a beta version and not everything works right away. We will continue to fix issues/bugs and release updates."},
"dump-system": { "reply": "Please refer to our __system dumping guide__. <https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/>"},
"compat": { "reply": "Click here to view our game compatibility list: <https://citra-emu.org/game/>. ***A Reminder***: Compatibility ratings are user submitted data and may not accurately reflect the game's playability. Your experience may or may not be the same due to differences in hardware or software environments."},
"alpha": { "reply": "*Citra* is currently in early stages of development. Some games usually run less than full-speed even on the best computers. Expect bugs and glitches to appear in most games. Many features found in more mature emulators are still in the works. For any major updates, please visit <https://citra-emu.org/>"},
"updates": { "reply": "You can check our latest updates on *Github*. <https://github.com/citra-emu/citra/pulse>"},
"download": { "reply": "Please only download from the official *Citra* website, as downloading from other sources is not supported here. <https://citra-emu.org/download/>"},
"legal": { "reply": "*Citra* is legal, we don't support illegal activities. Dumping your purchased games from your 3DS is legal. Downloading them is not."},
"building": { "reply": "Please refer to our __building guides__.\nWindows: <https://citra-emu.org/wiki/building-for-windows> \nmacOS: <https://citra-emu.org/wiki/building-for-macos> \nLinux: <https://citra-emu.org/wiki/building-for-linux>"},
"log": { "reply": "For assistance with your issue, please post a log file. The following guide shows how to __get the log file__: <https://community.citra-emu.org/t/how-to-upload-the-log-file/296>"},
"canary": { "reply": "The Nightly build of Citra contains already reviewed and tested features. If you require support with the installation or use of Citra, or you want to report bugs you should use this version.\n\nThe Canary build of Citra is the same as our Nightly builds, with additional features that are still waiting on review before making it into the official Citra builds. We will not provide support for issues found only in this version. If you believe youve found a bug, please retest on our Nightly builds. Both versions are still in development, so expect crashes and bugs."},
"cheats": { "reply": "Looking to add cheats? Here's how:\n\n**Desktop**\nThere are two ways to access the Cheats menu:\n1. Right click on your game in Citra's game list, then select `Properties`, and select the `Cheats` tab. \n2. After launching your game, go to `Emulation > Configure Current Game > Cheats tab`\n\nSelect **Add Cheat**, enter the name of your cheat code and the cheat code itself into the relevant boxes, and select **Save**. To enable the cheat code, tick the box next to the name of the cheat code in the `Available Cheats` list.\n\n**Android**\nLaunch your game, then press the Back button to open Citra's in-game menu, and select the `Open Cheats` option. Select the `+` button on the bottom right, then enter the name of your cheat code and the cheat code itself into the relevant boxes. Press Ok, and then press the arrow button on the top left to return to the Cheats menu. Activate the Cheat code.\n\n Cheats are stored in the gateway format. A sample collection of cheats are located at <https://github.com/iSharingan/CTRPF-AR-CHEAT-CODES/tree/master/Cheats>"},
"keys": { "reply": "Users need to provide encryption keys to use encrypted ROMs on Citra. Please follow the __AES Keys guide__ to provide the keys needed. <https://citra-emu.org/wiki/aes-keys/>"},
"hack": {"reply": "To mod your 3DS, please follow the __3DS hacking guide__: <https://3ds.hacks.guide/> \nYou will need a way to read an SD card on your device, such as a built in reader or an adapter."},
"multiplayer": {"reply": "Please refer to our __multiplayer setup guide__: <https://citra-emu.org/help/feature/multiplayer>"},
"modding": {"reply": "Please refer to our __game modding guide__: <https://citra-emu.org/help/feature/game-modding>"},
"portable": {"reply": "Go to <https://citra-emu.org/download/> and select `Manual download`. Then under **Nightly Build**, click on your operating system's icon to the right of the latest build available to download the build.\nExtract it (.7z can be extracted using Winrar or 7zip) and put it wherever you wish. Inside the extracted `nightly` folder, create a folder called `user`. This Citra should now store all of its config, save files and such inside of this `user` folder.\nCheck to make sure that this `user` folder is in the same folder that contains `citra-qt(.exe)`\nNow run the `citra-qt` executable found inside of the `nightly` folder."},
"ios": {"reply": "Citra does not have an official iOS version because iOS is a complicated operating system to develop for. We currently do not have any developers with an interest in developing for this platform. There are some iOS forks of Citra, however they are not supported by us."},
"piracy": {"reply": "*Citra* is legal, we don't support illegal activities. Dumping your purchased games from your 3DS is legal. Downloading them is not.\n\nRefer to our __game dumping guides__. \nFor Cartridges: <https://citra-emu.org/wiki/dumping-game-cartridges/> \nFor Installed Titles: <https://citra-emu.org/wiki/dumping-installed-titles/> \nTo dump DLC and Updates, use this guide <https://citra-emu.org/wiki/dumping-updates-and-dlcs/> and install the results through `File -> Install CIA...`\n\nAlso, please refresh yourself on the <#417321837916192779> you agreed to when you joined this server."},
"saves": {"reply": "Download Checkpoint and open it on the 3DS. Select the game you want, click L. If you require extdata too, not all games do, press X then press L.\n\nThese will be dumped to /3ds/Checkpoint/saves then either saves or extdata.\n\nLaunch Citra and make sure your game directory is listed on the main screen so the game shows up there. Right click on the game you want then choose Open Save Data Location or Open Extra Data Location. Make sure to launch the game at least once in Citra to create these folders first. \n\nOnce those have been open, just replace any save files in Citra with the ones dumped from your 3DS.\n<https://github.com/FlagBrew/Checkpoint>\nTo reverse this and put a Citra save on the 3DS, work backwards and use R to restore it."},
"states": {"reply": "Save states save the entirety of Citra's emulation state. As a result of this, Citra updates that add new things to serialize in the emulation state will break the loading of existing save states that were created on prior Citra versions. Additionally, saving upon save state created progress will make the emulation state gradually more unstable, since the emulation state never gets to clear out emulation bugs with a proper shut down.\nThis is why in-game save files should be used most of all when saving and loading your progress. Save states should only be used within single gaming sessions, not beyond them.\nLastly, save states cannot be transferred between different Citra installations. They may load, but they'll break the virtual file system."},
"folder": {"reply": "When you open the game directory selector, you need to choose the folder your games are in, not the games themselves. Those will auto-populate. The only exception to this are .cia ROM files, which you'll need to install using `File>Install CIA...`"},
"eshop": {"reply": "We're aware that the e-shop has closed. However, whether a game is available for purchase has no bearing on the legality of downloading that game. The games are still copyrighted, thus downloading them off of the internet is still piracy."},
"usecase": {"reply": "Emulators are for enhancing your bought game beyond what the console can offer. Which means resolution upscaling, mod support, texture packs, texture filters, speed ups, ect.\nMost importantly it's for the preservation of your games when the console will inevitably no longer be available for purchase.\nEmulators are not for people to commit theft online."},
"legacy": {"reply": "Please refer to our __legacy builds guide__ to download any older builds of Citra you may need: <https://citra-emu.org/wiki/citra-legacy-builds/>"},
"issue": {"reply": "Please refer to our __GitHub issues page__ to file an issue or a feature request: <https://github.com/citra-emu/citra/issues/new/choose>"},
"legal": { "reply": "*Citra* is legal, we don't support illegal activities. Dumping your purchased games and system files from your 3DS is legal. Downloading them is not."},
"building": { "reply": "Please refer to our building guides.\nWindows: <https://citra-emu.org/wiki/building-for-windows> \nmacOS: <https://citra-emu.org/wiki/building-for-macos> \nLinux: <https://citra-emu.org/wiki/building-for-linux>"},
"controller": { "reply": "This forum topic tells you how to __configure your gamepad / controller__: <https://community.citra-emu.org/t/temporary-controller-configurations-for-citra/1061>"},
"issues": { "reply": "This forum topic lists __known issues in games and their workarounds__: <https://community.citra-emu.org/t/known-problems-typical-issues-and-their-workarounds/1317> \nPlease read it carefully. It includes help with most games"},
"forum": { "reply": "This question might be more suitable for the *Citra* forum. <https://community.citra-emu.org/>"},
"log": { "reply": "For assistance with your issue, please accurately describe the problem and post a log file. The following guide shows how to __get the log file__: <https://community.citra-emu.org/t/how-to-upload-the-log-file/296>"},
"canary": { "reply": "The nightly build of Citra contains already reviewed and tested features. If you require support with the installation or use of Citra, or you want to report bugs you should use this version.\nThe Canary build of Citra is the same as our nightly builds, with additional features that are still waiting on review before making it into the official Citra builds. We will not provide support for issues found only in this version. If you believe youve found a bug, please retest on our nightly builds. Both versions are still in development, so expect crashes and bugs."},
"cheats": { "reply": "Looking to add cheats to Citra? Drop a .txt file with the Title ID of the game you're playing into the Cheats folder. You can find it by clicking 'File -> Open Citra Folder'. \nCheats are stored in the gateway format. A sample collection of cheats are located at <https://github.com/iSharingan/CTRPF-AR-CHEAT-CODES/tree/master/Cheats> "},
"scam": { "reply": "If you believe that a user is attempting to scam you or others, please report them to discord trust and safety and promptly block them. \nTo report the user, please follow this link <https://dis.gd/request>. \nFor more information about how to report them, please look here: <https://support.discord.com/hc/en-us/articles/360000291932-How-to-Properly-Report-Issues-to-Trust-Safety>. \nFinally, if this is the only server you share, please report the user (including screenshots of the conversation) to our moderation team, so that we can address the issue."},
"keys": { "reply": "Users need to provide encryption keys to use encrypted ROMs on Citra. Please follow the provided instructions below to obtain the keys from your 3DS. <https://citra-emu.org/wiki/aes-keys/>"},
"lenny": { "reply": "( ͡° ͜ʖ ͡°)"},
"( ͡° ͜ʖ ͡°)": { "reply": "lenny"},
"r1": { "reply": ":beginner: **Rule #1:** \nStay courteous and respectful to others."},
@ -40,7 +32,7 @@
"r6": { "reply": ":beginner: **Rule #6:** \nEnglish Only — This is an English only server. If you don't speak English well, please use a translation service such as <https://www.deepl.com/en/translator>."},
"r7": { "reply": ":beginner: **Rule #7:** \nNo Shitposting — No excessive posting of memes, low quality posts, copypastas, or other content deemed disruptive."},
"r8": { "reply": ":beginner: **Rule #8:** \nTrying to evade or circumvent any of this server's rules will be considered as breaking them. Doing so will result in warnings, and eventually a ban."},
"r9": { "reply": ":beginner: **Rule #9:** \nIn case of no written rule — the discretion of moderators shall take precedence."},
"case": {"reply": "We do not know anything other than the public filing, and we are not able to discuss the matter at this time."}
"r9": { "reply": ":beginner: **Rule #9:** \nIn case of no written rule — the discretion of moderators shall take precedence."}
}
}

View File

@ -4,7 +4,7 @@
"help": { "reply": "Need help? Refer to our **asking for help** guide. <https://yuzu-emu.org/help/reference/asking/>" },
"ea": { "reply": "In order to use **yuzu Early Access**, you must have an active forum account with your Patreon account linked to it. Please refer to the following guide to receive prioritized support: <https://yuzu-emu.org/help/early-access/>" },
"faq": { "reply": "For information on your issue, please refer to our **Frequently Asked Questions**. <https://yuzu-emu.org/wiki/faq/>" },
"hardware": { "reply": "For information on Hardware Requirements for yuzu, please refer to this page: <https://yuzu-emu.org/help/quickstart/#hardware-requirements>"},
"cpu": { "reply": "For information on Hardware Requirements for yuzu, please refer to this page: <https://yuzu-emu.org/help/quickstart/#hardware-requirements>"},
"roms": { "reply": "Please read our __community rules__. Warez/downloading games talk is strictly prohibited. To prevent legal issues, you are not allowed to post links or refer to any kind of ROM, NAND, ISO, game, or other copyrighted material that has been illegally obtained or shared. <https://yuzu-emu.org/rules/>"},
"dump-cart": { "reply": "Please refer to our __cartridge dumping guide__. <https://yuzu-emu.org/help/quickstart/#dumping-cartridge-games>"},
"alpha": { "reply": "*yuzu* is currently in very early stages of development. Some games usually run less than full-speed even on the best computers. Expect bugs and glitches to appear in most games. Many features found in more mature emulators are still in the works. For any major updates, please visit <https://yuzu-emu.org/> or the #announcements channel."},
@ -13,11 +13,10 @@
"legal": { "reply": "*yuzu* is legal, we don't support illegal activities. Dumping your purchased games and system files from your Switch is legal. Downloading them is not."},
"building": { "reply": "Please refer to our building guides.\nWindows: <https://yuzu-emu.org/wiki/building-for-windows> \nLinux: <https://yuzu-emu.org/wiki/building-for-linux>"},
"contributing": { "reply": "Contributing to the project is the best way to help the project move forward. If you are a developer, please refer to: \nSwitch reference guides: <https://github.com/yuzu-emu/yuzu/wiki/Switch-Hardware-and-Software> \nSwitch homebrew applications for testing: <https://github.com/yuzu-emu/yuzu/wiki/Switch-Homebrew> \nWiki, dedicated to Switch research: <http://switchbrew.org> \nyuzu contributing tips: <https://github.com/yuzu-emu/yuzu/blob/master/CONTRIBUTING.md>"},
"multiplayer": { "reply": "Please refer to our __multiplayer setup guide__: <https://yuzu-emu.org/help/feature/multiplayer/>"},
"lenny": { "reply": "( ͡° ͜ʖ ͡°)"},
"( ͡° ͜ʖ ͡°)": { "reply": "lenny"},
"format": { "reply": "A full description of game formats the yuzu supports and when to use them can be found on our wiki. <https://yuzu-emu.org/wiki/overview-of-switch-game-formats/>"},
"keys": { "reply": "Most games require encryption keys to boot. You can dump them from your Switch by following this guide. <https://yuzu-emu.org/help/quickstart/#dumping-decryption-keys>"},
"keys": { "reply": "Most games require encryption keys to boot. You can dump them from your Switch by following this guide. <https://yuzu-emu.org/help/quickstart/#dumping-prodkeys-and-titlekeys>"},
"game-updates": { "reply": "Installing and using game updates are a separate process from the base game. Check out our updates tutorial on our wiki. <https://yuzu-emu.org/wiki/how-to-install-and-use-game-updates/>"},
"appdata": { "reply": "Please refer to the following guide to fully reinstall yuzu: <https://yuzu-emu.org/wiki/faq/#yuzu-will-not-update-further-or-starts-with-a-qt-platform-error>"},
"log": { "reply": "For assistance with your issue, please accurately describe the problem and post a log file. The following guide shows how to __get the log file__: <https://yuzu-emu.org/help/reference/log-files/>"},
@ -35,23 +34,16 @@
"r7": { "reply": ":beginner: **Rule #7:** \nNo advertising of any kind, unless permission is granted. — Exceptions being texture packs / game mods that relate to yuzu, but these are handled on a case-by-case basis."},
"r8": { "reply": ":beginner: **Rule #8:** \nEnglish Only — This is an English only server. If you don't speak English well, please use a translation service such as <https://www.deepl.com/en/translator>."},
"r9": { "reply": ":beginner: **Rule #9:** \nNo Shitposting — No excessive posting of memes, low quality posts, copypastas, or other content deemed disruptive."},
"r10": { "reply": ":beginner: **Rule #10:** \nTo make moderation easier, users are required to use ASCII usernames. Special characters can and will be removed by staff."},
"r11": { "reply": ":beginner: **Rule #11:** \nTrying to evade or circumvent any of this server's rules will be considered as breaking them. Doing so will result in warnings, and eventually a ban."},
"r12": { "reply": ":beginner: **Rule #12:** \nIn case of no written rule — the discretion of moderators shall take precedence."},
"r10": { "reply": ":beginner: **Rule #10:** \nTrying to evade or circumvent any of this server's rules will be considered as breaking them. Doing so will result in warnings, and eventually a ban."},
"r11": { "reply": ":beginner: **Rule #11:** \nIn case of no written rule — the discretion of moderators shall take precedence."},
"mods": { "reply": "For a list of useful mods for your favorite games, check our database for downloads here: <https://github.com/yuzu-emu/yuzu/wiki/Switch-Mods>"},
"vc": { "reply": "yuzu requires the latest versions of Microsoft Visual C++. Please download and install the following dependency: <https://aka.ms/vs/17/release/vc_redist.x64.exe>"},
"vc": { "reply": "yuzu requires the latest versions of Microsoft Visual C++. Please download and install the following dependency: <https://aka.ms/vs/16/release/vc_redist.x64.exe>"},
"infringe": { "reply": "We are aware of the emulator, and are pursuing legal action. They violate our license, and also ship with copyrighted Nintendo files. As such, any references or discussion of the emulator aren't allowed."},
"android": { "reply": "yuzu Android is available now in the Google Play Store! Download for free: <https://play.google.com/store/apps/details?id=org.yuzu.yuzu_emu> or support us by purchasing Early Access: <https://play.google.com/store/apps/details?id=org.yuzu.yuzu_emu.ea>"},
"firmware": { "reply": "Some games require a firmware dump to work properly. Click the following link for instructions on how to dump the firmware from your Nintendo Switch: <https://yuzu-emu.org/help/quickstart/#dumping-system-firmware>\nClick the following link to learn how to install it: <https://yuzu-emu.org/help/quickstart/#setting-up-the-decryption-keys-and-firmware>"},
"android": { "reply": "While we would love to see yuzu ported to Android in the future, we are currently focused on improving the emulator as a whole. Look out for any future announcements, as we'll make sure to let everyone know if an Android build is on the horizon."},
"firmware": { "reply": "Some games require a firmware dump to work properly. Click the following link for instructions on how to dump the firmware from your Nintendo Switch: <https://yuzu-emu.org/help/quickstart/#dumping-system-update-firmware>"},
"raptor": { "reply": "Raptor is a 3rd party service that provides a paid alternative to Nintendo Switch Online. We are not associated with Raptor in anyway. After key emulation members and the discerning public shared their opinion with us, we cut all ties with Raptor immediately in 2020. This new fork was made without our permission or cooperation and does not have permission to use the \"yuzu\" name or branding. At this time, we have no plans to add support for any 3rd party services, including Raptor.\n\nAs per our normal policies, we only provide support for our official releases and discussion of any unofficial builds isn't allowed."},
"role": { "reply": "To claim your Patreon Discord role, please follow this guide: <https://support.patreon.com/hc/en-us/articles/212052266-Get-my-Discord-role>"},
"release": { "reply": "yuzu builds can be manually downloaded on Github: <https://github.com/yuzu-emu/yuzu-mainline/releases>"},
"rec": { "reply": "For information on recommended settings and GPU drivers for yuzu, please refer to this page: <https://community.citra-emu.org/t/recommended-settings/319349>"},
"issue": {"reply": "Please refer to our __GitHub issues page__ to file an issue or a feature request: <https://github.com/yuzu-emu/yuzu/issues/new/choose>"},
"drivers": { "reply": "yuzu can use custom drivers for Android devices with an Adreno 6XX/7XX GPU (Snapdragon SoC). There are no custom drivers for Mediatek, Exynos, or any other SoCs currently.\n\nTo use custom drivers, head to #android-drivers, open a driver thread that corresponds with your phone's GPU, and open the latest link posted in the thread.\nIf you don't know your phone's GPU, search for your phone on <https://www.gsmarena.com/>.\nDownload the .zip file from the link, go to the GPU driver manager in the Settings tab and select the downloaded driver you'd like to install."},
"portable": {"reply": "Go to <https://yuzu-emu.org/downloads/> and scroll down to `Builds`. Then under **Builds**, click on your operating system's icon to the right of the latest build available to download the build.\nExtract it and put it wherever you wish. Inside the extracted `yuzu` folder, create a folder called `user`. This yuzu should now store all of its config, save files and such inside of this `user` folder.\nCheck to make sure that this `user` folder is in the same folder that contains `yuzu(.exe)`\nNow run the `yuzu` executable found inside of the `yuzu` folder."},
"usecase": {"reply": "Emulators are for enhancing your bought game beyond what the console can offer. Which means resolution upscaling, mod support, texture packs, texture filters, speed ups, ect.\nMost importantly it's for the preservation of your games when the console will inevitably no longer be available for purchase.\nEmulators are not for people to commit theft online."},
"case": {"reply": "We do not know anything other than the public filing, and we are not able to discuss the matter at this time."}
"release": { "reply": "yuzu builds can be manually downloaded on Github: <https://github.com/yuzu-emu/yuzu-mainline/releases>"}
}
}

View File

@ -1,43 +1,41 @@
import * as discord from 'discord.js';
// Check for environmental variables.
require('checkenv').check();
import discord = require('discord.js');
import path = require('path');
import fs = require('fs');
import logger from './logging';
import state from './state';
import * as data from './data';
import { IModule, ITrigger } from './models/interfaces';
import modules from './commands/_';
import triggers from './triggers/_';
// Check for environmental variables.
import * as checkenv from 'checkenv';
import envConfig from '../env.json';
checkenv.setConfig(envConfig);
checkenv.check();
interface IModuleMap {
[name: string]: IModule;
}
const cachedModules: IModuleMap = modules;
const cachedTriggers: ITrigger[] = triggers;
const client = new discord.Client({ intents: discord.GatewayIntentBits.GuildMembers | discord.GatewayIntentBits.Guilds | discord.GatewayIntentBits.GuildBans | discord.GatewayIntentBits.GuildMessages | discord.GatewayIntentBits.DirectMessages | discord.GatewayIntentBits.MessageContent });
let cachedModules: IModuleMap = {};
let cachedTriggers: ITrigger[] = [];
const client = new discord.Client({ intents: discord.Intents.FLAGS.GUILDS | discord.Intents.FLAGS.GUILD_BANS | discord.Intents.FLAGS.GUILD_MESSAGES });
const rulesTrigger = process.env.DISCORD_RULES_TRIGGER;
const rulesRole = process.env.DISCORD_RULES_ROLE;
const rluesRole = process.env.DISCORD_RULES_ROLE;
const mediaUsers = new Map();
logger.info('Application startup. Configuring environment.');
if (!rulesTrigger) {
throw new Error('DISCORD_RULES_TRIGGER somehow became undefined.');
}
if (!rulesRole) {
if (!rluesRole) {
throw new Error('DISCORD_RULES_ROLE somehow became undefined.');
}
function findArray (haystack: string | string[], arr: string[]) {
return arr.some((v: string) => haystack.indexOf(v) >= 0);
function findArray(haystack: string | any[], arr: any[]) {
return arr.some(function (v: any) {
return haystack.indexOf(v) >= 0;
});
}
function IsIgnoredCategory (categoryName: string) {
function IsIgnoredCategory(categoryName: string) {
const IgnoredCategory = ['internal', 'team', 'development'];
return IgnoredCategory.includes(categoryName);
}
@ -47,8 +45,8 @@ client.on('ready', async () => {
if (!process.env.DISCORD_LOG_CHANNEL || !process.env.DISCORD_MSGLOG_CHANNEL) {
throw new Error('DISCORD_LOG_CHANNEL or DISCORD_MSGLOG_CHANNEL not defined.');
}
const logChannel = await client.channels.fetch(process.env.DISCORD_LOG_CHANNEL) as discord.TextChannel;
const msglogChannel = await client.channels.fetch(process.env.DISCORD_MSGLOG_CHANNEL) as discord.TextChannel;
let logChannel = await client.channels.fetch(process.env.DISCORD_LOG_CHANNEL) as discord.TextChannel;
let msglogChannel = await client.channels.fetch(process.env.DISCORD_MSGLOG_CHANNEL) as discord.TextChannel;
if (!logChannel.send) throw new Error('DISCORD_LOG_CHANNEL is not a text channel!');
if (!msglogChannel.send) throw new Error('DISCORD_MSGLOG_CHANNEL is not a text channel!');
state.logChannel = logChannel;
@ -70,86 +68,90 @@ client.on('disconnect', () => {
logger.warn('Disconnected from Discord server.');
});
client.on('guildMemberAdd', async (member) => {
if (process.env.DISCORD_RULES_ROLE) { await member.roles.add(process.env.DISCORD_RULES_ROLE); }
client.on('guildMemberAdd', (member) => {
if (process.env.DISCORD_RULES_ROLE)
member.roles.add(process.env.DISCORD_RULES_ROLE);
});
client.on('messageDelete', async (message) => {
client.on('messageDelete', message => {
const AllowedRoles = ['Administrators', 'Moderators', 'Team', 'Developer', 'Support', 'VIP'];
const authorRoles = message.member?.roles?.cache?.map(x => x.name);
let authorRoles = message.member?.roles?.cache?.map(x => x.name);
if (!authorRoles) {
logger.error(`Unable to get the roles for ${message.author}`);
return;
}
if (!findArray(authorRoles, AllowedRoles)) {
const parent = (message.channel as discord.TextChannel).parent;
if (parent && !IsIgnoredCategory(parent.name)) {
if (((message.content && !message.content.startsWith('.')) || (message.attachments.size > 0)) && message.author?.bot === false) {
const messageAttachment = message.attachments.first()?.proxyURL;
let parent = (message.channel as discord.TextChannel).parent;
if (parent && IsIgnoredCategory(parent.name) === false) {
if (((message.content && message.content.startsWith('.') === false) || (message.attachments.size > 0)) && message.author?.bot === false) {
let messageAttachment = message.attachments.first()?.proxyURL
const deletionEmbed = new discord.EmbedBuilder()
.setAuthor({ name: message.author?.tag, iconURL: message.author?.displayAvatarURL() })
const deletionEmbed = new discord.MessageEmbed()
.setAuthor(message.author?.tag, message.author?.displayAvatarURL())
.setDescription(`Message deleted in ${message.channel.toString()}`)
.addFields({ name: 'Content', value: message.cleanContent || '<no content>', inline: false })
.addField('Content', message.cleanContent || '<no content>', false)
.setTimestamp()
.setColor('Red');
.setColor('RED');
if (messageAttachment) deletionEmbed.setImage(messageAttachment);
if (messageAttachment) deletionEmbed.setImage(messageAttachment)
const userInfo = `${message.author?.toString()} (${message.author?.username}) (${message.author})`;
let userInfo = `${message.author?.toString()} (${message.author?.username}) (${message.author})`
await state.msglogChannel?.send({ content: userInfo, embeds: [deletionEmbed] });
state.msglogChannel?.send({ content: userInfo, embeds: [deletionEmbed] });
logger.info(`${message.author?.username} ${message.author} deleted message: ${message.cleanContent}.`);
}
}
}
});
client.on('messageUpdate', async (oldMessage, newMessage) => {
client.on('messageUpdate', (oldMessage, newMessage) => {
const AllowedRoles = ['Administrators', 'Moderators', 'Team', 'Developer', 'Support', 'VIP'];
const authorRoles = oldMessage.member?.roles?.cache?.map(x => x.name);
let authorRoles = oldMessage.member?.roles?.cache?.map(x => x.name);
if (!authorRoles) {
logger.error(`Unable to get the roles for ${oldMessage.author}`);
return;
}
if (!findArray(authorRoles, AllowedRoles)) {
const parent = (oldMessage.channel as discord.TextChannel).parent;
if (parent && !IsIgnoredCategory(parent.name)) {
let parent = (oldMessage.channel as discord.TextChannel).parent;
if (parent && IsIgnoredCategory(parent.name) === false) {
const oldM = oldMessage.cleanContent || '<no content>';
const newM = newMessage.cleanContent;
if (oldMessage.content !== newMessage.content && newM) {
const messageAttachment = oldMessage.attachments.first()?.proxyURL;
if (oldMessage.content !== newMessage.content && oldM && newM) {
let messageAttachment = oldMessage.attachments.first()?.proxyURL
const editedEmbed = new discord.EmbedBuilder()
.setAuthor({ name: oldMessage.author?.tag || '<unknown>', iconURL: oldMessage.author?.displayAvatarURL() })
const editedEmbed = new discord.MessageEmbed()
.setAuthor(oldMessage.author?.tag || '<unknown>', oldMessage.author?.displayAvatarURL())
.setDescription(`Message edited in ${oldMessage.channel.toString()} [Jump To Message](${newMessage.url})`)
.addFields({ name: 'Before', value: oldM, inline: false }, { name: 'After', value: newM, inline: false })
.addField('Before', oldM, false)
.addField('After', newM, false)
.setTimestamp()
.setColor('Green');
.setColor('GREEN');
if (messageAttachment) editedEmbed.setImage(messageAttachment);
if (messageAttachment) editedEmbed.setImage(messageAttachment)
const userInfo = `${oldMessage.author?.toString()} (${oldMessage.author?.username}) (${oldMessage.author})`;
let userInfo = `${oldMessage.author?.toString()} (${oldMessage.author?.username}) (${oldMessage.author})`
await state.msglogChannel?.send({ content: userInfo, embeds: [editedEmbed] });
state.msglogChannel?.send({ content: userInfo, embeds: [editedEmbed] });
logger.info(`${oldMessage.author?.username} ${oldMessage.author} edited message from: ${oldM} to: ${newM}.`);
}
}
}
});
client.on('messageCreate', async (message) => {
if (message.author.bot && !message.content.startsWith('.ban')) { return; }
client.on('messageCreate', message => {
if (message.author.bot && message.content.startsWith('.ban') === false) { return; }
if (message.guild == null && state.responses.pmReply) {
// We want to reply to PM attempts.
await message.reply(state.responses.pmReply);
// We want to log PM attempts.
// logger.info(`${message.author.username} ${message.author} [PM]: ${message.content}`);
// state.logChannel.send(`${message.author.toString()} [PM]: ${message.content}`);
message.reply(state.responses.pmReply);
return;
}
logger.verbose(`${message.author.username} ${message.author} [Channel: ${(message.channel as discord.TextChannel).name} ${message.channel}]: ${message.content}`);
const authorRoles = message.member?.roles?.cache?.map(x => x.name);
let authorRoles = message.member?.roles?.cache?.map(x => x.name);
if (message.channel.id === process.env.DISCORD_MEDIA_CHANNEL && !message.author.bot) {
const AllowedMediaRoles = ['Administrators', 'Moderators', 'Team', 'VIP'];
@ -158,13 +160,13 @@ client.on('messageCreate', async (message) => {
return;
}
if (!findArray(authorRoles, AllowedMediaRoles)) {
const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_+.~#?&/=]*)/gi;
const urlRegex = new RegExp(/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_+.~#?&\/=]*)/gi);
if (message.attachments.size > 0 || message.content.match(urlRegex)) {
mediaUsers.set(message.author.id, true);
} else if (mediaUsers.get(message.author.id)) {
mediaUsers.set(message.author.id, false);
} else {
await message.delete();
message.delete();
mediaUsers.set(message.author.id, false);
}
}
@ -175,18 +177,18 @@ client.on('messageCreate', async (message) => {
if (message.content.toLowerCase().includes(rulesTrigger)) {
// We want to remove the 'Unauthorized' role from them once they agree to the rules.
logger.verbose(`${message.author.username} ${message.author} has accepted the rules, removing role ${process.env.DISCORD_RULES_ROLE}.`);
await message.member?.roles.remove(rulesRole, 'Accepted the rules.');
message.member?.roles.remove(rluesRole, 'Accepted the rules.');
}
// Delete the message in the channel to force a cleanup.
await message.delete();
} else if (message.content.startsWith('.') && !message.content.startsWith('..')) {
message.delete();
} else if (message.content.startsWith('.') && message.content.startsWith('..') === false) {
// We want to make sure it's an actual command, not someone '...'-ing.
const cmd = message.content.split(' ', 1)[0].slice(1);
// Check by the name of the command.
const cachedModule = cachedModules[`${cmd.toLowerCase()}`];
let quoteResponse: { reply: string; } | null = null;
let cachedModule = cachedModules[`${cmd.toLowerCase()}`];
let quoteResponse = null;
// Check by the quotes in the configuration.
if (!cachedModule) quoteResponse = state.responses.quotes[cmd];
if (!cachedModule && !quoteResponse) return; // Not a valid command.
@ -196,52 +198,69 @@ client.on('messageCreate', async (message) => {
logger.error(`Unable to get the roles for ${message.author}`);
return;
}
const allowedRoles = cmd === 'case' ? ['Admins', 'Moderators', 'Developer'] : cachedModule?.roles;
const isAllowed = (!allowedRoles) || (allowedRoles && findArray(authorRoles, allowedRoles));
if (!isAllowed) {
await state.logChannel?.send(`${message.author.toString()} attempted to use admin command: ${message.content}`);
if (cachedModule && cachedModule.roles && !findArray(authorRoles, cachedModule.roles)) {
state.logChannel?.send(`${message.author.toString()} attempted to use admin command: ${message.content}`);
logger.info(`${message.author.username} ${message.author} attempted to use admin command: ${message.content}`);
return;
}
logger.info(`${message.author.username} ${message.author} [Channel: ${message.channel}] executed command: ${message.content}`);
const executeModule = async () => {
try {
if (cachedModule) {
await cachedModule.command(message);
} else if (cachedModules.quote && quoteResponse) {
await cachedModules.quote.command(message, quoteResponse.reply);
}
} catch (err) { logger.error(err); }
};
const commandUsageEmbed = new discord.EmbedBuilder()
.setAuthor({ name: message.author.username, iconURL: message.author.displayAvatarURL() })
.setDescription(`Command used in ${message.channel.toString()} [Jump To Message](${message.url})`)
.addFields({ name: 'Command', value: `\`\`\`\n${message.content}\n\`\`\``, inline: false })
.setTimestamp()
.setColor('Blue');
const userInfo = `\`${message.author?.toString()}\` (${message.author?.username})`;
await Promise.all(
[
state.msglogChannel?.send({ content: userInfo, embeds: [commandUsageEmbed] }),
message.delete(),
executeModule()
]
);
} else if (!message.author.bot) {
message.delete();
try {
if (!!cachedModule) {
cachedModule.command(message);
} else if (cachedModules['quote']) {
cachedModules['quote'].command(message, quoteResponse?.reply);
}
} catch (err) { logger.error(err); }
} else if (message.author.bot === false) {
// This is a normal channel message.
await Promise.all(
cachedTriggers.map(async function (trigger) {
if (!trigger.roles || (authorRoles && findArray(authorRoles, trigger.roles))) {
if (trigger.trigger(message)) {
logger.debug(`${message.author.username} ${message.author} [Channel: ${message.channel}] triggered: ${message.content}`);
try {
await trigger.execute(message);
} catch (err) { logger.error(err); }
}
cachedTriggers.forEach(function (trigger) {
if (!trigger.roles || authorRoles && findArray(authorRoles, trigger.roles)) {
if (trigger.trigger(message) === true) {
logger.debug(`${message.author.username} ${message.author} [Channel: ${message.channel}] triggered: ${message.content}`);
try {
trigger.execute(message);
} catch (err) { logger.error(err); }
}
})
);
}
});
}
});
// Cache all command modules.
cachedModules = {};
fs.readdirSync('./commands/').forEach(function (file) {
// Load the module if it's a script.
if (path.extname(file) === '.js') {
if (file.includes('.disabled')) {
logger.info(`Did not load disabled module: ${file}`);
} else {
const moduleName = path.basename(file, '.js').toLowerCase();
logger.info(`Loaded module: ${moduleName} from ${file}`);
cachedModules[moduleName] = require(`./commands/${file}`);
}
}
});
// Cache all triggers.
cachedTriggers = [];
fs.readdirSync('./triggers/').forEach(function (file) {
// Load the module if it's a script.
if (path.extname(file) === '.js') {
if (file.includes('.disabled')) {
logger.info(`Did not load disabled trigger: ${file}`);
} else {
const moduleName = path.basename(file, '.js').toLowerCase();
logger.info(`Loaded trigger: ${moduleName} from ${file}`);
try {
cachedTriggers.push(require(`./triggers/${file}`));
} catch (e) {
logger.error(`Could not load trigger ${moduleName}: ${e}`);
}
}
}
});
@ -253,5 +272,5 @@ if (process.env.DATA_CUSTOM_RESPONSES) {
data.readCustomResponses();
}
client.login(process.env.DISCORD_LOGIN_TOKEN).catch(err => logger.error(err));
client.login(process.env.DISCORD_LOGIN_TOKEN);
logger.info('Startup completed. Established connection to Discord.');

View File

@ -1,7 +1,7 @@
import UserWarning from './models/UserWarning';
import UserBan from './models/UserBan';
import { IGameDBEntry, IResponses } from './models/interfaces';
import * as discord from 'discord.js';
import discord = require('discord.js');
/* Application State */
class State {

View File

@ -1,22 +1,15 @@
import { ban } from '../common';
import state from '../state';
import logger from '../logging';
import * as discord from 'discord.js';
import discord = require('discord.js');
const ExemptRoles = ['Administrators', 'Moderators', 'Team', 'Developer', 'Support', 'VIP'];
export function trigger (message: discord.Message) {
return message.mentions.users.size > 10;
export function trigger(message: discord.Message) {
return message.mentions.users.size > 10;
}
export async function execute (message: discord.Message) {
const count = message.mentions.users.size;
const exempt = message.member?.roles?.cache.find(role => ExemptRoles.includes(role.name)) !== undefined;
logger.info(`${message.author.toString()} tagged ${count} users in ${message.channel.toString()}`);
await state.logChannel?.send(`Ping bomb detected in ${message.channel.toString()} by ${message.author.toString()}`);
if (exempt) {
await state.logChannel?.send(`... however ${message.author.toString()} is exempt from the banning rule.`);
} else {
await ban(message.author, message.author, message.guild);
}
}
export function execute(message: discord.Message) {
const count = message.mentions.users.size;
logger.info(`${message.author.toString()} tagged ${count} users in ${message.channel.toString()}`);
state.logChannel?.send(`Ping bomb detected in ${message.channel.toString()} by ${message.author.toString()}`);
ban(message.author, message.author, message.guild);
};

View File

@ -1,21 +1,16 @@
{
"extends": "@tsconfig/node20/tsconfig.json",
"extends": "@tsconfig/node14/tsconfig.json",
"compilerOptions": {
"module": "node16",
"module": "CommonJS",
"noImplicitAny": true,
"strictNullChecks": true,
"resolveJsonModule": true,
"removeComments": true,
"preserveConstEnums": true,
"outDir": "dist/",
"sourceMap": true,
"esModuleInterop": true,
"lib": [
"es2018",
"dom"
],
"typeRoots": [
"./typings",
"./node_modules/@types/"
]
},
"include": [

View File

@ -1,5 +0,0 @@
declare module 'checkenv';
export function load(): string;
export function check(): void;
export function setConfig(config: any): void;

View File

@ -1,27 +0,0 @@
// Taken from https://github.com/efugulin/logdna-winston/blob/master/index.d.ts
import { ConstructorOptions } from 'logdna';
import Transport from 'winston-transport';
declare class LogDNATransport extends Transport {
constructor(options: LogDNATransport.TransportOptions);
}
declare namespace LogDNATransport {
interface TransportOptions
extends Transport.TransportStreamOptions,
ConstructorOptions {
/** The LogDNA API key. */
key: string;
/** The name of this transport (default: "LogDNA"). */
name?: string;
/** Level of messages that this transport should log (default: "debug"). */
level?: string;
/**
* Allow meta objects to be passed with each line (default: false).
* See logger ConstructorOptions for more information.
*/
index_meta?: boolean;
}
}
export = LogDNATransport;

6010
yarn.lock

File diff suppressed because it is too large Load Diff