corrade-group-discord-bridge – Blame information for rev 1
?pathlinks?
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 | }); |