start migrationing to typescript

This commit is contained in:
kdh8219 2023-04-26 19:23:30 +09:00
parent 6c3905ef7e
commit 5a474ba8fb
27 changed files with 371 additions and 123 deletions

1
.env Normal file
View file

@ -0,0 +1 @@
firebase_cert = '{"type":"service_account","project_id":"k2r-ec9d5","private_key_id":"3adfb8f91bb2de3c2b2814ebbd15bb7ed131d360","private_key":"btw","client_email":"firebase-adminsdk-ege3y@k2r-ec9d5.iam.gserviceaccount.com","client_id":"114795287777519067325","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_x509_cert_url":"https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-ege3y%40k2r-ec9d5.iam.gserviceaccount.com"}'

11
README.md Normal file
View file

@ -0,0 +1,11 @@
# 2K2R namebot
> A discord bot that manage users's minecraft accounts
## TO-DO
[ ] db 바꾸기(firebase/database->firebase/firestore)
[ ] typrscript 마이그레이션 완료하기
[ ] 유져 나가면 지우는 기능 만들기
[ ] 출력 형식 바꾸기
[ ] 샤딩

View file

@ -1,72 +0,0 @@
const { SlashCommandBuilder, PermissionFlagsBits } = require("discord.js");
const mojangAPI = new (require("mojang-api-js"))();
const firebase_admin = require("firebase-admin");
const { getDatabase } = require("firebase-admin/database");
const serviceAccount = require("../firebase/conf.json");
if (!firebase_admin.apps.length) {
firebase_admin.initializeApp({
credential: firebase_admin.credential.cert(serviceAccount),
databaseURL: "https://two-k-two-r-name-bot-default-rtdb.firebaseio.com",
databaseAuthVariableOverride: {
uid: process.env.FIREBASE_UID,
},
});
}
const firebase = getDatabase().ref("/");
module.exports = {
data: new SlashCommandBuilder()
.setName("add_nick_super")
.setDescription("Add your nickname on the list and share it to 2k2rs")
.addUserOption((option) => option.setName("discord").setDescription("discord account").setRequired(true))
.addStringOption((option) => option.setName("minecraft_id").setDescription("id of the minecraft account").setRequired(true))
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
.setDMPermission(false),
async execute(interaction) {
await interaction.deferReply({ ephemeral: true });
let minecraft;
try {
minecraft = await mojangAPI.nameToUuid(interaction.options.getString("minecraft_id"));
} catch (err) {
if (err.message == "204 status code") {
await interaction.editReply({ content: "`에러`: 닉네임 검색을 실패했어요(닉네임을 정확하게 입력했나요?)", ephemeral: true });
return;
}
}
let data = (await firebase.get()).val();
const discord_id = interaction.options.getUser("discord").id;
for (let discord_search_point in data["blacklist"]) {
if (data["blacklist"][discord_search_point].discord == discord_id) {
await interaction.editReply({ content: "`에러`:해당 디스코드 아이디는 블랙리스팅 되었습니다!", ephemeral: true });
return;
}
for (let minecraft_search_point in data["blacklist"][discord_search_point].minecraft) {
if (data["blacklist"][discord_search_point].minecraft[minecraft_search_point] == minecraft["id"]) {
await interaction.editReply({ content: "`에러`:해당 마인크래프트 아이디는 블랙리스팅 되었습니다!", ephemeral: true });
return;
}
} //신규 마크일 경우
} //신규 디코일 경우
for (let discord_search_point in data["members"]) {
for (let minecraft_search_point in data["members"][discord_search_point].minecraft) {
if (data["members"][discord_search_point].minecraft[minecraft_search_point] == minecraft["id"]) {
await interaction.editReply({ content: "`에러`:해당 마인크래프트 아이디는 이미 등록되었습니다!", ephemeral: true });
return;
}
} //신규 마크일 경우
if (data["members"][discord_search_point].discord == discord_id) {
await interaction.editReply({ content: `${minecraft["name"]}(${minecraft["id"]})이 성공적으로 부계정으로 등록되었습니다!`, ephemeral: true });
data["members"][discord_search_point].minecraft.push(minecraft["id"]);
await firebase.set(data);
return;
}
} //신규 디코일 경우
await interaction.editReply({ content: `${minecraft["name"]}(${minecraft["id"]})님 2k2r에 오신것을 환영합니다!`, ephemeral: true });
data["members"].push({ discord: discord_id, minecraft: [minecraft["id"]] });
await firebase.set(data);
},
};

41
main.js
View file

@ -1,41 +0,0 @@
const fs = require("node:fs");
const path = require("node:path");
const { Client, Collection, Events, GatewayIntentBits } = require("discord.js");
require("dotenv").config();
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
client.commands = new Collection();
const commandsPath = path.join(__dirname, "commands");
const commandFiles = fs.readdirSync(commandsPath).filter((file) => file.endsWith(".js"));
for (const file of commandFiles) {
const filePath = path.join(commandsPath, file);
const command = require(filePath);
client.commands.set(command.data.name, command);
}
client.once(Events.ClientReady, (c) => {
console.log(`Ready: discord client as ${c.user.tag}`);
});
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
const command = client.commands.get(interaction.commandName);
if (!command) return;
try {
await command.execute(interaction);
} catch (error) {
console.error(error);
try {
await interaction.reply({ content: "There was an error while executing this command!", ephemeral: true });
} catch (e) {
await interaction.editReply({ content: "There was an error while executing this command!", ephemeral: true });
}
}
});
client.login(process.env.DISCORD_TOKEN);

View file

@ -4,6 +4,13 @@
"dotenv": "^16.0.3",
"firebase-admin": "^11.2.0",
"mojang-api-js": "^1.0.1",
"nodemon": "^2.0.20"
}
"nodemon": "^2.0.20",
"typescritp": "^1.0.0"
},
"name": "two-k-two-r-name-bot",
"version": "2.0.0",
"description": "a discord bot for 2k2r",
"main": "src/main.ts",
"author": "kdh8219 <65698239+kdh8219@users.noreply.github.com>",
"private": true
}

6
src/command/commands.ts Normal file
View file

@ -0,0 +1,6 @@
import { TCommand } from "../functions.js";
import add_nick_super from "./commands/add_nick_super.js";
const commands: TCommand[] = [add_nick_super];
export default commands;

View file

@ -0,0 +1,89 @@
import {
SlashCommandBuilder,
PermissionFlagsBits,
ChatInputCommandInteraction,
User,
} from "discord.js";
import mojangAPI from "../../wrapper/mojang-api.js";
import firebase from "../../wrapper/firebase.js";
import {
check_user_type_by_discord_id,
check_user_type_by_minecraft_uuid,
EUserType,
TData,
} from "../../functions.js";
export default {
data: new SlashCommandBuilder()
.setName("add_nick_super")
.setDescription("Add ones nickname on the list and share it to 2k2rs")
.addUserOption((option) =>
option
.setName("discord")
.setDescription("discord account")
.setRequired(true)
)
.addStringOption((option) =>
option
.setName("minecraft_id")
.setDescription("id of the minecraft account")
.setRequired(true)
)
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
.setDMPermission(false),
async execute(interaction: ChatInputCommandInteraction) {
let data = (await firebase.get()).val() as TData;
const discord_id = interaction.options.getUser("discord")?.id as string;
const dcusertype = check_user_type_by_discord_id(data, discord_id);
if (dcusertype == EUserType.Blacklisted) {
interaction.editReply({
content: "`에러`: 해당 디스코드 아이디는 블랙리스트 되었습니다.",
});
return;
}
const mcid = interaction.options.getString("minecraft_id") as string;
let mcuuid: string;
try {
mcuuid = await mojangAPI.getUUIDFromId(mcid);
} catch {
await interaction.editReply({
content:
"`에러`: 마인크래프트 닉네임 검색을 실패했습니다. 마인크래프트 닉네임을 정확하게 입력했나요?",
});
return;
}
const mcusertype = check_user_type_by_minecraft_uuid(data, mcuuid);
if (mcusertype == EUserType.Blacklisted) {
interaction.editReply({
content: "`에러`: 해당 마인크래프트 아이디는 블랙리스트 되었습니다.",
});
return;
} else if (mcusertype == EUserType.Member) {
interaction.editReply({
content: "`에러`: 해당 마인크래프트 아이디는 이미 등록되었습니다.",
});
return;
}
if (dcusertype == EUserType.Unregistered) {
await interaction.editReply({
content: `${mcid}(${mcuuid})님 2k2r에 오신것을 환영합니다!`,
});
data["members"].push({ discord: discord_id, minecraft: [mcuuid] });
} else {
for (let member of data.members) {
if (member.discord == discord_id) {
await interaction.editReply({
content: `${mcid}(${mcuuid})이 성공적으로 부계정으로 등록되었습니다!`,
});
member.minecraft.push(mcuuid);
}
} //신규 디코일 경우
}
await firebase.set(data);
},
};

View file

