Pushing changes

This commit is contained in:
2017-03-23 23:52:08 -05:00
parent 6075860b82
commit ac667ec74f
1465 changed files with 345149 additions and 3 deletions

View File

@ -0,0 +1,12 @@
"use strict";
class BaseArrayCollection extends Array {
// avoid constructor, calling array mutation methods will call it (ES2015)
static create() {
const instance = new this();
this._constructor.apply(instance, arguments);
return instance;
}
}
module.exports = BaseArrayCollection;

View File

@ -0,0 +1,19 @@
"use strict";
const BaseModel = require("../models/BaseModel");
class BaseCollection extends Map {
constructor() {
super();
}
mergeOrSet(key, value) {
const old = this.get(key);
let merged = value;
if (old && old instanceof BaseModel) {
merged = old.merge(value);
}
this.set(key, merged);
}
}
module.exports = BaseCollection;

View File

@ -0,0 +1,97 @@
"use strict";
const Constants = require("../Constants");
const Events = Constants.Events;
const Utils = require("../core/Utils");
const BaseCollection = require("./BaseCollection");
const Call = require("../models/Call");
function emitRing(gw, channelId) {
const channel = this._discordie.DirectMessageChannels.get(channelId);
if (!channel) return;
this._discordie.Dispatcher.emit(Events.CALL_RING, {
socket: gw,
channel: channel
});
}
function checkRing(gw, prev, next) {
const channelId = next.channel_id;
const userId = this._discordie._user && this._discordie._user.id;
if (!channelId || !userId) return;
if (!next || !next.ringing) return;
const hasPrev = prev ? prev.ringing.indexOf(userId) >= 0 : false;
const hasNext = next.ringing.indexOf(userId) >= 0;
if (!hasPrev && hasNext) emitRing.call(this, gw, channelId);
}
function handleConnectionOpen(data) {
this.clear();
return true;
}
function handleCallCreate(call, e) {
this.set(call.channel_id, new Call(call));
checkRing.call(this, e.socket, null, call);
return true;
}
function handleCallUpdate(call, e) {
const prev = this.get(call.channel_id);
this.mergeOrSet(call.channel_id, new Call(call));
checkRing.call(this, e.socket, prev, call);
return true;
}
function handleCallDelete(call) {
const _call = this.get(call.channel_id);
if (!_call) return true;
if (call.unavailable === true) {
this.mergeOrSet(call.channel_id, {unavailable: true});
} else {
this.delete(call.channel_id);
}
return true;
}
class CallCollection extends BaseCollection {
constructor(discordie, gateway) {
super();
if (typeof gateway !== "function")
throw new Error("Gateway parameter must be a function");
discordie.Dispatcher.on(Events.GATEWAY_DISPATCH, e => {
if (e.socket != gateway()) return;
Utils.bindGatewayEventHandlers(this, e, {
READY: handleConnectionOpen,
CALL_CREATE: handleCallCreate,
CALL_UPDATE: handleCallUpdate,
CALL_DELETE: handleCallDelete
});
});
this._discordie = discordie;
Utils.privatify(this);
}
isActive(channelId, messageId) {
const call = this.get(channelId);
if (messageId) {
return call && !call.unavailable && call.message_id == messageId;
}
return call && !call.unavailable;
}
isUnavailable(channelId) {
const call = this.get(channelId);
return call && call.unavailable;
}
}
module.exports = CallCollection;

View File

@ -0,0 +1,193 @@
"use strict";
const Constants = require("../Constants");
const Events = Constants.Events;
const ChannelTypes = Constants.ChannelTypes;
const Utils = require("../core/Utils");
const BaseCollection = require("./BaseCollection");
const Channel = require("../models/Channel");
const PermissionOverwrite = require("../models/PermissionOverwrite");
const IPermissions = require("../interfaces/IPermissions");
function convertOverwrites(channel) {
const overwrites = channel.permission_overwrites || [];
// note: @everyone overwrite does not exist by default
// this will add one locally
if (channel.guild_id) {
const everyone = overwrites.find(o => o.id == channel.guild_id);
if (!everyone) {
overwrites.push({
id: channel.guild_id,
type: "role",
allow: IPermissions.NONE,
deny: IPermissions.NONE
});
}
}
return overwrites.map(o => new PermissionOverwrite(o));
}
function createChannel(channel) {
const channelRecipients =
Array.isArray(channel.recipients) ? channel.recipients.map(r => r.id) : [];
return new Channel({
id: channel.id,
type: channel.type || ChannelTypes.GUILD_TEXT,
name: channel.name || "",
topic: channel.topic || "",
position: channel.position || 0,
recipients: new Set(channelRecipients),
guild_id: channel.guild_id || null,
permission_overwrites: convertOverwrites(channel),
bitrate: channel.bitrate || Constants.BITRATE_DEFAULT,
user_limit: channel.user_limit || 0,
owner_id: channel.owner_id || null,
icon: channel.icon || null
});
}
function handleConnectionOpen(data) {
this.clear();
data.guilds.forEach(guild => handleGuildCreate.call(this, guild));
data.private_channels.forEach(channel => {
this.set(channel.id, createChannel(channel));
});
return true;
}
function handleCreateOrUpdateChannel(channel, e) {
const prev = this.get(channel.id);
const next = createChannel(channel);
this.mergeOrSet(channel.id, next);
if (e.type === Events.CHANNEL_UPDATE) {
e.data._prev = prev;
e.data._next = next;
}
return true;
}
function handleChannelDelete(channel) {
this.delete(channel.id);
return true;
}
function handleGuildCreate(guild) {
if (!guild || guild.unavailable) return true;
guild.channels.forEach(channel => {
channel.guild_id = guild.id;
this.set(channel.id, createChannel(channel));
});
return true;
}
function handleGuildDelete(guild) {
this.forEach((channel, id) => {
if (channel.guild_id == guild.id)
this.delete(id);
});
return true;
}
function processRecipientAddOrRemove(data, handler) {
const user = data.user;
const channel = this.get(data.channel_id);
if (!channel) return;
if (!this._discordie._user || this._discordie._user.id == user.id) return;
handler(channel, user);
}
function handleRecipientAdd(data) {
const handler = (channel, user) => channel.recipients.add(user.id);
processRecipientAddOrRemove.call(this, data, handler);
return true;
}
function handleRecipientRemove(data) {
const handler = (channel, user) => channel.recipients.delete(user.id);
processRecipientAddOrRemove.call(this, data, handler);
return true;
}
class ChannelCollection extends BaseCollection {
constructor(discordie, gateway) {
super();
if (typeof gateway !== "function")
throw new Error("Gateway parameter must be a function");
discordie.Dispatcher.on(Events.GATEWAY_DISPATCH, e => {
if (e.socket != gateway()) return;
Utils.bindGatewayEventHandlers(this, e, {
READY: handleConnectionOpen,
GUILD_CREATE: handleGuildCreate,
GUILD_DELETE: handleGuildDelete,
CHANNEL_CREATE: handleCreateOrUpdateChannel,
CHANNEL_UPDATE: handleCreateOrUpdateChannel,
CHANNEL_DELETE: handleChannelDelete,
CHANNEL_RECIPIENT_ADD: handleRecipientAdd,
CHANNEL_RECIPIENT_REMOVE: handleRecipientRemove
});
});
this._discordie = discordie;
Utils.privatify(this);
}
*getPrivateChannelIterator() {
for (let channel of this.values()) {
if (this._isPrivate(channel))
yield channel;
}
}
*getGuildChannelIterator() {
for (let channel of this.values()) {
if (!this._isPrivate(channel))
yield channel;
}
}
getPrivateChannel(channelId) {
var channel = this.get(channelId);
if (!channel) return null;
return this._isPrivate(channel) ? channel : null;
}
getGuildChannel(channelId) {
var channel = this.get(channelId);
if (!channel) return null;
return !this._isPrivate(channel) ? channel : null;
}
isPrivate(channelId) {
const channel = this.get(channelId);
if (channel)
return this._isPrivate(channel);
return null;
}
_isPrivate(channel) {
const type = channel.type;
return (type === ChannelTypes.DM || type === ChannelTypes.GROUP_DM);
}
getChannelType(channelId) {
const channel = this.get(channelId);
if (channel) return channel.type;
return null;
}
update(channel) {
handleCreateOrUpdateChannel.call(this, channel, {});
}
updatePermissionOverwrite(channelId, overwrite) {
const channel = this.get(channelId);
if (!channel) return;
const newOverwrites = channel.permission_overwrites
.filter(o => o.id != overwrite.id && o.type != overwrite.type);
newOverwrites.push(new PermissionOverwrite(overwrite));
this.set(channelId, channel.merge({
permission_overwrites: newOverwrites
}));
}
}
module.exports = ChannelCollection;

