summary refs log tree commit diff homepage
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Discord.ts120
-rw-r--r--src/MinecraftHandler.ts8
-rw-r--r--src/Rcon.ts4
-rw-r--r--src/Shulker.ts15
4 files changed, 89 insertions, 58 deletions
diff --git a/src/Discord.ts b/src/Discord.ts
index 0c96131..2b086ae 100644
--- a/src/Discord.ts
+++ b/src/Discord.ts
@@ -1,4 +1,4 @@
-import {Client, Message, Snowflake, TextChannel} from 'discord.js'
+import {Client, Intents, Message, TextChannel, User} from 'discord.js'
 
 import emojiStrip from 'emoji-strip'
 import axios from 'axios'
@@ -14,46 +14,58 @@ class Discord {
   channel: TextChannel | null
 
   uuidCache: Map<string, string>
+  mentionCache: Map<string, User>
 
   constructor (config: Config, onReady?: () => void) {
     this.config = config
 
-    this.client = new Client()
+    this.client = new Client({ intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES] })
     if (onReady) this.client.once('ready', () => onReady())
-    this.client.on('message', (message: Message) => this.onMessage(message))
+    this.client.on('messageCreate', (message: Message) => this.onMessage(message))
 
     this.channel = null
 
     this.uuidCache = new Map()
+    this.mentionCache = new Map()
   }
 
   public async init () {
     try {
       await this.client.login(this.config.DISCORD_TOKEN)
-      if (this.config.DISCORD_CHANNEL_NAME && !this.config.DISCORD_CHANNEL_ID) {
-        this.getChannelIdFromName(this.config.DISCORD_CHANNEL_NAME)
-      } else if (this.config.DISCORD_CHANNEL_ID) {
-        const channel = this.client.channels.find((ch) => ch.id === this.config.DISCORD_CHANNEL_ID && ch.type === 'text') as TextChannel
-        if (!channel) {
-          console.log(`[INFO] Could not find channel with ID ${this.config.DISCORD_CHANNEL_ID}. Please check that the ID is correct and that the bot has access to it.`)
-          process.exit(1)
-        }
-        this.channel = channel
-      }
     } catch (e) {
       console.log('[ERROR] Could not authenticate with Discord: ' + e)
       if (this.config.DEBUG) console.error(e)
+      process.exit(1)
+    }
+
+    if (this.config.DISCORD_CHANNEL_NAME && !this.config.DISCORD_CHANNEL_ID) {
+      await this.getChannelIdFromName(this.config.DISCORD_CHANNEL_NAME)
+    } else if (this.config.DISCORD_CHANNEL_ID) {
+      const channel = await this.client.channels.fetch(this.config.DISCORD_CHANNEL_ID) as TextChannel
+      if (!channel) {
+        console.log(`[INFO] Could not find channel with ID ${this.config.DISCORD_CHANNEL_ID}. Please check that the ID is correct and that the bot has access to it.`)
+        process.exit(1)
+      }
+      this.channel = channel
+    }
+
+    if (this.channel) {
+      console.log(`[INFO] Using channel #${this.channel.name} (id: ${this.channel.id}) in the server "${this.channel.guild.name}"`)
     }
   }
 
