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