View File

@ -0,0 +1,183 @@
"use strict";
const Constants = require("../Constants");
const Events = Constants.Events;
const Utils = require("../core/Utils");
const BaseCollection = require("./BaseCollection");
const MFALevels = Constants.MFALevels;
const VerificationLevel = Constants.VerificationLevel;
const UserNotificationSettings = Constants.UserNotificationSettings;
const Guild = require("../models/Guild");
const Role = require("../models/Role");
function convertRoles(roles) {
const map = new Map();
roles.forEach(role => {
map.set(role.id, new Role(role));
});
return map;
}
function createGuild(guild, old) {
return new Guild({
id: guild.id,
name: guild.name,
region: guild.region,
icon: guild.icon,
splash: guild.splash,
features: new Set(guild.features),
emojis: (guild.emojis != null ? guild.emojis : old.emojis) || [],
default_message_notifications:
guild.default_message_notifications ||
UserNotificationSettings.ALL_MESSAGES,
owner_id: guild.owner_id,
roles: convertRoles(guild.roles),
afk_channel_id: guild.afk_channel_id,
afk_timeout: guild.afk_timeout,
verification_level: guild.verification_level || VerificationLevel.NONE,
member_count:
(guild.member_count != null ? guild.member_count : old.member_count),
large: (guild.large != null ? guild.large : old.large) || false,
mfa_level: guild.mfa_level || MFALevels.NONE,
joined_at: guild.joined_at || old.joined_at
});
}
function handleConnectionOpen(data) {
this.clear();
data.guilds.forEach(guild => {
if (guild.unavailable) return;
this.set(guild.id, createGuild(guild, {}));
});
return true;
}
function handleCreateOrUpdateGuild(guild, e) {
if (!guild || guild.unavailable) return true;
const prev = this.get(guild.id) || {};
const next = createGuild(guild, prev);
this.mergeOrSet(guild.id, next);
if (e.type === Events.GUILD_UPDATE) {
e.data._prev = prev;
e.data._next = next;
}
return true;
}
function handleDeleteGuild(guild, e) {
const oldGuild = this.get(guild.id);
if (oldGuild) e.data._cached = oldGuild;
this.delete(guild.id);
return true;
}
function handleGuildSync(guild) {
let oldGuild = this.get(guild.id);
if (!oldGuild) return true;
this.mergeOrSet(guild.id, {
large: (guild.large != null ? guild.large : oldGuild.large)
});
return true;
}
function handleGuildRoleCreateOrUpdate(data, e) {
let guild = this.get(data.guild_id);
if (guild) {
const prev = guild.roles.get(data.role.id);
const next = new Role(data.role);
guild.roles.set(data.role.id, next);
if (e.type === Events.GUILD_ROLE_UPDATE) {
e.data._prev = prev;
e.data._next = next;
}
}
return true;
}
function handleGuildRoleDelete(data, e) {
let guild = this.get(data.guild_id);
if (guild) {
const oldRole = guild.roles.get(data.role_id);
if (oldRole) e.data._cached = oldRole;
guild.roles.delete(data.role_id);
}
return true;
}
function handleGuildMemberAdd(member) {
updateMemberCount.call(this, member.guild_id, +1);
return true;
}
function handleGuildMemberRemove(member) {
updateMemberCount.call(this, member.guild_id, -1);
return true;
}
function updateMemberCount(guildId, delta) {
let guild = this.get(guildId);
if (!guild) return true;
this.mergeOrSet(guildId, { member_count: guild.member_count + delta });
return true;
}
function handleGuildEmojisUpdate(data, e) {
if (!data || data.emojis == null) return true;
let guild = this.get(data.guild_id);
if (!guild) return true;
const prev = guild.emojis;
const next = data.emojis;
this.mergeOrSet(data.guild_id, { emojis: data.emojis });
if (e.type === Events.GUILD_EMOJIS_UPDATE) {
e.data._prev = prev;
e.data._next = next;
}
return true;
}
class GuildCollection extends BaseCollection {
constructor(discordie, gateway) {
super();
if (typeof gateway !== "function")
throw new Error("Gateway parameter must be a function");
discordie.Dispatcher.on(Events.GATEWAY_DISPATCH, e => {
if (e.socket != gateway()) return;
Utils.bindGatewayEventHandlers(this, e, {
READY: handleConnectionOpen,
GUILD_SYNC: handleGuildSync,
GUILD_CREATE: handleCreateOrUpdateGuild,
GUILD_UPDATE: handleCreateOrUpdateGuild,
GUILD_DELETE: handleDeleteGuild,
GUILD_ROLE_CREATE: handleGuildRoleCreateOrUpdate,
GUILD_ROLE_UPDATE: handleGuildRoleCreateOrUpdate,
GUILD_ROLE_DELETE: handleGuildRoleDelete,
GUILD_MEMBER_ADD: handleGuildMemberAdd,
GUILD_MEMBER_REMOVE: handleGuildMemberRemove,
GUILD_EMOJIS_UPDATE: handleGuildEmojisUpdate,
});
});
this._discordie = discordie;
Utils.privatify(this);
}
update(guild) {
handleCreateOrUpdateGuild.call(this, guild, {});
}
updateRole(guildId, role) {
const guild = this.get(guildId);
if (!guild || !guild.roles) return;
guild.roles.set(role.id, new Role(role));
}
}
module.exports = GuildCollection;

View File

@ -0,0 +1,239 @@
"use strict";
const Constants = require("../Constants");
const StatusTypes = Constants.StatusTypes;
const Events = Constants.Events;
const Utils = require("../core/Utils");
const BaseCollection = require("./BaseCollection");
const GuildMember = require("../models/GuildMember");
function createGuildMember(member) {
if (member.user) member.id = member.user.id;
return new GuildMember({
id: member.id,
guild_id: member.guild_id,
nick: member.nick || null,
roles: member.roles || [],
mute: member.mute || false,
deaf: member.deaf || false,
self_mute: member.self_mute || false,
self_deaf: member.self_deaf || false,
joined_at: member.joined_at
});
}
function handleConnectionOpen(data) {
this.clear();
data.guilds.forEach(guild => handleCreateGuild.call(this, guild));
return true;
}
function handleGuildMemberCreateOrUpdate(member, e) {
const memberCollection = this.get(member.guild_id);
if (!memberCollection) return true;
const prev = memberCollection.get(member.user.id);
const next = createGuildMember(member);
memberCollection.mergeOrSet(member.user.id, next);
if (e.type === Events.GUILD_MEMBER_UPDATE) {
e.data._prev = prev;
e.data._next = next;
}
return true;
}
function handleGuildMemberRemove(member, e) {
const memberCollection = this.get(member.guild_id);
if (!memberCollection) return true;
const oldMember = memberCollection.get(member.user.id);
if (oldMember) e.data._cached = oldMember;
memberCollection.delete(member.user.id);
return true;
}
function handleCreateGuild(guild) {
if (!guild || guild.unavailable) return true;
if (!this._discordie._guilds.get(guild.id)) return true; // GUILD_SYNC case
const memberCollection = new BaseCollection();
this.set(guild.id, memberCollection);
guild.members.forEach(member => {
member.guild_id = guild.id;
memberCollection.set(member.user.id, createGuildMember(member));
});
return true;
}
function handleDeleteGuild(guild) {
this.delete(guild.id);
return true;
}
function handleVoiceStateUpdate(data) {
const memberCollection = this.get(data.guild_id);
if (!memberCollection) return true;
const member = memberCollection.get(data.user_id);
if (!member) return true;
memberCollection.set(data.user_id,
member.merge({
mute: data.mute,
deaf: data.deaf,
self_mute: data.self_mute,
self_deaf: data.self_deaf
})
);
return true;
}
function handlePresenceUpdate(presence) {
if (!presence.user || !presence.user.id) return true;
// add members only for online users and if presence is not partial
if (presence.status == StatusTypes.OFFLINE || !presence.user.username)
return true;
const memberCollection = this.get(presence.guild_id);
if (!memberCollection) return true;
const cachedMember = memberCollection.get(presence.user.id);
if (!cachedMember) {
// note: presences only contain roles
memberCollection.set(presence.user.id, createGuildMember(presence));
}
return true;
}
function handleGuildMembersChunk(chunk) {
var guildId = chunk.guild_id;
if (!guildId || !chunk.members) return true;
var state = this._guildMemberChunks[guildId];
if (!state) return true;
var guild = this._discordie._guilds.get(guildId);
if (!guild) return true;
const memberCollection = this.get(guildId);
if (!memberCollection) return true;
chunk.members.forEach(member => {
member.guild_id = guildId;
memberCollection.mergeOrSet(member.user.id, createGuildMember(member))
});
if (memberCollection.size >= guild.member_count) state.resolve();
return true;
}
class GuildMemberCollection extends BaseCollection {
constructor(discordie, gateway) {
super();
if (typeof gateway !== "function")
throw new Error("Gateway parameter must be a function");
discordie.Dispatcher.on(Events.GATEWAY_DISPATCH, e => {
if (e.socket != gateway()) return;
Utils.bindGatewayEventHandlers(this, e, {
READY: handleConnectionOpen,
GUILD_SYNC: handleCreateGuild,
GUILD_CREATE: handleCreateGuild,
GUILD_DELETE: handleDeleteGuild,
GUILD_MEMBER_ADD: handleGuildMemberCreateOrUpdate,
GUILD_MEMBER_UPDATE: handleGuildMemberCreateOrUpdate,
GUILD_MEMBER_REMOVE: handleGuildMemberRemove,
VOICE_STATE_UPDATE: handleVoiceStateUpdate,
PRESENCE_UPDATE: handlePresenceUpdate,
GUILD_MEMBERS_CHUNK: handleGuildMembersChunk
});
});
discordie.Dispatcher.on(Events.GATEWAY_DISCONNECT, e => {
if (e.socket != gateway()) return;
for (var guildId in this._guildMemberChunks) {
var state = this._guildMemberChunks[guildId];
if (!state) continue;
state.reject(new Error("Gateway disconnect"));
}
this._guildMemberChunks = {};
});
this._guildMemberChunks = {};
this._discordie = discordie;
Utils.privatify(this);
}
fetchMembers(guilds){
const gateway = this._discordie.gatewaySocket;
if (!gateway || !gateway.connected)
return Promise.reject(new Error("No gateway socket (not connected)"));
const largeGuilds =
Array.from(this._discordie._guilds.values())
.filter(guild => {
if (!guild.large) return false;
var cachedMembers = this._discordie._members.get(guild.id);
if (!cachedMembers) return false;
return guild.member_count > cachedMembers.size;
})
.map(guild => guild.id);
// filter out only requested guilds (if specified) from large ones
// return a resolved promise if no large guilds in the list
let targetGuilds =
!guilds ?
largeGuilds :
largeGuilds.filter(guild => guilds.indexOf(guild) >= 0);
if (!targetGuilds.length) return Promise.resolve();
targetGuilds.forEach(guildId => {
if (this._guildMemberChunks[guildId]) return;
var state = {promise: null, resolve: null, reject: null, timer: null};
state.promise = new Promise((rs, rj) => {
const destroyState = () => {
if (!state.timer) return;
clearTimeout(state.timer);
state.timer = null;
delete this._guildMemberChunks[guildId];
};
state.resolve = result => { destroyState(); return rs(result); };
state.reject = reason => { destroyState(); return rj(reason); };
});
state.timer = setTimeout(() => {
if (!this._guildMemberChunks[guildId]) return;
state.reject(new Error(
"Guild member request timed out (" + guildId + ")"
));
delete this._guildMemberChunks[guildId];
}, 60000);
this._guildMemberChunks[guildId] = state;
});
gateway.requestGuildMembers(targetGuilds);
var targetPromises =
targetGuilds.map(guildId => this._guildMemberChunks[guildId].promise);
return Promise.all(targetPromises);
}
getMember(guildId, userId) {
const memberCollection = this.get(guildId);
if (!memberCollection) return null;
return memberCollection.get(userId);
}
}
module.exports = GuildMemberCollection;

