corrade-group-discord-bridge – Blame information for rev 1

Subversion Repositories:
Rev:
Rev Author Line No. Line
1 office 1 #!/usr/bin/env node
2 ///////////////////////////////////////////////////////////////////////////
3 // Copyright (C) 2019 Wizardry and Steamworks - License: CC BY 2.0 //
4 ///////////////////////////////////////////////////////////////////////////
5  
6 const mqtt = require('mqtt')
7 const YAML = require('yamljs')
8 const { createLogger, format, transports } = require('winston')
9 const Discord = require('discord.js')
10 const discordClient = new Discord.Client()
11 const qs = require('qs')
12 const path = require('path')
13 const fs = require('fs')
14 const uuid = require('uuid')
15  
16 // Stores messages sent by Corrade.
17 const corradeMessages = {}
18  
19 // Stores the Discord channel identifier.
20 var discordChannelID = -1
21  
22 // Regex that determines whether a SecondLife group message is a message
23 // that has been relayed by Corrade to the SecondLife group.
24 const groupDiscordRegex = new RegExp(/^.+?#[0-9]+? \[Discord\]:.+?$/gm)
25  
26 // Load configuration file.
27 const config = YAML.load('config.yml')
28  
29 // Set up logger.
30 const logger = createLogger({
31 format: format.combine(
32 format.splat(),
33 format.simple()
34 ),
35 transports: [
36 new transports.Console({
37 timestamp: true
38 }),
39 new transports.File(
40 {
41 timestamp: true,
42 filename: path.join(path.dirname(fs.realpathSync(__filename)), "log/corrade-group-discord-bridge.log")
43 }
44 )
45 ]
46 })
47  
48 // Subscribe to Corrade MQTT.
49 const mqttClient = mqtt.connect(config.corrade.mqtt)
50  
51 mqttClient.on('reconnect', () => {
52 logger.info('Reconnecting to Corrade MQTT server...')
53 })
54  
55 mqttClient.on('connect', () => {
56 logger.info('Connected to Corrade MQTT server.')
57 // Subscribe to group message notifications with group name and password.
58 mqttClient.subscribe(`${config.corrade.group.name}/${config.corrade.group.password}/group`, (error) => {
59 if (error) {
60 logger.info('Error subscribing to Corrade MQTT group messages.')
61 return
62 }
63  
64 logger.info('Subscribed to Corrade MQTT group messages.')
65 })
66 })
67  
68 mqttClient.on('close', () => {
69 logger.error('Disconnected from Corrade MQTT server...')
70 })
71  
72 mqttClient.on('error', (error) => {
73 logger.error(`Error found while connecting to Corrade MQTT server ${error}`)
74 })
75  
76 mqttClient.on('message', (topic, message) => {
77 // If the Discord channel is not yet known then do not process the notification.
78 if (discordChannelID === -1) {
79 logger.error('Message received from Corrade but Discord channel could not be retrieved, please check your configuration')
80 return
81 }
82  
83 // Make an object out of the notification.
84 let mqttMessage = qs.parse(message.toString())
85  
86 // Check that the "tell" command was successful and warn otherwise.
87 if (typeof mqttMessage.command !== 'undefined' && mqttMessage.command !== 'tell') {
88 // Do not process commands without a success status.
89 if (typeof mqttMessage.success === 'undefined') {
90 return
91 }
92 // Check for the message id sent as afterburn.
93 if (typeof mqttMessage.id === 'undefined' || typeof corradeMessages[mqttMessage.id] === 'undefined') {
94 logger.warn(`Found message that does not belong to us: ${JSON.stringify(mqttMessage)}`)
95 return
96 }
97 switch (mqttMessage.success) {
98 case 'True':
99 logger.info(`Successfully sent message with ID: ${mqttMessage.id}`)
100 break
101 case 'False':
102 logger.warn(`Tell command failed: ${JSON.stringify(mqttMessage)}`)
103 break
104 }
105 // Delete the message.
106 delete corradeMessages[mqttMessage.id]
107 return
108 }
109  
110 // Check the notification parameters for sanity.
111 if (typeof mqttMessage.type === 'undefined' || mqttMessage.type !== 'group') {
112 logger.info('Skipping message without notification type...')
113 return
114 }
115  
116 let notification = mqttMessage
117  
118 if (notification.group.toUpperCase() !== config.corrade.group.name.toUpperCase()) {
119 logger.info('Ignoring message for group not defined within the configuration...')
120 return
121 }
122  
123 // Ignore system messages; for example, when no group member is online in the group.
124 if (notification.firstname === 'Second' && notification.lastname === 'Life') {
125 logger.info('Ignoring system message...')
126 return
127 }
128  
129 // If this is a message relayed by Corrade to Discord, then ignore
130 // the message to prevent echoing the message multiple times.
131 if (notification.message.match(groupDiscordRegex)) {
132 logger.info('Ignoring relayed message...')
133 return
134 }
135  
136 // Send the message to the channel.
137 discordClient
138 .channels
139 .cache
140 .get(discordChannelID)
141 .send(`${notification.firstname} ${notification.lastname} [SL]: ${notification.message}`)
142 })
143  
144 discordClient.on('message', (message) => {
145 // For Discord, ignore messages from bots (including self).
146 if (message.author.bot) {
147 logger.info(`Not relaying Discord message from Discord bot ${JSON.stringify(message.author.username)}...`)
148 return
149 }
150  
151 let messageContent = message.content
152 if (message.attachments.length !== 0) {
153 message.attachments.forEach(attachment => messageContent = `${messageContent} ${attachment.url}`)
154 }
155  
156 // Ignore empty messages.
157 if (messageContent.length == 0) {
158 logger.info('Not relaying empty Discord message...')
159 return
160 }
161  
162 // Ignore messages that are not from the configured channel.
163 if (message.channel.id !== discordChannelID) {
164 logger.info(`Not relaying Discord message from Discord channel #${JSON.stringify(message.channel.name)} other than the configured channel...`)
165 return
166 }
167  
168 // Check if this is the intended server.
169 if (message.channel.guild.name !== config.discord.server) {
170 logger.info(`Not relaying Discord message from different server ${JSON.stringify(message.channel.guild.name)} other than the configured server...`)
171 return
172 }
173  
174 // Discard anything but text messages.
175 if (message.channel.type !== 'text') {
176 logger.info(`Not relaying Discord message of type ${JSON.stringify(message.channel.type)} that is not text...`)
177 return
178 }
179  
180 // If the message contains the special prefix then pass the message
181 // as it is without prefixing it with the Discord username.
182 let reply = `${message.author.username}#${message.author.discriminator} [Discord]: ${messageContent}`
183  
184 // Generate an unique identifier to be passed via afterburn to check whether messages have been sent.
185 let id = uuid.v4()
186  
187 // Build the command.
188 let payload = {
189 'command': 'tell',
190 'group': config.corrade.group.name,
191 'password': config.corrade.group.password,
192 'entity': 'group',
193 'target': config.corrade.group.uuid,
194 'message': reply,
195 'id': id
196 }
197  
198 // Build the tell command.
199 const corradeCommand = qs.stringify(payload)
200  
201 // Store the message and check success status later.
202 corradeMessages[id] = payload
203  
204 // Send the command to the Corrade MQTT broker.
205 mqttClient.publish(`${config.corrade.group.name}/${config.corrade.group.password}/group`, corradeCommand)
206 })
207  
208 // Retrieve channel ID when Discord is ready.
209 discordClient.on('ready', () => {
210 logger.info('Connected to Discord.')
211  
212 const channel = discordClient
213 .channels
214 .cache
215 .find(channel => channel.name === config.discord.channel &&
216 channel.guild.name === config.discord.server)
217  
218 logger.info('Querying channels...')
219 discordClient.channels.cache.forEach(channel => {
220 logger.info(`Found channel ${channel.name}`)
221 })
222  
223 if (typeof channel === 'undefined' || channel == null) {
224 logger.error('The channel could not be found on discord.')
225 return
226 }
227  
228 logger.info('Discord channel ID retrieved successfully.')
229 discordChannelID = channel.id
230 })
231  
232 discordClient.on('error', (error) => {
233 logger.error(`Error occurred whilst connecting to Discord: ${error}`)
234 })
235  
236 discordClient.on('reconnecting', () => {
237 logger.error('Reconnecting to Discord...')
238 })
239  
240 // Login to discord.
241 discordClient.login(config.discord.botKey)
242 .then(() => {
243 logger.info('Logged-in to Discord.')
244 })
245 .catch((error) => {
246 logger.error('Failed to login to Discord.')
247 });