1 module diggler.context;
2 
3 import std.array;
4 
5 import diggler.bot;
6 import diggler.tracker;
7 
8 import irc.client;
9 import irc.tracker : IrcTracker, TrackedChannel;
10 
11 /**
12  * _Context for operations used by command methods.
13  *
14  * $(DPREF command, CommandSet) subtypes an instance
15  * of this type, allowing command methods to access
16  * the properties and methods of this type without
17  * any preceding qualification.
18  *
19  * Some operations are synchronous, non-blocking operations;
20  * they may time a significant duration of time to complete,
21  * but they do not block the thread from handling other events,
22  * such as other command invocations.
23  */
24 struct Context
25 {
26 	private:
27 	Bot _bot;
28 	Bot.ClientEventHandler _client;
29 	BotTracker tracker;
30 	string target;
31 	IrcUser _user;
32 	bool isPm;
33 
34 	public:
35 	this(Bot _bot, Bot.ClientEventHandler client, BotTracker tracker, string target, ref IrcUser _user, bool isPm)
36 	{
37 		this._bot = _bot;
38 		this._client = client;
39 		this.tracker = tracker;
40 		this.target = target;
41 		this._user = _user;
42 		this.isPm = isPm;
43 	}
44 
45 	/**
46 	 * Create a new context from an existing one,
47 	 * but with a different originating channel/user.
48 	 *
49 	 * Params:
50 	 *    ctx = existing context to copy
51 	 *    target = channel name or user nick name
52 	 */
53 	this(Context ctx, string target)
54 	{
55 		import irc.protocol : channelPrefixes;
56 		this = ctx;
57 
58 		this.target = target;
59 
60 		switch(target.front)
61 		{
62 			foreach(channelPrefix; channelPrefixes)
63 			case channelPrefix:
64 				this.isPm = false;
65 				break;
66 			default:
67 				this.isPm = true;
68 		}
69 	}
70 
71 	/// The current bot.
72 	Bot bot() @property pure nothrow
73 	{
74 		return _bot;
75 	}
76 
77 	/// The current network.
78 	IrcClient network() @property pure nothrow
79 	{
80 		return _client;
81 	}
82 
83 	/// The _user that invoked the command.
84 	ref const(IrcUser) user() @property pure nothrow
85 	{
86 		return _user;
87 	}
88 
89 	/**
90 	 * The _channel the command was invoked in.
91 	 *
92 	 * Throws an exception if the command originated from
93 	 * a private message.
94 	 */
95 	TrackedChannel channel() @property
96 	{
97 		if(isPm)
98 			throw new Exception("not in a channel");
99 
100 		return *tracker.findChannel(target);
101 	}
102 
103 	/**
104 	 * Boolean whether or not the command was invoked
105 	 * from a private message.
106 	 */
107 	bool isPrivateMessage() @property pure nothrow
108 	{
109 		return isPm;
110 	}
111 
112 	/**
113 	 * Reply to the channel in which the command was invoked.
114 	 * If there is more than one argument, the first argument
115 	 * is formatted with subsequent ones.
116 	 *
117 	 * If the command originated in a private message,
118 	 * the _reply is sent to the invoking user as a private message.
119 	 * See_Also:
120 	 *    $(STDREF format, formattedWrite)
121 	 */
122 	void reply(FmtArgs...)(in char[] fmt, FmtArgs fmtArgs)
123 	{
124 		_client.sendf(target, fmt, fmtArgs);
125 	}
126 
127 	/**
128 	 * Wait the given length of time before returning.
129 	 *
130 	 * This is a synchronous but non-blocking operation.
131 	 */
132 	void wait(double time)
133 	{
134 		auto curFiber = _bot.commandQueue.fiber();
135 
136 		_bot.eventLoop.post(() {
137 			_bot.eventLoop.wakeFiber(curFiber);
138 		}, time);
139 
140 		curFiber.yield();
141 	}
142 
143 	/// Result of $(MREF Context.whois).
144 	static struct WhoisResult
145 	{
146 		/// Nickname, username and hostname of the _user.
147 		IrcUser user;
148 
149 		/// Real name of the user.
150 		string realName;
151 
152 		/// Channels the user is currently a member of.
153 		string[] channels;
154 
155 		/// Boolean whether or not the user is an IRC
156 		/// (server or network-wide) _operator.
157 		bool operator = false;
158 	}
159 
160 	/**
161 	 * Lookup more information about the user for the given nick name.
162 	 *
163 	 * This is a synchronous but non-blocking operation.
164 	 * Params:
165 	 *    nickName = nick name of user to lookup
166 	 */
167 	// TODO: handle error response and timeout
168 	WhoisResult whois(string nickName)
169 	{
170 		WhoisResult result;
171 
172 		auto curFiber = _bot.commandQueue.fiber();
173 
174 		void onWhoisReply(IrcUser user, in char[] realName)
175 		{
176 			if(user.nickName == nickName)
177 			{
178 				_client.onWhoisReply.unsubscribeHandler(&onWhoisReply);
179 				result.user = IrcUser(user.nickName.idup, user.userName.idup, user.hostName.idup);
180 				result.realName = realName.idup;
181 				_bot.eventLoop.wakeFiber(curFiber);
182 			}
183 		}
184 
185 		_client.onWhoisReply ~= &onWhoisReply;
186 		_client.queryWhois(nickName);
187 		curFiber.yield();
188 
189 		return result;
190 	}
191 
192 	// TODO: handle timeout
193 	bool isAdmin(string nickName)
194 	{
195 		auto sortedAdminList = bot.adminList.assumeSorted!((a, b) => a.nickName < b.nickName)();
196 		auto accounts = sortedAdminList.equalRange(Bot.Admin(nickName));
197 		if(accounts.empty)
198 			return false;
199 
200 		if(auto user = tracker.findUser(nickName))
201 		{
202 			if(user.payload.isAdmin)
203 				return true;
204 
205 			auto curFiber = _bot.commandQueue.fiber();
206 			bool result = false;
207 
208 			void onWhoisAccountReply(in char[] nick, in char[] accountName)
209 			{
210 				if(nick == nickName)
211 				{
212 					_client.onWhoisAccountReply.unsubscribeHandler(&onWhoisAccountReply);
213 					auto sortedAdminList = bot.adminList.assumeSorted!((a, b) => a.nickName < b.nickName)();
214 					result = sortedAdminList.contains(Bot.Admin(nickName, cast(immutable)accountName));
215 					_bot.eventLoop.wakeFiber(curFiber);
216 				}
217 			}
218 
219 			void onWhoisEnd(in char[] nick)
220 			{
221 				if(nick == nickName)
222 				{
223 					_client.onWhoisAccountReply.unsubscribeHandler(&onWhoisAccountReply);
224 					_client.onWhoisEnd.unsubscribeHandler(&onWhoisEnd);
225 					_bot.eventLoop.wakeFiber(curFiber);
226 				}
227 			}
228 
229 			_client.onWhoisAccountReply ~= &onWhoisAccountReply;
230 			_client.onWhoisEnd ~= &onWhoisEnd;
231 			_client.queryWhois(nickName);
232 			curFiber.yield();
233 
234 			return result;
235 		}
236 		else
237 			return false;
238 	}
239 
240 	/**
241 	 * Disconnect from the current network with the given message.
242 	 * Params:
243 	 *    msg = comment sent in _quit notification
244 	 */
245 	void quit(in char[] msg)
246 	{
247 		_client.quit(msg);
248 	}
249 }