View File

@ -0,0 +1,174 @@
"use strict";
const Constants = require("../Constants");
const Events = Constants.Events;
const Utils = require("../core/Utils");
const GUILD_SYNC_TIMEOUT = 3000;
function emitTaskFinished(gw) {
this._syncingGuilds.clear();
this._discordie.Dispatcher.emit(Events.READY_TASK_FINISHED, {
socket: gw,
name: this.constructor.name,
handler: this
});
}
function scheduleReadyTimeout(gw) {
this._syncingGuildsTimeout = setTimeout(() => {
this._syncingGuildsTimeout = null;
if (!this._discordie.connected) return;
setImmediate(() => emitTaskFinished.call(this, gw));
}, GUILD_SYNC_TIMEOUT);
}
function maybeReady(gw) {
if (this._syncingGuildsTimeout) {
clearTimeout(this._syncingGuildsTimeout);
}
if (this._syncingGuilds.size) {
scheduleReadyTimeout.call(this, gw);
} else {
setImmediate(() => emitTaskFinished.call(this, gw));
}
}
function handleExecuteReadyTask(data, e) {
clearAll.call(this);
if (this.isSupported(e.socket)) {
data.guilds.forEach(guild => {
this.add(guild.id);
this._syncingGuilds.add(guild.id);
});
commit.call(this);
}
maybeReady.call(this, e.socket);
return true;
}
function handleGuildCreate(guild, e) {
if (!this.isSupported(e.socket)) return true;
// ignore if became available
if (this._discordie.UnavailableGuilds.isGuildAvailable(guild)) return true;
this.sync(guild);
return true;
}
function handleGuildSync(guild, e) {
if (this._syncingGuilds.has(guild.id)) {
this._syncingGuilds.delete(guild.id);
maybeReady.call(this, e.socket);
}
return true;
}
function handleGuildDelete(guild, e) {
if (!this.isSupported(e.socket)) return true;
if (guild.unavailable) return true;
this.unsync(guild);
return true;
}
function commit() {
const gateway = this._gateway();
if (!gateway || !gateway.connected) return;
gateway.syncGuilds(Array.from(this));
}
function clearAll() {
this.clear();
this._syncingGuilds.clear();
}
class GuildSyncCollection extends Set {
constructor(discordie, gateway) {
super();
if (typeof gateway !== "function")
throw new Error("Gateway parameter must be a function");
discordie.Dispatcher.on(Events.GATEWAY_DISPATCH, e => {
if (e.socket != gateway()) return;
Utils.bindGatewayEventHandlers(this, e, {
GUILD_SYNC: handleGuildSync,
GUILD_CREATE: handleGuildCreate,
GUILD_DELETE: handleGuildDelete,
});
});
this._discordie = discordie;
this._gateway = gateway;
this._syncingGuilds = new Set();
this._syncingGuildsTimeout = null;
Utils.privatify(this);
}
_executeReadyTask(data, socket) {
handleExecuteReadyTask.call(this, data, {socket});
}
isSupported(socket) {
if (!socket) socket = this._gateway();
if (!socket) return false;
return socket.remoteGatewayVersion >= 5 && !this._discordie._user.bot;
}
sync(guild) {
if (!this.isSupported()) return false;
if (!guild) return false;
if (Array.isArray(guild)) {
const guilds = guild
.map(g => g.id || g.valueOf())
.filter(id => !this.has(id));
if (!guilds.length) return false;
guilds.forEach(id => this.add(id));
} else {
if (this.has(guild.id)) return false;
this.add(guild.id);
}
commit.call(this);
return true;
}
unsync(guild) {
if (!this.isSupported()) return false;
if (!guild) return false;
if (Array.isArray(guild)) {
const guilds = guild
.map(g => g.id || g.valueOf())
.filter(id => this.has(id));
if (!guilds.length) return false;
guilds.forEach(id => this.delete(id));
} else {
if (!this.delete(guild.id)) return false;
}
commit.call(this);
return true;
}
syncAll() {
const available = this._discordie.Guilds.map(g => g.valueOf());
const unavailable = this._discordie.UnavailableGuilds;
const all = available.concat(unavailable);
return this.sync(all);
}
unsyncAll() {
if (!this.isSupported()) return false;
if (!this.size) return false;
this.clear();
commit.call(this);
return true;
}
}
module.exports = GuildSyncCollection;

View File