@ -1,8 +1,12 @@
const { SlashCommandBuilder, AttachmentBuilder, PermissionFlagsBits } = require("discord.js");
const {
SlashCommandBuilder,
AttachmentBuilder,
PermissionFlagsBits,
} = require("discord.js");
const firebase_admin = require("firebase-admin");
const { getDatabase } = require("firebase-admin/database");
const serviceAccount = require("../firebase/conf.json");
const serviceAccount = require("../../firebase/conf.json");
if (!firebase_admin.apps.length) {
firebase_admin.initializeApp({
credential: firebase_admin.credential.cert(serviceAccount),
@ -15,10 +19,16 @@ if (!firebase_admin.apps.length) {
const firebase = getDatabase().ref("/");
module.exports = {
data: new SlashCommandBuilder().setName("get_file_super").setDescription("get the file").setDefaultMemberPermissions(PermissionFlagsBits.Administrator).setDMPermission(false),
data: new SlashCommandBuilder()
.setName("get_file_super")
.setDescription("get the file")
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
.setDMPermission(false),
async execute(interaction) {
await interaction.deferReply({ ephemeral: true });
await interaction.editReply("해당 커맨드에 버그가 있어서 임시로 비활성화 되었습니다...........")
await interaction.deferReply({ ephemeral: true });
await interaction.editReply(
"해당 커맨드에 버그가 있어서 임시로 비활성화 되었습니다..........."
);
// const data = (await firebase.get()).val();
// for (let discord_search_point in data["members"]) {
// if (data["members"][discord_search_point].discord == interaction.user.id) {

View file

@ -18,7 +18,12 @@ module.exports = {
data: new SlashCommandBuilder()
.setName("mov_blacklist_super")
.setDescription("add user to blacklist")
.addUserOption((option) => option.setName("discord").setDescription("discord id of blacklister").setRequired(true))
.addUserOption((option) =>
option
.setName("discord")
.setDescription("discord id of blacklister")
.setRequired(true)
)
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
.setDMPermission(false),
async execute(interaction) {
@ -32,10 +37,17 @@ module.exports = {
data["members"].splice(discord_search_point, 1);
data["blacklist"].push(blackListing);
await firebase.set(data);
await interaction.editReply({ content: "성공적으로 해당 discord id와 minecraft id를 블렉리스팅 했어요!", ephemeral: true });
await interaction.editReply({
content:
"성공적으로 해당 discord id와 minecraft id를 블렉리스팅 했어요!",
ephemeral: true,
});
return;
}
} //등록되지 않았을경우
await interaction.editReply({ content: "`에러`:해당 discord id를 찾을 수 없었어요", ephemeral: true }); //TODO:
await interaction.editReply({
content: "`에러`:해당 discord id를 찾을 수 없었어요",
ephemeral: true,
});
},
};

65
src/functions.ts Normal file
View file

@ -0,0 +1,65 @@
import { SlashCommandBuilder } from "discord.js";
export type TCommand = {
data: SlashCommandBuilder;
execute: Function;
};
export type TUser = {
discord: string; // id
minecraft: string[]; //uuid
};
export type TData = {
blacklist: TUser[];
members: TUser[];
};
export enum EUserType {
Unregistered,
Blacklisted,
Member,
}
export function check_user_type_by_discord_id(
data: TData,
discord_id: string
): EUserType {
for (let discord_search_point in data["blacklist"]) {
if (data.blacklist[discord_search_point].discord === discord_id) {
return EUserType.Blacklisted;
}
}
for (let discord_search_point in data["members"]) {
if (data.blacklist[discord_search_point].discord === discord_id) {
return EUserType.Member;
}
}
return EUserType.Unregistered;
}
export function check_user_type_by_minecraft_uuid(
data: TData,
minecraft_uuid: string
): EUserType {
for (let discord_search_point in data.blacklist) {
for (let minecraft_search_point in data.blacklist[discord_search_point]
.minecraft) {
if (
data.blacklist[discord_search_point].minecraft[
minecraft_search_point
] === minecraft_uuid
) {
return EUserType.Blacklisted;
}
}
}
for (let discord_search_point in data.members) {
for (let minecraft_search_point in data.members[discord_search_point]
.minecraft) {
if (
data.members[discord_search_point].minecraft[minecraft_search_point] ===
minecraft_uuid
) {
return EUserType.Member;
}
}
}
return EUserType.Unregistered;
}

54
src/main.ts Executable file
View file

@ -0,0 +1,54 @@
import { Client, Collection, Events, GatewayIntentBits } from "discord.js";
import { config } from "dotenv";
config();
import { TCommand } from "./functions.js";
import commands from "./command/commands.js";
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
const commandColection = getCommands();
export let onTime: Date;
client.once(Events.ClientReady, (c) => {
console.log(`Ready: discord client as ${c.user.tag}`);
onTime = new Date();
});
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
const command = commandColection.get(interaction.commandName);
if (interaction.commandName !== "ping") {
await interaction.deferReply({ ephemeral: true });
}
if (!command) return;
try {
await command.execute(interaction);
} catch (error) {
console.error(error);
try {
await interaction.reply({
content: "There was an error while executing this command!",
ephemeral: true,
});
} catch (e) {
await interaction.editReply({
content: "There was an error while executing this command!",
});
console.error(e);
}
}
});
client.login(process.env.DISCORD_TOKEN);
function getCommands(): Collection<string, TCommand> {
const commandColection = new Collection<string, TCommand>();
for (const command of commands) {
commandColection.set(command.data.name, command);
}
return commandColection;
}

47
src/utils/two-way-map.ts Normal file
View file

@ -0,0 +1,47 @@
/**
* map
*
* ex) -uuid
*/
export default class TwoWayMap<T1, T2> {
private firstMap: Map<T1, T2> = new Map();
private secondMap: Map<T2, T1> = new Map();
get_by_first(getter: T1): T2 | undefined {
return this.firstMap.get(getter);
}
get_by_second(getter: T2): T1 | undefined {
return this.secondMap.get(getter);
}
set_by_first(first: T1, second: T2) {
const secondGet = this.secondMap.get(second);
if (!secondGet) {
throw new Error("Duplicate second argument");
}
this.firstMap.set(first, second);
this.secondMap.set(second, first);
}
set_by_second(first: T1, second: T2) {
const firstGet = this.firstMap.get(first);
if (!firstGet) {
throw new Error("Duplicate first argument");
}
this.firstMap.set(first, second);
this.secondMap.set(second, first);
}
remove_by_first(first: T1): T2 | undefined {
const firstget = this.firstMap.get(first);
if (!firstget) return undefined;
this.firstMap.delete(first);
this.secondMap.delete(firstget);
return firstget;
}
remove_by_second(second: T2): T1 | undefined {
const secondget = this.secondMap.get(second);
if (!secondget) return undefined;
this.firstMap.delete(secondget);
this.secondMap.delete(second);
return secondget;
}
}

19
src/wrapper/firebase.ts Normal file
View file

@ -0,0 +1,19 @@
import firebase_admin from "firebase-admin";
import { getDatabase } from "firebase-admin/database";
import { config } from "dotenv";
config();
if (!firebase_admin.apps.length) {
firebase_admin.initializeApp({
credential: firebase_admin.credential.cert(
JSON.parse(process.env["firebase_cert"] as string)
),
databaseURL: "https://two-k-two-r-name-bot-default-rtdb.firebaseio.com",
databaseAuthVariableOverride: {
uid: process.env.FIREBASE_UID,
},
});
}
const firebase = getDatabase().ref("/");
export default firebase;

34
src/wrapper/mojang-api.ts Normal file
View file

@ -0,0 +1,34 @@
import Client from "mojang-api-js";
import TwoWayMmap from "../utils/two-way-map.js";
class MojangAPI {
private mojangAPI: Client = new Client();
private cached: TwoWayMmap<string, string> = new TwoWayMmap<string, string>(); // uuid, id
async getIdFromUUID(minecraft_uuid: string): Promise<string> {
let id = this.cached.get_by_first(minecraft_uuid);
if (id) return id;
let mcid = await this.mojangAPI.uuidToName(minecraft_uuid);
id = mcid.id;
if (!id) throw new Error("Failed to get minecraft id from api");
this.cached.set_by_first(minecraft_uuid, id);
return id;
}
async getUUIDFromId(minecraft_id): Promise<string> {
let uuid = this.cached.get_by_second(minecraft_id);
if (uuid) return uuid;
let mcid = await this.mojangAPI.nameToUuid(minecraft_id);
uuid = mcid.id;
if (!uuid) throw new Error("Failed to get minecraft id from api");
this.cached.remove_by_first(uuid);
this.cached.set_by_second(uuid, minecraft_id);
return uuid;
}
}
const mojangAPI = new MojangAPI();
export default mojangAPI;

View file

@ -0,0 +1 @@
// firebase/databse -> firebase/firestore

View file

@ -1680,6 +1680,11 @@ type-check@~0.3.2:
dependencies:
prelude-ls "~1.1.2"
typescritp@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/typescritp/-/typescritp-1.0.0.tgz#7923ebbd3ec74f780633c8be2e8aa1d2ecac2b8f"
integrity sha512-88hSOM0JRNE+lY8LQvwebA6WCf54+FoDUQuNCuoBvHxu+tj76LteTUymKzwuS6VVAuBLfI5xcZyv88sj4yxDaw==
uc.micro@^1.0.1, uc.micro@^1.0.5:
version "1.0.6"
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"