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 }