@ -0,0 +1,393 @@
"use strict";
const Constants = require("../Constants");
const Events = Constants.Events;
const Utils = require("../core/Utils");
const BaseCollection = require("./BaseCollection");
const LimitedCache = require("../core/LimitedCache");
const Message = require("../models/Message");
function getOrCreateChannelCache(channelId) {
return (
this._messagesByChannel[channelId] =
this._messagesByChannel[channelId] ||
new LimitedCache(this._messageLimit)
);
}
function updatePinnedMessages(channelId, messageId, message) {
if (message && message.pinned && !message.deleted) {
var channelPinned =
this._pinnedMessagesByChannel[channelId] =
this._pinnedMessagesByChannel[channelId] || [];
// insert or replace
const idx = channelPinned.findIndex(msg => msg.id === messageId);
if (idx < 0) channelPinned.unshift(message);
else channelPinned[idx] = message;
} else {
var channelPinned = this._pinnedMessagesByChannel[channelId];
if (channelPinned) {
// delete if exists
const idx = channelPinned.findIndex(msg => msg.id === messageId);
if (idx >= 0) channelPinned.splice(idx, 1);
}
}
}
function updateMessages(channelId, messageId, msg) {
const messages = getOrCreateChannelCache.call(this, channelId);
if (msg.nonce && messages.has(msg.nonce)) {
messages.rename(msg.nonce, messageId);
}
var edited = true;
var message = null;
if (messages.has(messageId)) {
edited = !!msg.edited_timestamp;
messages.set(messageId, message = messages.get(messageId).merge(msg));
} else {
messages.set(messageId, message = new Message(msg));
}
if (edited) {
const edits = this._messageEdits[messageId] || [];
this._messageEdits[messageId] = edits;
var edit = message.merge({embeds: [], reactions: []});
edits.push(edit);
trimEdits.call(this, messageId);
}
}
function trimEdits(messageId) {
if (!messageId) {
for (var id in this._messageEdits)
if (id) trimEdits.call(this, id);
}
const edits = this._messageEdits[messageId];
if (!edits) return;
if (edits.length > this._editsLimit)
edits.splice(0, edits.length - this._editsLimit);
}
function handleMessageCreate(msg) {
msg.deleted = false;
updateMessages.call(this, msg.channel_id, msg.id, msg);
return true;
}
function handleMessageUpdate(msg) {
msg.deleted = false;
// update pinned cache only if pinned messages have been fetched
var channelPinned = this._pinnedMessagesByChannel[msg.channel_id];
if (channelPinned) {
updatePinnedMessages.call(this, msg.channel_id, msg.id, msg);
}
const channelCache = this._messagesByChannel[msg.channel_id];
if (!channelCache || !channelCache.has(msg.id)) return true;
updateMessages.call(this, msg.channel_id, msg.id, msg);
return true;
}
function handleMessageDelete(msg) {
msg.deleted = true;
updatePinnedMessages.call(this, msg.channel_id, msg.id, msg);
const channelCache = this._messagesByChannel[msg.channel_id];
if (!channelCache || !channelCache.has(msg.id)) return true;
updateMessages.call(this, msg.channel_id, msg.id, msg);
return true;
}
function handleMessageDeleteBulk(msg) {
const channelCache = this._messagesByChannel[msg.channel_id];
if (!channelCache) return true;
const update = { deleted: true };
msg.ids.forEach(id => {
updatePinnedMessages.call(this, msg.channel_id, id, update);
if (!channelCache.has(id)) return;
updateMessages.call(this, msg.channel_id, id, update);
});
return true;
}
function handleReaction(reaction, e) {
const channelCache = this._messagesByChannel[reaction.channel_id];
if (!channelCache) return true;
const localUser = this._discordie._user || {};
const me = (localUser.id == reaction.user_id);
var message = channelCache.get(reaction.message_id);
if (!message) return true;
const emoji = reaction.emoji;
const idx = message.reactions
.findIndex(r => r.emoji.id === emoji.id && r.emoji.name == emoji.name);
const old = message.reactions[idx];
if (e.type === Events.MESSAGE_REACTION_ADD) {
if (!message.reactions) {
message = message.merge({reactions: []});
channelCache.set(reaction.message_id, message);
}
if (old) {
old.count += 1;
if (me) old.me = true;
} else {
message.reactions.push({emoji, me, count: 1});
}
}
if (e.type === Events.MESSAGE_REACTION_REMOVE) {
if (old) {
old.count -= 1;
if (me) old.me = false;
if (old.count <= 0) message.reactions.splice(idx, 1);
}
}
return true;
}
function handleReactionRemoveAll(reaction, e) {
const channelCache = this._messagesByChannel[reaction.channel_id];
if (!channelCache) return true;
const message = channelCache.get(reaction.message_id);
if (!message) return true;
if (!message.reactions || !message.reactions.length) return true;
e.data._cached = message.reactions;
channelCache.set(reaction.message_id, message.merge({reactions: []}));
}
function handleConnectionOpen(data) {
this.purgeAllCache();
}
function handleCleanup() {
for (let channelId in this._messagesByChannel) {
if (!this._messagesByChannel.hasOwnProperty(channelId)) continue;
if (this._discordie._channels.get(channelId)) continue;
var messageIds = this._messagesByChannel[channelId]._keys;
for (var i = 0, len = messageIds.length; i < len; i++)
delete this._messageEdits[messageIds[i]];
delete this._pinnedMessagesByChannel[channelId];
delete this._messagesByChannel[channelId];
delete this._hasMoreByChannel[channelId];
}
}
function handleLoadedMoreMessages(e) {
var messagesLength = e.messages.length;
if (!messagesLength) return;
const channelId = e.messages[0].channel_id;
if (!e.before && !e.after) {
this._hasMoreByChannel[channelId] = (e.limit == messagesLength);
}
const messages = getOrCreateChannelCache.call(this, channelId);
const limit = messages.limit;
messages.setLimit(limit + messagesLength);
var i = messagesLength;
while (i--) {
var msg = e.messages[i];
updateMessages.call(this, msg.channel_id, msg.id, msg);
}
messages.setLimit(Math.max(messages.size + 500, limit));
// increase channel cache limits
// in case new messages arrive and invalidate old message references
}
function handleLoadedPinnedMessages(e) {
const channelId = e.channelId;
const channelPinned =
this._pinnedMessagesByChannel[channelId] =
this._pinnedMessagesByChannel[channelId] || [];
var messagesLength = e.messages.length;
if (!messagesLength) return;
const channelCache = this._messagesByChannel[channelId];
e.messages.forEach(msg => {
const cached = channelCache ? channelCache.get(msg.id) : null;
channelPinned.push(cached || new Message(msg));
});
}
class MessageCollection {
constructor(discordie, gateway) {
if (typeof gateway !== "function")
throw new Error("Gateway parameter must be a function");
discordie.Dispatcher.on(Events.GATEWAY_DISPATCH, e => {
if (e.socket != gateway()) return;
Utils.bindGatewayEventHandlers(this, e, {
READY: handleConnectionOpen,
MESSAGE_CREATE: handleMessageCreate,
MESSAGE_UPDATE: handleMessageUpdate,
MESSAGE_DELETE: handleMessageDelete,
MESSAGE_DELETE_BULK: handleMessageDeleteBulk,
CHANNEL_DELETE: handleCleanup,
GUILD_DELETE: handleCleanup,
MESSAGE_REACTION_ADD: handleReaction,
MESSAGE_REACTION_REMOVE: handleReaction,
MESSAGE_REACTION_REMOVE_ALL: handleReactionRemoveAll
});
});
discordie.Dispatcher.on(Events.LOADED_MORE_MESSAGES,
handleLoadedMoreMessages.bind(this));
discordie.Dispatcher.on(Events.LOADED_PINNED_MESSAGES,
handleLoadedPinnedMessages.bind(this));
this.purgeAllCache();
this._messageLimit = 1000;
this._editsLimit = 50;
this._discordie = discordie;
Utils.privatify(this);
}
getChannelMessageLimit(channelId) {
if (!this._discordie._channels.get(channelId)) return -1;
if (!this._messagesByChannel.hasOwnProperty(channelId)) return -1;
return this._messagesByChannel[channelId].limit;
}
setChannelMessageLimit(channelId, limit) {
if (!limit) return false;
if (!this._discordie._channels.get(channelId)) return false;
const messages = getOrCreateChannelCache.call(this, channelId);
messages.setLimit(limit);
return true;
}
getMessageLimit() { return this._messageLimit; }
setMessageLimit(limit) {
if (!limit) return;
if (!(limit > 0)) limit = 1;
var keys = Object.keys(this._messagesByChannel);
for (var i = 0, len = keys.length; i < len; i++) {
var cache = this._messagesByChannel[keys[i]];
// decrease only for channels with default limit
if (!cache) continue;
if (cache.limit != this._messageLimit && limit < cache.limit)
continue;
var removed = cache.setLimit(limit);
if (!removed) continue;
for (var messageId of removed)
delete this._messageEdits[messageId];
}
this._messageLimit = limit;
}
getEditsLimit() { return this._editsLimit; }
setEditsLimit(limit) {
if (!limit) return;
if (!(limit > 0)) limit = 1;
this._editsLimit = limit;
trimEdits.call(this);
}
*getIterator() {
for (var channelId in this._messagesByChannel) {
if (!this._messagesByChannel.hasOwnProperty(channelId)) continue;
if (!this._discordie._channels.get(channelId)) continue;
var channelMessages = this._messagesByChannel[channelId];
var keys = channelMessages._keys;
for (var i = 0, len = keys.length; i < len; i++) {
var message = channelMessages.get(keys[i]);
if (message) yield message;
}
}
}
get(messageId) {
for (var channelId in this._messagesByChannel) {
if (!this._messagesByChannel.hasOwnProperty(channelId)) continue;
if (!this._discordie._channels.get(channelId)) continue;
var channelMessages = this._messagesByChannel[channelId];
var message = channelMessages.get(messageId);
if (message) return message;
}
for (var channelId in this._pinnedMessagesByChannel) {
if (!this._pinnedMessagesByChannel.hasOwnProperty(channelId)) continue;
if (!this._discordie._channels.get(channelId)) continue;
var channelPinned = this._pinnedMessagesByChannel[channelId];
var message = channelPinned.find(msg => msg.id === messageId);
if (message) return message;
}
}
getChannelCache(channelId) {
if (!this._discordie._channels.get(channelId)) return null;
if (!this._messagesByChannel.hasOwnProperty(channelId)) return null;
return this._messagesByChannel[channelId];
}
purgeChannelCache(channelId) {
delete this._hasMoreByChannel[channelId];
delete this._messagesByChannel[channelId];
}
getChannelPinned(channelId) {
if (!this._discordie._channels.get(channelId)) return null;
if (!this._pinnedMessagesByChannel.hasOwnProperty(channelId)) return null;
return this._pinnedMessagesByChannel[channelId];
}
purgeChannelPinned(channelId) {
delete this._pinnedMessagesByChannel[channelId];
}
purgePinned() {
this._pinnedMessagesByChannel = {};
}
getEdits(messageId) {
var edits = this._messageEdits[messageId];
return edits ? edits.slice().reverse() : [];
}
purgeEdits() {
this._messageEdits = {};
}
purgeAllCache() {
this._messagesByChannel = {};
this._hasMoreByChannel = {};
this.purgeEdits();
this.purgePinned();
}
create(message) {
handleMessageCreate.call(this, message);
}
update(message) {
handleMessageUpdate.call(this, message);
}
channelHasMore(channelId) {
if (this._hasMoreByChannel[channelId] === undefined)
return true;
return this._hasMoreByChannel[channelId];
}
}
module.exports = MessageCollection;

