Skip to content

Instantly share code, notes, and snippets.

@copygirl
Created April 11, 2016 18:30
Show Gist options
  • Save copygirl/972b762815c2796bcc2bfa472f1a4c4f to your computer and use it in GitHub Desktop.
Save copygirl/972b762815c2796bcc2bfa472f1a4c4f to your computer and use it in GitHub Desktop.
"use strict";
let { Client } = require("discord.js");
let Service = require("./Service");
let { map, filter, any, join } = require("../utility");
let DiscordService = module.exports = class DiscordService extends Service {
constructor(id, { token, email, password }) {
super(id);
this.token = token;
this.email = email;
this.password = password;
this._discord = new Client();
this._users = new Map();
this._channels = new Map();
this._pendingMessages = [ ];
this._missingSilentMsgs = new Set();
this._silentMessageCount = 0;
this._discord.on("ready", () => {
this.emit("connected", this._getUser(this._discord.user, true));
// Create user and channel objects.
for (let user of this._discord.users)
this._getUser(user, true);
for (let channel of this._discord.channels)
this._getChannel(channel, true);
});
// User events.
this._discord.on("serverNewMember", (server, user) =>
this._getUser(user, true));
this._discord.on("serverMemberRemoved", (server, user) => {
if (this._discord.users.has("id", user.id)) return;
user = this._getUser(user);
this._users.delete(user._id);
user.emit("removed");
});
this._discord.on("presence", (oldUser, newUser) => {
let user = this._getUser(newUser);
if (oldUser.name != newUser.name) {
let resolves = user.resolveStrings;
user._name = newUser.name;
user.emit("renamed", oldUser.name, newUser.name, resolves);
}
});
// Channel events.
this._discord.on("channelCreated", (channel) =>
this._getChannel(channel, true));
this._discord.on("channelDeleted", (channel) => {
channel = this._getChannel(channel);
this._channels.delete(channel._id);
channel.emit("removed");
});
this._discord.on("channelUpdated", (oldChannel, newChannel) => {
let channel = this._getChannel(newChannel);
if (oldChannel.name != newChannel.name) {
let resolves = channel.resolveStrings;
channel._name = newChannel.name;
channel.emit("renamed", oldChannel.name, newChannel.name, resolves);
}
});
// TODO: Handle joining / leaving servers.
this._discord.on("message", this._message.bind(this));
this._discord.on("disconnected", () => {
this._users.clear();
this._channels.clear();
this.emit("disconnected", ((this.isConnected)
? "Disconnected" : "Unable to connect / login"));
});
}
_getUser(id, create = false) {
if (typeof id != "string") id = id.id;
let user = this._users.get(id);
if ((user == null) && create) {
this._users.set(id, (user = new DiscordService.User(this, id)));
this.emit("newUser", user);
}
return user;
}
_getChannel(id, create = false) {
if (typeof id != "string") id = id.id;
let channel = this._channels.get(id);
if ((channel == null) && create) {
this._channels.set(id, (channel = new DiscordService.Channel(this, id)));
this.emit("newChannel", channel);
}
return channel;
}
_message(message, silent = false) {
// Since _discord.on("message") pretty much always fires before the _discord.sendMessage() promise
// resolves, with an odd delay (probably due to the former going over websockets and the latter
// being the result of an API call), we don't know whether an incoming message is supposed to be
// silent or not until the promise does resolve (see DiscordService.Channel.sendSilent).
// To fix this, whenever there are silent messages that have been sent but not yet received, any
// regular message coming through will get added to a buffer. When a silent message is received,
// it is removed from the buffer to make sure it doesn't fire a message event. Once all silent
// messages have been received, all buffered messages will be "released".
if (silent) {
// If for some reason we didn't receive the message
// yet, it's may arrive afterwards - keep track of it.
if (this._pendingMessages.delete(message) == 0)
this._missingSilentMsgs.add(message.id);
this._reduceSilentCount();
return;
}
// We found one of those missing silent messages! Let's make sure
// it doesn't fire a message event just because it's a little late.
else if (this._missingSilentMsgs.delete(message.id))
return;
// If there's silent messages that we're currently
// waiting for, add incoming messages to a buffer.
else if (this._silentMessageCount > 0) {
this._pendingMessages.push(message);
return;
}
let time = new Date(message.timestamp);
let sender = this._getUser(message.author, true);
let target = this._getChannel(message.channel, true);
let parts = [ message.content ];
// TODO: Split up message into text, mentions, newlines and attachments.
// TODO: Detect action-style messages.
// TODO: Parse markdown formatting of messages.
message = new Service.Message(this, time, target, sender, parts);
this.emit("message", message);
}
_reduceSilentCount() {
this._silentMessageCount--;
if (this._silentMessageCount > 0) return;
// Once all silent messages were received and
// processed, deal with all the backed-up messages.
for (let message of this._pendingMessages)
this._message(message);
this._pendingMessages.clear();
}
get users() { return this._users.values(); }
get channels() { return this._channels.values(); }
connect() {
return ((this.token != null)
? this._discord.loginWithToken(this.token, this.email, this.password)
: this._discord.login(this.email, this.password));
}
disconnect(reason) {
return this._discord.logout();
}
type(resolveStr) {
// TODO: Allow resolving @username for mentions.
let result = /^(?:(?:(\d{18})\/)?#([^\d].+)|#(\d{18})|@(\d{18}))$/.exec(resolveStr);
return ((result != null) ? ((result[4] != null) ? "user" : "channel")
: null);
}
toString() { return `Discord (${ this.email })`; }
};
DiscordService.User = class DiscordUser extends Service.User {
constructor(service, id) {
super(service);
this._id = id;
this._name = this._discordUser.username;
}
get _discordUser() { return this.service._discord.users.get("id", this._id); }
get _discordMention() { return `<@${ this._id }>`; }
get name() { return this._name; }
get mentionStr() { return `@${ this._name }`; }
get resolveStrings() { return [ `@${ this._id }` ]; }
};
DiscordService.Channel = class DiscordChannel extends Service.Channel {
constructor(service, id) {
super(service);
this._id = id;
this._name = this._discordChannel.name;
}
get _discordChannel() { return this.service._discord.channels.get("id", this._id); }
get _discordMention() { return `<#${ this._id }>`; }
get name() { return `#${ this._name }`; }
get resolveStrings() { return [ `#${ this._id }`, `#${ this._name }`,
`${ this._discordChannel.server.id }/#${ this._name }` ]; }
get topic() { return (this._discordChannel.topic || null); }
send(...parts) { this._send(parts, false); }
sendSilent(...parts) { this._send(parts, true); }
_send(parts, silent) {
let isAction = false;
let content = join(map(parts, (part) =>
// Get dem newlines in here!
(part == Service.NewLine) ? "\n" :
// If there's an Action identifier, format the message afterwards.
(part == Service.Action) ? (isAction = true, "") :
// If a discord user/channel is being mentioned, transform it to a proper mention.
((part instanceof Service.Mention) && part.mentionable._discordMention) ? part._discordMention :
// Otherwise just toString the part.
part
), "");
if (isAction) content = `_${ content }_`;
let promise = this.service._discord.sendMessage(this._discordChannel, content);
if (silent) {
this.service._silentMessageCount++;
promise.then((message) => this.service._message(message, true),
(error) => this.service._reduceSilentMessage());
// TODO: Do something with failed messages?
}
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment