1 module dcord.state;
2
3 import std.functional,
4 std.stdio,
5 std.algorithm.iteration,
6 std.experimental.logger;
7
8 import vibe.core.sync : createManualEvent, LocalManualEvent;
9 import std.algorithm.searching : canFind, countUntil;
10 import std.algorithm.mutation : remove;
11
12 import dcord.api,
13 dcord.types,
14 dcord.client,
15 dcord.gateway,
16 dcord.util.emitter;
17
18 /**
19 The State class is used to track and maintain client state.
20 */
21 class State: Emitter {
22 // Client
23 Client client;
24 APIClient api;
25 GatewayClient gw;
26
27 /// Currently logged in user, recieved from READY payload.
28 User me;
29
30 /// All users that the bot sees
31 UserMap users;
32
33 /// All currently loaded guilds
34 GuildMap guilds;
35
36 /// All currently loaded DMs
37 ChannelMap directMessages;
38
39 /// All currently loaded channels
40 ChannelMap channels;
41
42 /// All voice states
43 VoiceStateMap voiceStates;
44
45 /// Event triggered when all guilds are synced
46 LocalManualEvent ready;
47
48 bool requestOfflineMembers = true;
49
50 private {
51 Snowflake[] awaitingCreate;
52
53 Logger log;
54 EventListenerArray listeners;
55 }
56
57 this(Client client) {
58 this.client = client;
59 this.log = client.log;
60 this.api = client.api;
61 this.gw = client.gw;
62
63 this.users = new UserMap;
64 this.guilds = new GuildMap;
65 this.directMessages = new ChannelMap;
66 this.channels = new ChannelMap;
67 this.voiceStates = new VoiceStateMap;
68
69 this.ready = createManualEvent();
70
71 // Finally bind all listeners
72 this.bindListeners();
73 }
74
75 private void listen(Ty...)() {
76 foreach (T; Ty) {
77 this.listeners ~= this.client.events.listen!T(mixin("&this.on" ~ T.stringof));
78 }
79 }
80
81 private void bindListeners() {
82 // Unbind all listeners
83 this.listeners.each!((l) => l.unbind());
84
85 // Always listen for ready payload
86 this.listen!(
87 Ready, GuildCreate, GuildUpdate, GuildDelete, GuildMemberAdd, GuildMemberRemove,
88 GuildMemberUpdate, GuildMembersChunk, GuildRoleCreate, GuildRoleUpdate, GuildRoleDelete,
89 GuildEmojisUpdate, ChannelCreate, ChannelUpdate, ChannelDelete, VoiceStateUpdate, MessageCreate,
90 PresenceUpdate
91 );
92 }
93
94 private void onReady(Ready r) {
95 this.me = r.me;
96
97 foreach (guild; r.guilds) {
98 this.awaitingCreate ~= guild.id;
99 }
100
101 foreach (dm; r.dms) {
102 this.directMessages[dm.id] = dm;
103 }
104 }
105
106 private void onGuildCreate(GuildCreate c) {
107 // If this guild is "coming online" and we're awaiting its creation, clear that state here
108 if (!c.unavailable && this.awaitingCreate.canFind(c.guild.id)) {
109 this.awaitingCreate.remove(this.awaitingCreate.countUntil(c.guild.id));
110
111 // If no other guilds are awaiting, emit the event
112 if (this.awaitingCreate.length == 0) {
113 this.ready.emit();
114 }
115 }
116
117 this.guilds[c.guild.id] = c.guild;
118
119 c.guild.channels.each((c) {
120 this.channels[c.id] = c;
121 });
122
123 c.guild.members.each((m) {
124 this.users[m.user.id] = m.user;
125 });
126
127 c.guild.voiceStates.each((v) {
128 this.voiceStates[v.sessionID] = v;
129 });
130
131 if (this.requestOfflineMembers) {
132 c.guild.requestOfflineMembers();
133 }
134 }
135
136 private void onGuildUpdate(GuildUpdate c) {
137 if (!this.guilds.has(c.guild.id)) return;
138 // TODO: handle updates, iterate over raw data
139 // this.guilds[c.guild.id].fromUpdate(c);
140 }
141
142 private void onGuildDelete(GuildDelete c) {
143 if (!this.guilds.has(c.guildID)) return;
144 this.guilds.remove(c.guildID);
145 }
146
147 private void onGuildMemberAdd(GuildMemberAdd c) {
148 if (this.users.has(c.member.user.id)) {
149 this.users[c.member.user.id] = c.member.user;
150 }
151
152 if (this.guilds.has(c.member.guild.id)) {
153 this.guilds[c.member.guild.id].members[c.member.user.id] = c.member;
154 }
155 }
156
157 private void onGuildMemberRemove(GuildMemberRemove c) {
158 if (!this.guilds.has(c.guildID)) return;
159 if (!this.guilds[c.guildID].members.has(c.user.id)) return;
160 this.guilds[c.guildID].members.remove(c.user.id);
161 }
162
163 private void onGuildMemberUpdate(GuildMemberUpdate c) {
164 if (!this.guilds.has(c.member.guildID)) return;
165 if (!this.guilds[c.member.guildID].members.has(c.member.user.id)) return;
166 }
167
168 private void onGuildRoleCreate(GuildRoleCreate c) {
169 if (!this.guilds.has(c.guildID)) return;
170 this.guilds[c.guildID].roles[c.role.id] = c.role;
171 }
172
173 private void onGuildRoleDelete(GuildRoleDelete c) {
174 if (!this.guilds.has(c.guildID)) return;
175 if (!this.guilds[c.guildID].roles.has(c.role.id)) return;
176 this.guilds[c.guildID].roles.remove(c.role.id);
177 }
178
179 private void onGuildRoleUpdate(GuildRoleUpdate c) {
180 if (!this.guilds.has(c.guildID)) return;
181 if (!this.guilds[c.guildID].roles.has(c.role.id)) return;
182 this.guilds[c.guildID].roles[c.role.id] = c.role;
183 }
184
185 private void onChannelCreate(ChannelCreate c) {
186 this.channels[c.channel.id] = c.channel;
187 }
188
189 private void onChannelUpdate(ChannelUpdate c) {
190 this.channels[c.channel.id] = c.channel;
191 }
192
193 private void onChannelDelete(ChannelDelete c) {
194 if (this.channels.has(c.channel.id)) {
195 this.channels.remove(c.channel.id);
196 }
197 }
198
199 private void onVoiceStateUpdate(VoiceStateUpdate u) {
200 // TODO: shallow tracking, don't require guilds
201 auto guild = this.guilds.get(u.state.guildID);
202 if (!guild) return;
203
204 if (!u.state.channelID) {
205 this.voiceStates.remove(u.state.sessionID);
206 guild.voiceStates.remove(u.state.sessionID);
207 } else {
208 this.voiceStates[u.state.sessionID] = u.state;
209 guild.voiceStates[u.state.sessionID] = u.state;
210 }
211 }
212
213 private void onGuildMembersChunk(GuildMembersChunk c) {
214 // TODO
215 }
216
217 private void onGuildEmojisUpdate(GuildEmojisUpdate c) {
218 // TODO
219 }
220
221 private void onMessageCreate(MessageCreate mc) {
222 // TODO
223 }
224
225 private void onPresenceUpdate(PresenceUpdate p) {
226 // TODO
227 }
228 }