View File

@ -0,0 +1,162 @@
"use strict";
const Constants = require("../Constants");
const Events = Constants.Events;
const StatusTypes = Constants.StatusTypes;
const Utils = require("../core/Utils");
const BaseCollection = require("./BaseCollection");
const User = require("../models/User");
function updatePresence(guildId, userId, status, game) {
if (userId === this._discordie._user.id) return;
// ignore friend list presences (without `guild_id`)
if (!guildId) return;
// get presences in all guilds
var presencesForUser =
(this._presencesForGuilds[userId] =
this._presencesForGuilds[userId] || {});
var previousPresencesForUser =
(this._previousPresencesForGuilds[userId] =
this._previousPresencesForGuilds[userId] || {});
var previousPresence = presencesForUser[guildId];
// copy current to previous
previousPresencesForUser[guildId] =
Object.assign(previousPresencesForUser[guildId] || {}, previousPresence);
if (!previousPresence)
delete previousPresencesForUser[guildId];
if (status === StatusTypes.OFFLINE) {
// delete current presence cache if user is not online anymore
delete presencesForUser[guildId];
if (!Object.keys(presencesForUser).length) {
delete this._presencesForGuilds[userId];
}
delete this._statuses[userId];
delete this._games[userId];
} else {
// set current presence
presencesForUser[guildId] = {status, game};
// update global status and game (for user statuses in DM list)
this._statuses[userId] = status;
this._games[userId] = game || null;
}
}
function initializeCache() {
this._presencesForGuilds = {};
this._previousPresencesForGuilds = {};
this._statuses = {};
this._games = {};
}
function handleConnectionOpen(data) {
initializeCache.call(this);
data.guilds.forEach(guild => handleGuildCreate.call(this, guild));
return true;
}
function handleGuildCreate(guild) {
if (!guild || guild.unavailable) return true;
guild.presences.forEach(presence => {
updatePresence.call(this,
guild.id,
presence.user.id,
presence.status,
presence.game
);
});
return true;
}
function handleGuildDelete(guild) {
for (let userId of Object.keys(this._presencesForGuilds)) {
if (!this._presencesForGuilds[userId][guild.id]) continue;
updatePresence.call(this, guild.id, userId, StatusTypes.OFFLINE, null);
}
return true;
}
function handlePresenceUpdate(presence) {
updatePresence.call(this,
presence.guild_id,
presence.user.id,
presence.status,
presence.game
);
return true;
}
function getPresence(collection, userId, guildId) {
if (collection.hasOwnProperty(userId)) {
const presencesForUser = collection[userId];
guildId = guildId || Object.keys(presencesForUser)[0];
if (presencesForUser.hasOwnProperty(guildId)) {
return presencesForUser[guildId];
}
}
return null;
}
class PresenceCollection {
constructor(discordie, gateway) {
if (typeof gateway !== "function")
throw new Error("Gateway parameter must be a function");
discordie.Dispatcher.on(Events.GATEWAY_DISPATCH, e => {
if (e.socket != gateway()) return;
Utils.bindGatewayEventHandlers(this, e, {
READY: handleConnectionOpen,
GUILD_SYNC: handleGuildCreate,
GUILD_CREATE: handleGuildCreate,
GUILD_DELETE: handleGuildDelete,
PRESENCE_UPDATE: handlePresenceUpdate
});
});
initializeCache.call(this);
this._discordie = discordie;
Utils.privatify(this);
}
getStatus(userId, guildId) {
if (this._discordie._user && this._discordie._user.id == userId) {
return this._discordie._user.status;
}
const presence = getPresence.call(this,
this._presencesForGuilds, userId, guildId
) || {};
return presence.status || StatusTypes.OFFLINE;
}
getPreviousStatus(userId, guildId) {
const presence = getPresence.call(this,
this._previousPresencesForGuilds, userId, guildId
) || {};
return presence.status || StatusTypes.OFFLINE;
}
getGame(userId, guildId) {
if (this._discordie._user && this._discordie._user.id == userId) {
return this._discordie._user.game;
}
const presence = getPresence.call(this,
this._presencesForGuilds, userId, guildId
) || {};
return presence.game || null;
}
getPreviousGame(userId, guildId) {
const presence = getPresence.call(this,
this._previousPresencesForGuilds, userId, guildId
) || {};
return presence.game || null;
}
}
module.exports = PresenceCollection;

View File

@ -0,0 +1,135 @@
"use strict";
const Constants = require("../Constants");
const Events = Constants.Events;
const Utils = require("../core/Utils");
const BaseArrayCollection = require("./BaseArrayCollection");
const GUILD_STREAMING_TIMEOUT = 3000;
function emitReady(gw) {
const timedOut = Array.from(this._streamingGuilds);
this._streamingGuilds.clear();
this._discordie.Dispatcher.emit(Events.COLLECTION_READY, {
socket: gw,
name: this.constructor.name,
collection: this
});
if (!timedOut.length) return;
timedOut.forEach(id => {
this._discordie.Dispatcher.emit(Events.GUILD_UNAVAILABLE, {
socket: gw,
guildId: id
});
});
}
function scheduleReadyTimeout(gw) {
this._streamingGuildsTimeout = setTimeout(() => {
this._streamingGuildsTimeout = null;
if (!this._discordie.connected) return;
setImmediate(() => emitReady.call(this, gw));
}, GUILD_STREAMING_TIMEOUT);
}
function maybeReady(gw) {
if (this._streamingGuildsTimeout) {
clearTimeout(this._streamingGuildsTimeout);
}
if (this._streamingGuilds.size) {
scheduleReadyTimeout.call(this, gw);
} else {
setImmediate(() => emitReady.call(this, gw));
}
}
function handleConnectionOpen(data, e) {
clearCollections.call(this);
data.guilds.forEach(guild => {
if (!guild.unavailable) return;
addUnavailable.call(this, guild.id);
this._streamingGuilds.add(guild.id);
});
maybeReady.call(this, e.socket);
return true;
}
function handleGuildCreate(guild, e) {
handleUnavailable.call(this, guild);
if (this.isGuildAvailable(guild) && this._streamingGuilds.has(guild.id)) {
e.suppress = true;
this._streamingGuilds.delete(guild.id);
maybeReady.call(this, e.socket);
}
return true;
}
function handleGuildDelete(guild) {
handleUnavailable.call(this, guild);
return true;
}
function handleUnavailable(guild) {
if (guild.unavailable) {
addUnavailable.call(this, guild.id)
} else {
removeUnavailable.call(this, guild.id);
}
}
function addUnavailable(id) {
if (this._set.has(id)) return;
this._set.add(id);
this.push(id);
}
function removeUnavailable(id) {
if (!this._set.has(id)) return;
this._set.delete(id);
var idx = this.indexOf(id);
this.splice(idx, 1);
}
function clearCollections() {
this._streamingGuilds.clear();
this._set.clear();
this.length = 0;
}
class UnavailableGuildCollection extends BaseArrayCollection {
static _constructor(discordie, gateway) {
if (typeof gateway !== "function")
throw new Error("Gateway parameter must be a function");
discordie.Dispatcher.on(Events.GATEWAY_DISPATCH, e => {
if (e.socket != gateway()) return;
Utils.bindGatewayEventHandlers(this, e, {
READY: handleConnectionOpen,
GUILD_CREATE: handleGuildCreate,
GUILD_DELETE: handleGuildDelete,
});
});
this._discordie = discordie;
this._set = new Set();
this._streamingGuilds = new Set();
this._streamingGuildsTimeout = null;
Utils.privatify(this);
}
isGuildAvailable(guild) {
// unavailable guilds that became available have key `unavailable`
return guild.unavailable === false;
}
}
module.exports = UnavailableGuildCollection;