-  private getChannelIdFromName (name: string) {
+  private async getChannelIdFromName (name: string) {
     // remove the # if there is one
     if (name.startsWith('#')) name = name.substring(1, name.length)
-    // @ts-ignore
-    const channel: TextChannel = this.client.channels.find((c: TextChannel) => c.type === 'text' && c.name === name && !c.deleted)
+
+    // fetch all the channels in every server
+    for (const guild of this.client.guilds.cache.values()) {
+      await guild.channels.fetch()
+    }
+
+    const channel = this.client.channels.cache.find((c) => c.isText() && c.type === 'GUILD_TEXT' && c.name === name && !c.deleted)
     if (channel) {
-      this.channel = channel
-      console.log(`[INFO] Found channel #${channel.name} (id: ${channel.id}) in the server "${channel.guild.name}"`)
+      this.channel = channel as TextChannel
     } else {
       console.log(`[INFO] Could not find channel ${name}! Check that the name is correct or use the ID of the channel instead (DISCORD_CHANNEL_ID)!`)
       process.exit(1)
@@ -61,7 +73,7 @@ class Discord {
   }
 
   private parseDiscordWebhook (url: string) {
-    const re = /discordapp.com\/api\/webhooks\/([^\/]+)\/([^\/]+)/
+    const re = /discord[app]?.com\/api\/webhooks\/([^\/]+)\/([^\/]+)/
 
     // the is of the webhook
     let id = null
@@ -85,9 +97,9 @@ class Discord {
     // no channel, done
     if (!this.channel) return
     // don't want to check other channels
-    if (message.channel.id !== this.channel.id || message.channel.type !== 'text') return
+    if (message.channel.id !== this.channel.id || message.channel.type !== 'GUILD_TEXT') return
     // if using webhooks, ignore this!
-    if (message.webhookID) {
+    if (message.webhookId) {
       // backwards compatability with older config
       if (this.config.USE_WEBHOOKS && this.config.IGNORE_WEBHOOKS === undefined) return
 
@@ -97,7 +109,7 @@ class Discord {
       } else if (this.config.USE_WEBHOOKS) {
         // otherwise, ignore all webhooks that are not the same as this one
         const { id } = this.parseDiscordWebhook(this.config.WEBHOOK_URL)
-        if (id === message.webhookID) {
+        if (id === message.webhookId) {
           if (this.config.DEBUG) console.log('[INFO] Ignoring webhook from self')
           return
         }
@@ -108,9 +120,9 @@ class Discord {
     // ensure that the message is a text message
     if (message.type !== 'DEFAULT') return
     // if the same user as the bot, ignore
-    if (message.author.id === this.client.user.id) return
+    if (message.author.id === this.client.user?.id) return
     // ignore any attachments
-    if (message.attachments.array().length) return
+    if (message.attachments.size) return
 
     const rcon = new Rcon(this.config.MINECRAFT_SERVER_RCON_IP, this.config.MINECRAFT_SERVER_RCON_PORT, this.config.DEBUG)
     try {
@@ -120,10 +132,10 @@ class Discord {
       if (this.config.DEBUG) console.error(e)
     }
 
-    let command = ''
-    if (this.config.ALLOW_SLASH_COMMANDS && this.config.SLASH_COMMAND_ROLES && message.cleanContent.startsWith('/')) {
-      const author = message.member
-      if (author.roles.find(r => this.config.SLASH_COMMAND_ROLES.includes(r.name))) {
+    let command: string | undefined;
+    if (this.config.ALLOW_SLASH_COMMANDS && this.config.SLASH_COMMAND_ROLES && message.cleanContent.startsWith('/') && message.member) {
+      const hasSlashCommandRole = message.member.roles.cache.find(r => this.config.SLASH_COMMAND_ROLES.includes(r.name))
+      if (hasSlashCommandRole) {
         // send the raw command, can be dangerous...
         command = message.cleanContent
       } else {
@@ -140,16 +152,20 @@ class Discord {
     if (this.config.DEBUG) console.log(`[DEBUG] Sending command "${command}" to the server`)
 
     if (command) {
-      await rcon.command(command).catch((e) => {
+      let response: string | undefined;
+      try {
+        response = await rcon.command(command)
+      } catch (e) {
         console.log('[ERROR] Could not send command!')
         if (this.config.DEBUG) console.error(e)
-      }).then((str) => {
-        if (str === 'Unknown command. Try /help for a list of commands') {
-            console.error('[ERROR] Could not send command! (Unknown command)')
-            console.error('if this was a chat message, please look into MINECRAFT_TELLRAW_DOESNT_EXIST!')
-            console.error('command: ' + command)
+      }
+
+      if (response?.startsWith('Unknown command') || response?.startsWith('Unknown or incomplete command')) {
+        console.log('[ERROR] Could not send command! (Unknown command)')
+        if (command.startsWith('/tellraw')) {
+          console.log('Your Minecraft version may not support tellraw, please look into MINECRAFT_TELLRAW_DOESNT_EXIST!')
         }
-      })
+      }
     }
     rcon.close()
   }
@@ -167,16 +183,14 @@ class Discord {
       variables[v] = JSON.stringify(variables[v]).slice(1,-1)
     }
     
-    if (this.config.MINECRAFT_TELLRAW_DOESNT_EXIST)
-    {
-        return this.config.MINECRAFT_TELLRAW_DOESNT_EXIST_SAY_TEMPLATE
-                .replace(/%username%/g, variables.username)
-                .replace(/%nickname%/g, variables.nickname)
-                .replace(/%discriminator%/g, variables.discriminator)
-                .replace(/%message%/g, variables.text)
+    if (this.config.MINECRAFT_TELLRAW_DOESNT_EXIST) {
+      return this.config.MINECRAFT_TELLRAW_DOESNT_EXIST_SAY_TEMPLATE
+        .replace(/%username%/g, variables.username)
+        .replace(/%nickname%/g, variables.nickname)
+        .replace(/%discriminator%/g, variables.discriminator)
+        .replace(/%message%/g, variables.text)
     }
 
-
     return this.config.MINECRAFT_TELLRAW_TEMPLATE
       .replace(/%username%/g, variables.username)
       .replace(/%nickname%/g, variables.nickname)
@@ -184,7 +198,7 @@ class Discord {
       .replace(/%message%/g, variables.text)
   }
 
-  private replaceDiscordMentions(message: string): string {
+  private async replaceDiscordMentions(message: string): Promise<string> {
     const possibleMentions = message.match(/@[^#\s]*[#]?[0-9]{4}/gim)
     if (possibleMentions) {
       for (let mention of possibleMentions) {
@@ -192,9 +206,18 @@ class Discord {
         let username = mentionParts[0].replace('@', '')
         if (mentionParts.length > 1) {
           if (this.config.ALLOW_USER_MENTIONS) {
-            const user = this.client.users.find(user => user.username === username && user.discriminator === mentionParts[1])
+            let user = this.mentionCache.get(mention)
+            if (!user) {
+              // try fetching members by username to update cache
+              await this.channel!.guild.members.fetch({ query: username })
+              user = this.client.users.cache.find(user => user.username === username && user.discriminator === mentionParts[1])
+            }
+
             if (user) {
               message = message.replace(mention, '<@' + user.id + '>')
+              if (!this.mentionCache.has(mention)) this.mentionCache.set(mention, user)
+            } else {
+              console.log(`[ERROR] Could not find user by mention: "${mention}"`)
             }
           }
         }
@@ -220,6 +243,7 @@ class Discord {
       const response = await (await axios.get('https://api.mojang.com/users/profiles/minecraft/' + username)).data
       const uuid = response.id
       this.uuidCache.set(username, uuid)
+      if (this.config.DEBUG) console.log(`[DEBUG] Fetched UUID ${uuid} for username "${username}"`)
       return uuid
     } catch (e) {
       console.log(`[ERROR] Could not fetch uuid for ${username}, falling back to Steve for the skin`)
@@ -233,8 +257,6 @@ class Discord {
   }
 
   private async makeDiscordWebhook (username: string, message: string) {
-    message = this.replaceDiscordMentions(message)
-
     const defaultHead = this.getHeadUrl(this.config.DEFAULT_PLAYER_HEAD || 'c06f89064c8a49119c29ea1dbd1aab82') // MHF_Steve
 
     let avatarURL
@@ -253,14 +275,14 @@ class Discord {
   }
 
   private makeDiscordMessage(username: string, message: string) {
-    message = this.replaceDiscordMentions(message)
-
     return this.config.DISCORD_MESSAGE_TEMPLATE
       .replace('%username%', username)
       .replace('%message%', message)
   }
 
   public async sendMessage (username: string, message: string) {
+    message = await this.replaceDiscordMentions(message)
+
     if (this.config.USE_WEBHOOKS) {
       const webhook = await this.makeDiscordWebhook(username, message)
       try {
diff --git a/src/MinecraftHandler.ts b/src/MinecraftHandler.ts
index 452784a..0882d19 100644
--- a/src/MinecraftHandler.ts
+++ b/src/MinecraftHandler.ts
@@ -58,7 +58,7 @@ class MinecraftHandler {
 
     if (logLine.startsWith('<')) {
       if (this.config.DEBUG){
-        console.log('[DEBUG]: A player sent a chat message')
+        console.log('[DEBUG] A player sent a chat message')
       }
 
       const re = new RegExp(this.config.REGEX_MATCH_CHAT_MC)
@@ -84,18 +84,18 @@ class MinecraftHandler {
     ) {
       // handle disconnection etc.
       if (this.config.DEBUG){
-        console.log(`[DEBUG]: A player's connection status changed`)
+        console.log(`[DEBUG] A player's connection status changed`)
       }
 
       return { username: serverUsername, message: logLine }
     } else if (this.config.SHOW_SERVER_STATUS && (logLine.includes('Starting minecraft server'))) {
         if (this.config.DEBUG) {
-          console.log('[DEBUG]: Server has started')
+          console.log('[DEBUG] Server has started')
         }
         return { username: serverUsername, message: 'Server is online' }
     } else if (this.config.SHOW_SERVER_STATUS && (logLine.includes('Stopping the server'))) {
         if (this.config.DEBUG) {
-          console.log('[DEBUG]: Server has stopped')
+          console.log('[DEBUG] Server has stopped')
         }
         return { username: serverUsername, message: 'Server is offline' }
     } else if (this.config.SHOW_PLAYER_ADVANCEMENT && logLine.includes('made the advancement')) {
diff --git a/src/Rcon.ts b/src/Rcon.ts
index 180ad38..fc3b989 100644
--- a/src/Rcon.ts
+++ b/src/Rcon.ts
@@ -55,7 +55,7 @@ class Rcon {
   }
 
   public async auth (password: string): Promise<void> {
-    if (this.authed) { throw new Error('Already authed') }
+    if (this.authed) throw new Error('Already authed')
 
     if (this.connected){
       try {
@@ -88,7 +88,7 @@ class Rcon {
     const id = this.nextId
     this.nextId++
 
-    if (!this.connected) { throw new Error('Cannot send package while not connected') }
+    if (!this.connected) throw new Error('Cannot send package while not connected')
 
     const length = 14 + payload.length
     const buff = Buffer.alloc(length)
diff --git a/src/Shulker.ts b/src/Shulker.ts
index 4c79c78..df635ad 100644
--- a/src/Shulker.ts
+++ b/src/Shulker.ts
@@ -1,3 +1,6 @@
+import path from 'path'
+import fs from 'fs'
+
 import DiscordClient from './Discord'
 import Handler, { LogLine } from './MinecraftHandler'
 
@@ -12,10 +15,16 @@ class Shulker {
   }
 
   loadConfig () {
-    const configFile = (process.argv.length > 2) ? process.argv[2] : '../config.json'
+    const configFile = process.argv.length > 2 ? process.argv[2] : './config.json'
+    if (!fs.existsSync(configFile)) {
+      console.log('[ERROR] Could not find config file!')
+      return false
+    }
     console.log('[INFO] Using configuration file:', configFile)
-    this.config = require(configFile)
-    if (!this.config) {
+
+    try {
+      this.config = JSON.parse(fs.readFileSync(configFile, 'utf8'))
+    } catch (e) {
       console.log('[ERROR] Could not load config file!')
       return false
     }