1 /++
2     The CTCP service handles responding to CTCP (client-to-client protocol)
3     requests behind the scenes.
4 
5     It has no commands and is not aware in the normal sense; it only blindly
6     responds to requests.
7 
8     See_Also:
9         [kameloso.plugins.common.core],
10         [kameloso.plugins.common.misc]
11 
12     Copyright: [JR](https://github.com/zorael)
13     License: [Boost Software License 1.0](https://www.boost.org/users/license.html)
14 
15     Authors:
16         [JR](https://github.com/zorael)
17  +/
18 module kameloso.plugins.services.ctcp;
19 
20 version(WithCTCPService):
21 
22 private:
23 
24 import kameloso.plugins;
25 import kameloso.plugins.common.core;
26 import kameloso.messaging;
27 import dialect.defs;
28 import std.typecons : Flag, No, Yes;
29 
30 
31 // onCTCPs
32 /++
33     Handles `CTCP` requests.
34 
35     This is a catch-all function handling most `CTCP` requests we support,
36     instead of having five different functions each dealing with one.
37     Either design works; both end up with a switch.
38  +/
39 @(IRCEventHandler()
40     //.onEvent(IRCEvent.Type.CTCP_SLOTS)  // We don't really need to handle those
41     .onEvent(IRCEvent.Type.CTCP_VERSION)
42     .onEvent(IRCEvent.Type.CTCP_FINGER)
43     .onEvent(IRCEvent.Type.CTCP_SOURCE)
44     .onEvent(IRCEvent.Type.CTCP_PING)
45     .onEvent(IRCEvent.Type.CTCP_TIME)
46     .onEvent(IRCEvent.Type.CTCP_USERINFO)
47     .onEvent(IRCEvent.Type.CTCP_DCC)
48     .onEvent(IRCEvent.Type.CTCP_AVATAR)
49     .onEvent(IRCEvent.Type.CTCP_LAG)
50 )
51 void onCTCPs(CTCPService service, const ref IRCEvent event)
52 {
53     import kameloso.constants : KamelosoInfo;
54     import std.format : format;
55 
56     // https://modern.ircdocs.horse/ctcp.html
57 
58     string line;
59 
60     with (IRCEvent.Type)
61     switch (event.type)
62     {
63     case CTCP_VERSION:
64         import std.system : os;
65         /*  This metadata query is used to return the name and version of the
66             client software in use. There is no specified format for the version
67             string.
68 
69             VERSION is universally implemented. Clients MUST implement this CTCP message.
70 
71             Example:
72             Query:     VERSION
73             Response:  VERSION WeeChat 1.5-rc2 (git: v1.5-rc2-1-gc1441b1) (Apr 25 2016)
74          */
75 
76         enum pattern = "VERSION kameloso %s, built %s, running on %s";
77         line = pattern.format(cast(string)KamelosoInfo.version_, cast(string)KamelosoInfo.built, os);
78         break;
79 
80     case CTCP_FINGER:
81         /*  This metadata query returns miscellaneous info about the user,
82             typically the same information that’s held in their realname field.
83             However, some implementations return the client name and version instead.
84 
85             FINGER is widely implemented, but largely obsolete. Clients MAY
86             implement this CTCP message.
87 
88             Example:
89             Query:     FINGER
90             Response:  FINGER WeeChat 1.5
91          */
92         line = "FINGER kameloso " ~ cast(string)KamelosoInfo.version_;
93         break;
94 
95     case CTCP_SOURCE:
96         /*  This metadata query is used to return the location of the source
97             code for the client.
98 
99             SOURCE is rarely implemented. Clients MAY implement this CTCP message.
100 
101             Example:
102             Query:     SOURCE
103             Response:  SOURCE https://weechat.org/download
104          */
105         line = "SOURCE " ~ cast(string)KamelosoInfo.source;
106         break;
107 
108     case CTCP_PING:
109         /*  This extended query is used to confirm reachability with other
110             clients and to check latency. When receiving a CTCP PING, the reply
111             must contain exactly the same parameters as the original query.
112 
113             PING is universally implemented. Clients MUST implement this CTCP message.
114 
115             Example:
116             Query:     PING 1473523721 662865
117             Response:  PING 1473523721 662865
118 
119             Query:     PING foo bar baz
120             Response:  PING foo bar baz
121          */
122         line = "PING " ~ event.content;
123         break;
124 
125     case CTCP_TIME:
126         import std.datetime.systime : Clock;
127         /*  This extended query is used to return the client’s local time in an
128             unspecified human-readable format. We recommend ISO 8601 format, but
129             raw ctime() output appears to be the most common in practice.
130 
131             New implementations SHOULD default to UTC time for privacy reasons.
132 
133             TIME is almost universally implemented. Clients SHOULD implement
134             this CTCP message.
135 
136             Example:
137             Query:     TIME
138             Response:  TIME 2016-09-26T00:45:36Z
139          */
140         line = "TIME " ~ Clock.currTime.toUTC().toString();
141         break;
142 
143     /* case CTCP_CLIENTINFO:
144         // more complex; handled in own function
145         break; */
146 
147     case CTCP_USERINFO:
148         /*  This metadata query returns miscellaneous info about the user,
149             typically the same information that’s held in their realname field.
150 
151             However, some implementations return <nickname> (<realname>) instead.
152 
153             USERINFO is widely implemented, but largely obsolete. Clients MAY
154             implement this CTCP message.
155 
156             Example:
157             Query:     USERINFO
158             Response:  USERINFO fred (Fred Foobar)
159          */
160         enum pattern = "USERINFO %s (%s)";
161         line = pattern.format(service.state.client.nickname, service.state.client.realName);
162         break;
163 
164     case CTCP_DCC:
165         /*  DCC (Direct Client-to-Client) is used to setup and control
166             connections that go directly between clients, bypassing the IRC
167             server. This is typically used for features that require a large
168             amount of traffic between clients or simply wish to bypass the
169             server itself such as file transfer, direct chat, and voice messages.
170 
171             Properly implementing the various DCC types requires a document all
172             of its own, and are not described here.
173 
174             DCC is widely implemented. Clients MAY implement this CTCP message.
175          */
176         break;
177 
178     case CTCP_AVATAR:
179         /*  http://www.kvirc.net/doc/doc_ctcp_avatar.html
180 
181             Every IRC user has a client-side property called AVATAR.
182 
183             Let's say that there are two users: A and B.
184             When user A wants to see the B's avatar he simply sends a CTCP
185             AVATAR request to B (the request is sent through a PRIVMSG IRC
186             command). User B replies with a CTCP AVATAR notification (sent
187             through a NOTICE IRC command) with the name or URL of his avatar.
188 
189             The actual syntax for the notification is:
190 
191             AVATAR <avatar_file> [<filesize>]
192 
193             The <avatar_file> may be either the name of a B's local image file
194             or a URL pointing to an image on some web server.
195          */
196         // FIXME: return something hardcoded?
197         break;
198 
199     case CTCP_LAG:
200         // g-line fishing? do nothing?
201         break;
202 
203     default:
204         import lu.conv : Enum;
205         assert(0, "Missing `CTCP_` case entry for `IRCEvent.Type." ~
206             Enum!(IRCEvent.Type).toString(event.type) ~ '`');
207     }
208 
209     version(unittest)
210     {
211         return;
212     }
213     else
214     {
215         import dialect.common : I = IRCControlCharacter;
216 
217         enum properties = Message.Property.quiet;
218         enum pattern = "NOTICE %s :%c%s%2$c";
219         immutable target = event.sender.isServer ?
220             event.sender.address: event.sender.nickname;
221         immutable message = pattern.format(target, cast(char)I.ctcp, line);
222         raw(service.state, message, properties);
223     }
224 }
225 
226 unittest
227 {
228     // Ensure onCTCPs implement cases for all its annotated
229     // [dialect.defs.IRCEvent.Type|IRCEvent.Type]s.
230     import std.traits : getUDAs;
231 
232     IRCPluginState state;
233     auto service = new CTCPService(state);
234 
235     foreach (immutable type; getUDAs!(onCTCPs, IRCEventHandler)[0].acceptedEventTypes)
236     {
237         IRCEvent event;
238         event.type = type;
239         onCTCPs(service, event);
240     }
241 }
242 
243 
244 // onCTCPClientinfo
245 /++
246     Sends a list of which `CTCP` events we understand.
247 
248     This builds a string of the names of all [dialect.defs.IRCEvent.Type|IRCEvent.Type]s
249     that begin with `CTCP_`, at compile-time. As such, as long as we name any
250     new such types `CTCP_SOMETHING`, this list will always be correct.
251  +/
252 @(IRCEventHandler()
253     .onEvent(IRCEvent.Type.CTCP_CLIENTINFO)
254 )
255 void onCTCPClientinfo(CTCPService service, const ref IRCEvent event)
256 {
257     import dialect.common : I = IRCControlCharacter;
258     import std.format : format;
259 
260     /*  This metadata query returns a list of the CTCP messages that this
261         client supports and implements. CLIENTINFO is widely implemented.
262 
263         Clients SHOULD implement this CTCP message.
264 
265         Example:
266         Query:     CLIENTINFO
267         Response:  CLIENTINFO ACTION DCC CLIENTINFO FINGER PING SOURCE TIME USERINFO VERSION
268      */
269 
270     // Don't forget to add ACTION, it's handed elsewhere
271     enum responseSkeleton = "CLIENTINFO ACTION CLIENTINFO";
272 
273     enum allCTCPTypes = ()
274     {
275         import lu.string : beginsWith;
276         import std.array : Appender;
277         import std.traits : getUDAs;
278 
279         Appender!(char[]) sink;
280         sink.reserve(128);  // ~95
281         sink.put(responseSkeleton);
282 
283         foreach (sym; service.Introspection.allEventHandlerFunctionsInModule)
284         {
285             static foreach (immutable type; getUDAs!(sym, IRCEventHandler)[0].acceptedEventTypes)
286             {{
287                 import lu.conv : Enum;
288 
289                 enum typestring = Enum!(IRCEvent.Type).toString(type);
290 
291                 static if (typestring.beginsWith("CTCP_"))
292                 {
293                     sink.put(' ');
294                     sink.put(typestring[5..$]);
295                 }
296             }}
297         }
298 
299         return sink.data;
300     }().idup;
301 
302     static assert((allCTCPTypes.length > responseSkeleton.length),
303         "Concatenated CTCP type list is empty");
304 
305     enum pattern = "NOTICE %s :%c%s%2$c";
306     immutable message = pattern.format(event.sender.nickname, cast(char)I.ctcp, allCTCPTypes);
307     raw(service.state, message);
308 }
309 
310 
311 mixin PluginRegistration!(CTCPService, -20.priority);
312 
313 public:
314 
315 
316 // CTCPService
317 /++
318     The `CTCP` service (client-to-client protocol) answers to special queries
319     sometime made over the IRC protocol. These are generally of metadata about
320     the client itself and its capabilities.
321 
322     Information about these were gathered from the following sites:
323         - https://modern.ircdocs.horse/ctcp.html
324         - http://www.irchelp.org/protocol/ctcpspec.html
325  +/
326 final class CTCPService : IRCPlugin
327 {
328 private:
329     // isEnabled
330     /++
331         Override
332         [kameloso.plugins.common.core.IRCPlugin.isEnabled|IRCPlugin.isEnabled]
333         (effectively overriding [kameloso.plugins.common.core.IRCPluginImpl.isEnabled|IRCPluginImpl.isEnabled])
334         and inject a server check, so this service does nothing on Twitch servers.
335 
336         Returns:
337             `true` if this service should react to events; `false` if not.
338      +/
339     version(TwitchSupport)
340     override public bool isEnabled() const @property pure nothrow @nogc
341     {
342         return (state.server.daemon != IRCServer.Daemon.twitch);
343     }
344 
345     mixin IRCPluginImpl;
346 }