View File

@ -0,0 +1,191 @@
"use strict";
const Constants = require("../Constants");
const Events = Constants.Events;
const Utils = require("../core/Utils");
const BaseCollection = require("./BaseCollection");
const User = require("../models/User");
const AuthenticatedUser = require("../models/AuthenticatedUser");
function handleConnectionOpen(data) {
this.clear();
this.set(data.user.id, new AuthenticatedUser(data.user));
data.guilds.forEach(guild => {
if (guild.unavailable) return;
guild.members.forEach(member => {
if (member.user.id == data.user.id) return;
this.set(member.user.id, new User(member.user));
});
});
data.private_channels.forEach(channel => {
if (!channel.recipients) return;
channel.recipients.forEach(u => {
if (u.id === data.user.id) return;
this.set(u.id, new User(u));
});
});
return true;
}
function handleUpdateUser(user) {
const cachedUser = this._discordie._user;
delete user.token;
this._discordie._user = new AuthenticatedUser(
cachedUser ? cachedUser.merge(user) : user
);
this.mergeOrSet(user.id, this._discordie._user);
return true;
}
function handleLoadedMoreOrPinnedMessages(e) {
e.messages.forEach(message => {
this.mergeOrSet(message.author.id, new User(message.author));
message.mentions.forEach(mention => {
this.mergeOrSet(mention.id, new User(mention));
});
});
return true;
}
function handleIncomingMessage(message) {
if (message.author) {
this.mergeOrSet(message.author.id, new User(message.author));
}
if (message.mentions) {
message.mentions.forEach(mention => {
this.mergeOrSet(mention.id, new User(mention));
});
}
return true;
}
function handleCreateOrUpdateChannel(channel) {
if (channel.recipient) {
this.mergeOrSet(channel.recipient.id, new User(channel.recipient));
}
if (channel.recipients) {
channel.recipients.forEach(u => this.mergeOrSet(u.id, new User(u)));
}
return true;
}
function handlePresenceUpdate(presence, e) {
if (!presence.user || !presence.user.id) return true;
const cachedUser = this.get(presence.user.id);
if (!cachedUser) {
// update 2015-10-22:
// Discord client now creates local GUILD_MEMBER_ADD event
// update 2015-12-01:
// Discord client creates local GUILD_MEMBER_ADD event only for
// online users with `user.username != null`
this.set(presence.user.id, new User(presence.user));
return true;
}
const replacer = (hasChanges, key) => {
if (presence.user.hasOwnProperty(key)) {
hasChanges = hasChanges ||
(cachedUser[key] != presence.user[key]);
}
return hasChanges;
};
const hasChanges =
["username", "avatar", "discriminator"].reduce(replacer, false);
if (hasChanges) {
const oldUser = this.get(cachedUser.id);
this.mergeOrSet(cachedUser.id, new User(presence.user));
const newUser = this.get(cachedUser.id);
this._discordie.Dispatcher.emit(Events.PRESENCE_MEMBER_INFO_UPDATE, {
socket: e.socket,
old: oldUser,
new: newUser
});
}
return true;
}
function handleLoadedGuildBans(bans) {
bans.forEach(ban => {
this.mergeOrSet(ban.user.id, new User(ban.user));
});
}
function handleBanOrMember(member) {
this.mergeOrSet(member.user.id, new User(member.user));
return true;
}
function handleGuildCreate(guild) {
if (!guild || guild.unavailable) return true;
guild.members.forEach(member => {
if (this._discordie._user.id == member.user.id) return;
this.mergeOrSet(member.user.id, new User(member.user));
});
return true;
}
function handleGuildMembersChunk(chunk) {
if (!chunk.members) return true;
handleGuildCreate.call(this, chunk);
return true;
}
class UserCollection extends BaseCollection {
constructor(discordie, gateway) {
super();
if (typeof gateway !== "function")
throw new Error("Gateway parameter must be a function");
discordie.Dispatcher.on(Events.GATEWAY_DISPATCH, e => {
if (e.socket != gateway()) return;
Utils.bindGatewayEventHandlers(this, e, {
READY: handleConnectionOpen,
USER_UPDATE: handleUpdateUser,
PRESENCE_UPDATE: handlePresenceUpdate,
MESSAGE_CREATE: handleIncomingMessage,
MESSAGE_UPDATE: handleIncomingMessage,
GUILD_SYNC: handleGuildCreate,
GUILD_CREATE: handleGuildCreate,
GUILD_BAN_ADD: handleBanOrMember,
GUILD_BAN_REMOVE: handleBanOrMember,
GUILD_MEMBER_ADD: handleBanOrMember,
GUILD_MEMBER_REMOVE: handleBanOrMember,
CHANNEL_RECIPIENT_ADD: handleBanOrMember,
CHANNEL_RECIPIENT_REMOVE: handleBanOrMember,
CHANNEL_CREATE: handleCreateOrUpdateChannel,
CHANNEL_UPDATE: handleCreateOrUpdateChannel,
GUILD_MEMBERS_CHUNK: handleGuildMembersChunk
});
});
discordie.Dispatcher.on(Events.LOADED_MORE_MESSAGES,
handleLoadedMoreOrPinnedMessages.bind(this));
discordie.Dispatcher.on(Events.LOADED_PINNED_MESSAGES,
handleLoadedMoreOrPinnedMessages.bind(this));
discordie.Dispatcher.on(Events.LOADED_GUILD_BANS,
handleLoadedGuildBans.bind(this));
this._discordie = discordie;
Utils.privatify(this);
}
updateAuthenticatedUser(user) {
handleUpdateUser.call(this, user);
}
update(user) {
this.mergeOrSet(user.id, new User(user));
}
}
module.exports = UserCollection;

View File

