This repository has been archived by the owner on Jul 5, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
/
index.js
310 lines (270 loc) · 9.97 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
const IRC = require('irc-framework')
const colors = require('irc-colors')
const Eris = require('eris')
const BiMap = require('bimap')
const moment = require('moment')
const config = require('./config')
const dc = new Eris.Client(config.eris.token)
const irc = new IRC.Client()
let guild // The guild the bot is connected to
let channelsCategoryId // The ID of the category representing IRC channels
let usersCategoryId // The ID of the category representing IRC private messages
let noticesChannelId // The ID of the channel for displaying notices/wallops
let channelMap = new BiMap() // The bidirectional map of IRC channel names and Discord channel IDs
dc.on('error', console.error)
dc.on('ready', () => {
console.log('Discord client connected.')
// We need the guild that we're going to be working with.
guild = dc.guilds.get(config.guildId)
if (!guild) {
console.log('Guild not found. Is the ID valid?')
process.exit(1)
}
// Once we have the guild, find the channel for notices and wallops.
const noticesChannel = guild.channels.find(c => c.name === 'notices' && c.type === 0 && !c.parentID)
if (!noticesChannel) {
// TODO: Create these.
console.log('Guild has no notices channel. Create a text channel called #notices under no category.')
process.exit(1)
}
noticesChannelId = noticesChannel.id
// The guild also needs to have categories set up to differentiate between
// channels and users.
const channelsCategory = guild.channels.find(c => c.name === 'Channels' && c.type === 4)
const usersCategory = guild.channels.find(c => c.name === 'Private Messages' && c.type == 4)
if (!channelsCategory || !usersCategory) {
// TODO: Create these.
console.log('Guild does not have the right categories. Create categories called "Channels" and "Private Messages".')
process.exit(1)
}
channelsCategoryId = channelsCategory.id
usersCategoryId = usersCategory.id
// Now that we know the guild is all set, we can connect to IRC.
irc.connect({
host: config.irc.host,
port: config.irc.port,
nick: config.irc.nick
})
})
irc.on('registered', () => {
console.log('IRC client connected.')
// If we have a nickserv password, use it immediately.
if (config.irc.nickservPass) {
irc.say('NickServ', `identify ${config.irc.nickservPass}`)
}
// Note: At this point if the user is registered they may not have the proper
// channel modes and stuff. If stuff happens that relies on that, we
// shouldn't continue until we hear back from NickServ.
// Set up the channel map - associate each IRC channel with a Discord channel.
for (let channel of config.channels) {
console.log('Handling channel:', channel)
const {name, pass} = (typeof channel === 'string') ? {name: channel, pass: ''} : channel
// IRC channels must have a name that starts with '#'.
if (!name || !name.startsWith('#')) {
console.log('Skipping channel with invalid or missing name', name)
continue
}
// Join the channel on IRC.
const ircChannel = irc.channel(name)
ircChannel.join()
// Get (or create) the Discord counterpart for this channel.
// First, get a valid name for the Discord channel.
const discordName = name
// The # prefix is an IRC thing and isn't present in Discord's names.
.replace(/^#/, '')
// Discord text channel names are all lowercase.
.toLowerCase()
// They're also restricted to alphanumerics, dashes, and underscores.
.replace(/[^a-z0-9-_]/g, '-')
// First character must be alphanumeric, not a dash or space.
.replace(/^[-_]/, '')
// With this name, see if the channel exists.
let discordChannel = guild.channels.find(c => c.name === discordName && c.parentID === channelsCategoryId)
if (discordChannel) {
channelMap.push(name, discordChannel.id)
} else {
// TODO: Refactor this code so we can create channels here. For now,
// just yell about it.
console.log(`No Discord channel found for IRC channel ${name} (was expecting #${discordName}). If it's there, is it in the right category?`)
}
}
// The channel map is now complete. We're ready to start bridging messages.
console.log(channelMap.kv)
console.log()
})
// Handle messages from Discord and send them to the correct IRC channel/user.
dc.on('messageCreate', msg => {
// Ignore if this is a bot message.
if (msg.author.id === dc.user.id) return
// In here we do commands and stuff
if (!msg.channel.guild) return handleCommand(msg)
// Also ignore this if it's not in a channel we know.
if (!channelMap.val(msg.channel.id)) return
// Get the right IRC channel to send this message to.
const ircName = channelMap.val(msg.channel.id)
// Replace Discord mentions with plaintext.
const guild = dc.guilds.get(config.guildId)
const text = msg.content
.replace(/<@(\d+)>/g, (match, id) => {
const member = guild.members.get(id)
return `@${member.username}`
})
.replace(/<#(\d+)>/g, (match, id) => {
const channel = guild.channels.get(id)
return `#${channel.name}`
})
// Send the modified message to the IRC channel.
irc.say(ircName, text)
// If the configured to, delete the Discord message and replace it with one in
// the proper format.
// NOTE: This uses a timeout because otherwise the client was freaking out. We
// also create a fake message object here to keep the format consistent;
// however, this object is not complete, and only includes properties
// that change its text representation.
if (config.replaceMessages) {
msg.channel.deleteMessage(msg.id).then(() => setTimeout(() => {
const message = {
nick: irc.user.nick,
message: msg.content
}
handleIrcThing('privmsg', message, msg.channel.id)
}), 50)
}
})
// Handle messages from IRC and send them to the correct Discord channel.
// TODO: Honestly, just refactor these to work with `irc.on('message', ...)`.
irc.on('notice', e => {
handleIrcThing('notice', e, noticesChannelId)
})
irc.on('wallops', e => {
handleIrcThing('wallops', e, noticesChannelId)
})
irc.on('privmsg', e => {
let discordChannelId
if (e.target.startsWith('#')) {
discordChannelId = channelMap.key(e.target)
} else {
// TODO
return
}
handleIrcThing('privmsg', e, discordChannelId)
})
irc.on('action', e => {
let discordChannelId
if (e.target.startsWith('#')) {
discordChannelId = channelMap.key(e.target)
} else {
// TODO
return
}
handleIrcThing('action', e, discordChannelId)
})
irc.on('nick', e => {
// TODO: Filter this so it only includes channels the user was actually in
const discordChannelId = Object.keys(channelMap.vk)
handleIrcThing ('nick', e, discordChannelId)
})
irc.on('join', e => {
const discordChannelId = channelMap.key(e.channel)
handleIrcThing('join', e, discordChannelId)
})
irc.on('part', e => {
const discordChannelId = channelMap.key(e.channel)
handleIrcThing('part', e, discordChannelId)
})
irc.on('quit', e => {
// TODO: Filter this so it only includes channels the user was actually in
const discordChannelIds = Object.keys(channelMap.vk)
handleIrcThing ('quit', e, discordChannelIds)
})
irc.on('kick', e => {
const discordChannelIds = channelMap.key(e.channel)
handleIrcThing('kick', e, discordChannelIds)
})
function formatNick (nick) {
nick = nick.replace(/`/g, '$&\u200B') // Add zero-width space after backtick to prevent escaping markdown
const isMe = nick === irc.user.nick // If this is my nick, underline it
return `${isMe ? '__' : ''}**\`\`${nick}\`\`**${isMe ? '__' : ''}`
}
function formatMessage (msg) {
// Escape anything that could possibly be a markdown trigger, woo overkill
return msg.replace(/[!@#$%^&*()`\-=~_+[\]\\{}|;'",]/g, '\\$&')
}
function handleIrcThing (type, e, discordChannelIds) {
let message = ''
// Format the message acording to its type
switch (type) {
case 'privmsg':
message = `${formatNick(e.nick)} ${formatMessage(e.message)}`
break
case 'action':
message = `\`*\` ${formatNick(e.nick)} ${formatMessage(e.message)}`
break
case 'wallops':
message = `\`[wallops]\` ${e.from_server ? '\`[server]\`' : `${formatNick(e.nick)}`} ${formatMessage(e.message)}`
break
case 'notice':
message = `\`[notice]\` ${e.from_server ? '\`[server]\`' : `${formatNick(e.nick)}`} ${e.message}`
break
case 'nick':
message = `\\↔ ${formatNick(e.nick)} is now ${formatNick(e.new_nick)}`
break
case 'away':
message = `⇠ ${formatNick(e.nick)} went away${e.message ? ` (${e.message})` : ''}`
break
case 'back':
message = `⇢ ${formatNick(e.nick)} is back${e.message ? ` (${e.message})` : ''}`
break
case 'join':
message = `→ ${formatNick(e.nick)} has joined`
break
case 'part':
message = `← ${formatNick(e.nick)} has left${e.message ? ` (${e.message})` : ''}`
break
case 'quit':
message = `← ${formatNick(e.nick)} has quit${e.message ? ` (${e.message})` : ''}`
break
case 'kick':
message = `← ${formatNick(e.kicked)} has left (Kicked by ${formatNick(e.nick)}${e.reason ? `: ${e.reason}` : ''})`
break
}
// Strip IRC color codes from the message
message = colors.stripColorsAndStyle(message)
// If we were mentioned in a privmsg or action, ping us.
if (['privmsg', 'action'].includes(type)) {
if (e.message.toLowerCase().indexOf(irc.user.nick.toLowerCase()) >= 0) {
// TODO: Dynamically use the ID of the bot's owner for this
message += ' (<@122902150291390469>)'
}
}
// Since this could be an array or not, convert it to an array and loop.
if (!Array.isArray(discordChannelIds)) {
discordChannelIds = [discordChannelIds]
}
discordChannelIds.forEach(id => {
dc.createMessage(id, message).then(msg => {
// Nothing else to do
}, err => {
console.log(err)
})
})
}
function handleCommand(msg) {
console.log('== eval ==')
if (msg.content.startsWith('```') && msg.content.endsWith('```')) {
if (msg.content.startsWith('```js')) {
msg.content = msg.content.substring(5, msg.content.length - 3)
} else {
msg.content = msg.content.substring(3, msg.content.length - 3)
}
let things
try {
things = new String(eval(msg.content)).toString()
} catch (e) {
things = e.toString()
}
console.log(things)
msg.channel.createMessage(things)
}
}
dc.connect()