@ -0,0 +1,273 @@
"use strict";
const Constants = require("../Constants");
const Errors = Constants.Errors;
const Events = Constants.Events;
const Utils = require("../core/Utils");
const BaseArrayCollection = require("./BaseArrayCollection");
const IVoiceConnection = require("../interfaces/IVoiceConnection");
/**
* @class
*/
class VoiceConnectionInfo {
constructor(gatewaySocket, voiceSocket, voiceConnection) {
/**
* @instance
* @memberOf VoiceConnectionInfo
* @name gatewaySocket
* @returns {GatewaySocket}
*/
this.gatewaySocket = gatewaySocket;
/**
* @instance
* @memberOf VoiceConnectionInfo
* @name voiceSocket
* @returns {VoiceSocket}
*/
this.voiceSocket = voiceSocket;
/**
* @instance
* @memberOf VoiceConnectionInfo
* @name voiceConnection
* @returns {IVoiceConnection}
*/
this.voiceConnection = voiceConnection;
Object.freeze(this);
}
}
class VoiceConnectionCollection extends BaseArrayCollection {
static _constructor(discordie, primaryGateway) {
this._gateways = new Set();
this._pendingConnections = new Map();
// handle pending disconnects first to avoid firing a rejected pending
discordie.Dispatcher.on(Events.VOICESOCKET_DISCONNECT, e => {
var voiceSocket = e.socket;
var gatewaySocket = e.socket.gatewaySocket;
var guildId = voiceSocket.guildId;
const awaitingEndpoint =
e.error && e.error.message == Errors.VOICE_CHANGING_SERVER;
if (awaitingEndpoint) {
this._createPending(guildId);
} else {
var pending = this._pendingConnections.get(guildId);
if (!pending) return;
gatewaySocket.disconnectVoice(guildId);
this._pendingConnections.delete(guildId);
e ? pending.reject(e.error) : pending.reject();
}
});
// process voice connections
discordie.Dispatcher.on(Events.VOICE_SESSION_DESCRIPTION, e => {
const gw = e.socket.gatewaySocket;
const voicews = e.socket;
const voiceConnection = new IVoiceConnection(discordie, gw, voicews);
this.push(new VoiceConnectionInfo(gw, voicews, voiceConnection));
discordie.Dispatcher.emit(Events.VOICE_CONNECTED, {
socket: voicews,
voiceConnection: voiceConnection
});
});
discordie.Dispatcher.on(Events.VOICESOCKET_DISCONNECT, e => {
const idx = this.findIndex(c => c.voiceSocket == e.socket);
if (idx < 0) return;
var info = this[idx];
// delete from this array
this.splice(idx, 1);
var guildId = info.voiceSocket.guildId;
const awaitingEndpoint =
e.error && e.error.message == Errors.VOICE_CHANGING_SERVER;
const manual =
e.error && e.error.message == Errors.VOICE_MANUAL_DISCONNECT;
const endpointAwait =
awaitingEndpoint ? this._createPending(guildId) : null;
discordie.Dispatcher.emit(Events.VOICE_DISCONNECTED, {
socket: info.voiceSocket,
voiceConnection: info.voiceConnection,
error: (e.error instanceof Error) ? e.error : null,
manual, endpointAwait
});
if (guildId && !endpointAwait)
info.gatewaySocket.disconnectVoice(guildId);
info.voiceConnection.dispose();
});
// resolve promise when we have a voice connection created
discordie.Dispatcher.on(Events.VOICE_SESSION_DESCRIPTION, e => {
var voiceSocket = e.socket;
var guildId = voiceSocket.guildId;
var pending = this._pendingConnections.get(guildId);
if (!pending) return;
this._pendingConnections.delete(guildId);
pending.resolve(this.getForVoiceSocket(voiceSocket));
});
discordie.Dispatcher.on(Events.GATEWAY_DISPATCH, e => {
if (e.type === "READY" || e.type === "RESUMED") {
this._gateways.add(e.socket);
}
// process edge cases
if (e.type === "CALL_DELETE") {
const info = this.getForChannel(e.data.channel_id);
if (!info) return;
if (e.data.unavailable) {
info.voiceConnection._disconnect(Errors.VOICE_CALL_UNAVAILABLE);
} else {
info.voiceConnection._disconnect();
}
}
if (e.type === "GUILD_DELETE") {
const info = this.getForGuild(e.data.id);
if (!info) return;
if (e.data.unavailable) {
info.voiceConnection._disconnect(Errors.VOICE_GUILD_UNAVAILABLE);
} else {
info.voiceConnection._disconnect();
}
}
if (e.type === "CHANNEL_DELETE") {
const info = this.getForChannel(e.data.id);
if (info) info.voiceConnection._disconnect();
}
});
discordie.Dispatcher.on(Events.GATEWAY_DISCONNECT, e => {
Array.from(this._pendingConnections.keys()).forEach(guildId => {
var pending = this._pendingConnections.get(guildId);
if (!pending) return;
this._pendingConnections.delete(guildId);
pending.reject(new Error("Gateway disconnected"));
});
this._gateways.delete(e.socket);
});
this._discordie = discordie;
Utils.privatify(this);
}
_createPending(guildId) {
return this._getOrCreate(guildId, null, null, null, true);
}
_cancelPending(guildId) {
let pending = this._pendingConnections.get(guildId);
if (!pending) return;
this._pendingConnections.delete(guildId);
pending.reject(new Error("Cancelled"));
}
_isPending(guildId) {
return this._pendingConnections.get(guildId);
}
_getOrCreate(guildId, channelId, selfMute, selfDeaf, silent) {
const gateway = this._discordie.gatewaySocket;
if (!gateway || !gateway.connected)
return Promise.reject(new Error("No gateway socket (not connected)"));
// voiceStateUpdate triggers some events and can update pending connections
var newState = {guildId, channelId, selfMute, selfDeaf};
if (!silent && this.shouldUpdateVoiceState(newState)) {
if (!channelId && this.shouldCancelPending(guildId)) {
this._cancelPending(guildId);
}
gateway.voiceStateUpdate(guildId, channelId, selfMute, selfDeaf);
}
if (!silent && !channelId) return;
let pending = this._pendingConnections.get(guildId);
if (pending) return pending.promise;
const existing = this.getForGuild(guildId);
if (existing) return Promise.resolve(existing);
pending = {gateway, promise: null, resolve: null, reject: null};
this._pendingConnections.set(guildId, pending);
return (pending.promise = new Promise((rs, rj) => {
pending.resolve = rs;
pending.reject = rj;
}));
}
getForGuild(guildId) {
guildId = guildId ? guildId.valueOf() : null;
return this.find(c => c.voiceSocket.guildId == guildId) || null;
}
getForChannel(channelId) {
channelId = channelId.valueOf();
for (var gatewaySocket of this._gateways.values()) {
for (var voiceState of gatewaySocket.voiceStates.values()) {
if (!voiceState || voiceState.channelId !== channelId) continue;
return this.getForGuild(voiceState.guildId);
}
}
return null;
}
getForVoiceSocket(voiceSocket) {
return this.find(c => c.voiceSocket == voiceSocket) || null;
}
getForGatewaySocket(gatewaySocket) {
return this.find(c => c.gatewaySocket == gatewaySocket) || null;
}
isLocalSession(sessionId) {
return !!Array.from(this._gateways).find(gw => gw.sessionId == sessionId);
}
shouldUpdateVoiceState(newState) {
var guildId = newState.guildId;
for (var gatewaySocket of this._gateways.values()) {
var state = gatewaySocket.voiceStates.get(guildId);
if (!state) continue;
var hasChanges = Object.keys(state).reduce((r, key) => {
return r ||
(newState[key] !== undefined && newState[key] != state[key]);
}, false);
return hasChanges;
}
return true;
}
shouldCancelPending(guildId) {
// cancel pending connection only if there are no sockets open
// otherwise disconnect existing socket and fire VOICESOCKET_DISCONNECT
for (var gatewaySocket of this._gateways.values()) {
var socket = gatewaySocket.voiceSockets.get(guildId);
if (!socket) continue;
return false;
}
return true;
}
getPendingChannel(guildId) {
for (var gatewaySocket of this._gateways.values()) {
var state = gatewaySocket.voiceStates.get(guildId);
if (!state) continue;
return state.channelId;
}
return null;
}
cancelIfPending(guildId, channelId) {
const pendingChannelId = this.getPendingChannel(guildId);
if (pendingChannelId && pendingChannelId === channelId)
this._getOrCreate(guildId, null);
}
}
module.exports = VoiceConnectionCollection;

View File

@ -0,0 +1,352 @@
"use strict";
const Constants = require("../Constants");
const StatusTypes = Constants.StatusTypes;
const Events = Constants.Events;
const Utils = require("../core/Utils");
const BaseCollection = require("./BaseCollection");
const User = require("../models/User");
const AuthenticatedUser = require("../models/AuthenticatedUser");
function getUserStates(userId) {
var states = [];
var channels = this._channelsForUser.get(userId);
if (!channels) return states;
Array.from(channels).forEach(channelId => {
var userMap = this._usersForChannel.get(channelId);
if (!userMap || !userMap.has(userId)) return;
states.push(userMap.get(userId));
});
return states;
}
function createChangeEvent(state, e) {
return {
socket: e.socket,
user: this._discordie.Users.get(state.user_id),
channel:
this._discordie.Channels.get(state.channel_id) ||
this._discordie.DirectMessageChannels.get(state.channel_id),
channelId: state.channel_id,
guildId: state.guild_id
};
}
function emitMuteDeafUpdate(type, state, key, e) {
var e = createChangeEvent.call(this, state, e);
e.state = state[key];
this._discordie.Dispatcher.emit(type, e);
}
function emitChanges(before, after, e) {
if (!before.length && !after.length) return;
var leave =
before.filter(b => !after.find(a => a.channel_id == b.channel_id));
var join =
after.filter(a => !before.find(b => a.channel_id == b.channel_id));
var moved = leave.length === 1 && join.length === 1;
leave.forEach(state => {
var event = createChangeEvent.call(this, state, e);
event.newChannelId = moved ? join[0].channel_id : null;
event.newGuildId = moved ? join[0].guild_id : null;
this._discordie.Dispatcher.emit(
Events.VOICE_CHANNEL_LEAVE,
event
);
});
join.forEach(state => {
this._discordie.Dispatcher.emit(
Events.VOICE_CHANNEL_JOIN,
createChangeEvent.call(this, state, e)
);
});
if (!leave.length && !join.length) {
var sm = after.find(b => before.find(a => a.self_mute != b.self_mute));
var sd = after.find(b => before.find(a => a.self_deaf != b.self_deaf));
var m = after.find(b => before.find(a => a.mute != b.mute));
var d = after.find(b => before.find(a => a.deaf != b.deaf));
var _emitMuteDeafUpdate = emitMuteDeafUpdate.bind(this);
if (sm)_emitMuteDeafUpdate(Events.VOICE_USER_SELF_MUTE, sm, "self_mute", e);
if (sd)_emitMuteDeafUpdate(Events.VOICE_USER_SELF_DEAF, sd, "self_deaf", e);
if (m) _emitMuteDeafUpdate(Events.VOICE_USER_MUTE, m, "mute", e);
if (d) _emitMuteDeafUpdate(Events.VOICE_USER_DEAF, d, "deaf", e);
}
}
function shouldCalculateChanges() {
var Dispatcher = this._discordie.Dispatcher;
return Dispatcher.hasListeners(Events.VOICE_CHANNEL_JOIN) ||
Dispatcher.hasListeners(Events.VOICE_CHANNEL_LEAVE) ||
Dispatcher.hasListeners(Events.VOICE_USER_SELF_MUTE) ||
Dispatcher.hasListeners(Events.VOICE_USER_SELF_DEAF) ||
Dispatcher.hasListeners(Events.VOICE_USER_MUTE) ||
Dispatcher.hasListeners(Events.VOICE_USER_DEAF);
}
function getOrCreate(type, target, key) {
const _T = target.get(key);
const got = _T || new type();
if (!_T) target.set(key, got);
return got;
}
function speakingDelete(userId, guildId) {
if (guildId) {
const info = this._discordie.VoiceConnections.getForGuild(guildId);
if (!info) return;
const speakingSet = this._speakingForVC.get(info.voiceConnection);
if (speakingSet) speakingSet.delete(userId);
return;
}
for (var speakingSet of this._speakingForVC.values()) {
speakingSet.delete(userId);
}
}
function ssrcDelete(userId, guildId) {
function removeFromMap(ssrcMap) {
for (var pair of ssrcMap.entries()) {
var ssrc = pair[0];
var user = pair[1];
if (user == userId) ssrcMap.delete(ssrc);
}
}
if (guildId) {
const info = this._discordie.VoiceConnections.getForGuild(guildId);
if (!info) return;
const ssrcMap = this._ssrcForVC.get(info.voiceConnection);
if (ssrcMap) removeFromMap(ssrcMap);
return;
}
for (var ssrcMap of this._ssrcForVC.values()) {
removeFromMap(ssrcMap);
}
}
function userDelete(userId, guildId) {
var channels = this._channelsForUser.get(userId);
if (!channels) return;
for (var channelId of channels.values()) {
var userMap = this._usersForChannel.get(channelId);
if (!userMap) continue;
if (guildId) {
var state = userMap.get(userId);
if (state.guild_id && state.guild_id !== guildId) continue;
}
userMap.delete(userId);
channels.delete(channelId);
if (!userMap.size) this._usersForChannel.delete(channelId);
if (!channels.size) this._channelsForUser.delete(userId);
}
}
function channelDelete(channelId) {
var userMap = this._usersForChannel.get(channelId);
this._usersForChannel.delete(channelId);
if (!userMap) return;
for (var state of userMap.values()) {
var channels = this._channelsForUser.get(state.user_id);
channels.delete(state.channel_id);
if (!channels.size) this._channelsForUser.delete(state.user_id);
}
}
function initializeCache() {
this._speakingForVC = new Map(); // Map<IVoiceConnection, Map<userId, bool>>
this._ssrcForVC = new Map(); // Map<IVoiceConnection, Map<userId, ssrc>>
this._usersForChannel = new Map(); // Map<channelId, Map<userId, voiceState>>
this._channelsForUser = new Map(); // Map<userId, Set<channelId>>
}
function handleConnectionOpen(data) {
initializeCache.call(this);
data.guilds.forEach(guild => handleGuildCreate.call(this, guild));
return true;
}
function handleGuildCreate(guild) {
if (guild.unavailable) return;
guild.voice_states.forEach(state => {
// states in READY don't contain `guild_id`
state.guild_id = guild.id;
insertVoiceState.call(this, state);
});
}
function handleVoiceStateUpdateChanges(data, e) {
// process only if we have event listeners for those events
if (!shouldCalculateChanges.call(this)) {
handleVoiceStateUpdate.call(this, data);
return true;
}
var before = getUserStates.call(this, data.user_id);
handleVoiceStateUpdate.call(this, data);
var after = getUserStates.call(this, data.user_id);
process.nextTick(() => emitChanges.call(this, before, after, e));
return true;
}
function handleVoiceStateUpdate(data) {
userDelete.call(this, data.user_id, data.guild_id);
if (!data.channel_id) {
speakingDelete.call(this, data.user_id, data.guild_id);
ssrcDelete.call(this, data.user_id, data.guild_id);
return true;
}
insertVoiceState.call(this, data);
return true;
}
function insertVoiceState(data) {
getOrCreate(Map, this._usersForChannel, data.channel_id)
.set(data.user_id, data);
getOrCreate(Set, this._channelsForUser, data.user_id)
.add(data.channel_id);
}
function handleVoiceSpeaking(data, voiceSocket) {
const info = this._discordie.VoiceConnections.getForVoiceSocket(voiceSocket);
if (!info) return true;
const vc = info.voiceConnection;
const speakingSet = getOrCreate(Set, this._speakingForVC, vc);
data.speaking ?
speakingSet.add(data.user_id) :
speakingSet.delete(data.user_id);
getOrCreate(Map, this._ssrcForVC, vc)
.set(data.ssrc, data.user_id);
return true;
}
function handleChannelDelete(channel, e) {
// just silently delete voice states as clients still stay connected to
// deleted channels
//var userMap = this._usersForChannel.get(channel.id);
//for (var userId of userMap.keys()) {
// var event = createChangeEvent.call(this, {
// user_id: userId,
// channel_id: channel.id,
// guild_id: channel.guild_id
// }, e);
// event.newChannelId = event.newGuildId = null;
//
// this._discordie.Dispatcher.emit(Events.VOICE_CHANNEL_LEAVE, event);
//}
channelDelete.call(this, channel.id);
return true;
}
function handleGuildDelete(guild) {
handleCleanup.call(this);
return true;
}
function handleCleanup() {
for (var channelId of this._usersForChannel.keys()) {
// delete all channel states if channel is no longer in cache
if (!this._discordie._channels.get(channelId))
channelDelete.call(this, channelId);
}
}
function handleCallCreate(call) {
if (!call || !call.voice_states) return true;
call.voice_states.forEach(state => {
state.guild_id = null;
insertVoiceState.call(this, state);
})
}
function handleCallDelete(call) {
if (!call || !call.channel_id) return true;
channelDelete.call(this, call.channel_id);
}
class VoiceStateCollection {
constructor(discordie, gateway) {
if (typeof gateway !== "function")
throw new Error("Gateway parameter must be a function");
discordie.Dispatcher.on(Events.GATEWAY_DISPATCH, e => {
if (e.socket != gateway()) return;
Utils.bindGatewayEventHandlers(this, e, {
READY: handleConnectionOpen,
GUILD_CREATE: handleGuildCreate,
GUILD_DELETE: handleGuildDelete,
CHANNEL_DELETE: handleChannelDelete,
VOICE_STATE_UPDATE: handleVoiceStateUpdateChanges,
CALL_CREATE: handleCallCreate,
CALL_DELETE: handleCallDelete
});
});
discordie.Dispatcher.on(Events.VOICE_SPEAKING, e => {
if (handleVoiceSpeaking.call(this, e.data, e.socket))
e.handled = true;
if (e.data && e.data.user_id) {
const user = this._discordie.Users.get(e.data.user_id);
if (user) e.user = user;
const info = discordie.VoiceConnections.getForVoiceSocket(e.socket);
if (info) e.voiceConnection = info.voiceConnection;
}
});
discordie.Dispatcher.on(Events.VOICE_DISCONNECTED, e => {
this._speakingForVC.delete(e.voiceConnection);
this._ssrcForVC.delete(e.voiceConnection);
});
initializeCache.call(this);
this._discordie = discordie;
Utils.privatify(this);
}
getStatesInChannel(channelId) {
channelId = channelId.valueOf();
const userMap = this._usersForChannel.get(channelId);
if (!userMap) return new Map();
return userMap;
}
getUserStateInGuild(guildId, userId) {
// note: there can be more than 1 voice member with same user id in guild
// this will return only the first voice state registered
var channels = this._channelsForUser.get(userId);
if (!channels) return null;
for (var channelId of channels.values()) {
const userMap = this._usersForChannel.get(channelId);
if (!userMap) continue;
var state = userMap.get(userId);
if (!state) continue;
if (state.guild_id == guildId) return state;
}
return null;
}
ssrcToUserId(voiceConnection, ssrc) {
const ssrcMap = this._ssrcForVC.get(voiceConnection);
if (ssrcMap) return ssrcMap.get(ssrc) || null;
return null;
}
}
module.exports = VoiceStateCollection;