1 /++ 2 + IRCv3 tags capability support. 3 +/ 4 module virc.ircv3.tags; 5 6 7 import core.time : Duration, seconds; 8 import std.algorithm : filter, findSplit, map, splitter, startsWith; 9 import std.array : empty, front; 10 import std.datetime : msecs, SysTime, UTC; 11 import std.exception : enforce; 12 import std.meta : aliasSeqOf; 13 import std.range : dropOne, isInputRange, only; 14 import std.traits : isArray; 15 import std.typecons : Nullable; 16 import std.utf; 17 18 import virc.ircv3.batch; 19 /++ 20 + 21 +/ 22 struct IRCTags { 23 /// 24 string[string] tags; 25 alias tags this; 26 this(string tagString) @safe pure { 27 tags = parseTagString(tagString).tags; 28 } 29 this(string[string] inTags) @safe pure nothrow { 30 tags = inTags; 31 } 32 string toString() const @safe pure { 33 alias escape = replaceEscape!(string, only(`\`, `\\`), only(`;`, `\:`), only("\r", `\r`), only("\n", `\n`), only(" ", `\s`)); 34 import std.string : join; 35 string[] pieces; 36 foreach (key, value; tags) { 37 string piece = escape(key); 38 if (value != "") { 39 piece ~= "="~escape(value); 40 } 41 pieces ~= piece; 42 } 43 return pieces.join(";"); 44 } 45 bool empty() const @safe pure nothrow { 46 return tags.length == 0; 47 } 48 void reply(string id) @safe pure nothrow { 49 tags["+draft/reply"] = id; 50 } 51 void typing(TypingState state) @safe pure nothrow { 52 tags["+typing"] = cast(string)state; 53 } 54 void batch(string id) @safe pure nothrow { 55 tags["batch"] = id; 56 } 57 void multilineConcat() @safe pure nothrow { 58 tags["draft/multiline-concat"] = ""; 59 } 60 } 61 62 /// 63 @safe unittest { 64 { 65 const tags = IRCTags("a=b;x="); 66 assert(tags["a"] == "b"); 67 assert(tags["x"] == ""); 68 } 69 { 70 const tags = IRCTags(["a": "b", "x": ""]); 71 assert(tags["a"] == "b"); 72 assert(tags["x"] == ""); 73 } 74 { 75 const tags = IRCTags(["a": "\\"]); 76 assert(tags["a"] == "\\"); 77 //assert(tags.toString() == "a=\\\\"); 78 } 79 } 80 81 /++ 82 + 83 +/ 84 Nullable!bool booleanTag(string tag)(IRCTags tags) { 85 Nullable!bool output; 86 if (tag in tags) { 87 if (tags[tag] == "1") { 88 output = true; 89 } else if (tags[tag] == "0") { 90 output = false; 91 } //Other values treated as if tag not present 92 } 93 return output; 94 } 95 /// 96 @safe pure nothrow unittest { 97 assert(IRCTags(string[string].init).booleanTag!"test".isNull); 98 assert(IRCTags(["test": "aaaaa"]).booleanTag!"test".isNull); 99 assert(!IRCTags(["test": "0"]).booleanTag!"test".get); 100 assert(IRCTags(["test": "1"]).booleanTag!"test".get); 101 } 102 /++ 103 + 104 +/ 105 Nullable!string stringTag(string tag)(IRCTags tags) { 106 return typeTag!(tag, string)(tags); 107 } 108 /++ 109 + 110 +/ 111 Nullable!Type typeTag(string tag, Type)(IRCTags tags) { 112 import std.conv : to; 113 Nullable!Type output; 114 if (tag in tags) { 115 try { 116 output = tags[tag].to!Type; 117 } catch (Exception) {} //Act as if tag doesn't exist if malformed 118 } 119 return output; 120 } 121 /// 122 @safe pure nothrow unittest { 123 assert(IRCTags(string[string].init).typeTag!("test", uint).isNull); 124 assert(IRCTags(["test": "a"]).typeTag!("test", uint).isNull); 125 assert(IRCTags(["test": "0"]).typeTag!("test", uint) == 0); 126 assert(IRCTags(["test": "10"]).typeTag!("test", uint) == 10); 127 assert(IRCTags(["test": "words"]).typeTag!("test", string) == "words"); 128 assert(IRCTags(["test": "words"]).stringTag!"test" == "words"); 129 static struct Something { 130 char val; 131 this(string str) @safe pure nothrow { 132 val = str[0]; 133 } 134 } 135 assert(IRCTags(["test": "words"]).typeTag!("test", Something).get.val == 'w'); 136 } 137 /++ 138 + 139 +/ 140 auto arrayTag(string tag, string delimiter = ",", Type = string[])(IRCTags tags) if (isArray!Type){ 141 import std.algorithm : splitter; 142 import std.conv : to; 143 import std.range : ElementType; 144 Nullable!Type output; 145 if (tag in tags) { 146 auto split = tags[tag].splitter(delimiter); 147 output = []; 148 foreach (element; split) { 149 try { 150 output.get ~= element.to!(ElementType!Type); 151 } catch (Exception) { //Malformed, reset everything 152 output = output.init; 153 break; 154 } 155 } 156 } 157 return output; 158 } 159 /// 160 @safe pure nothrow unittest { 161 assert(IRCTags(string[string].init).arrayTag!("test").isNull); 162 assert(IRCTags(["test":""]).arrayTag!("test").get.empty); 163 assert(IRCTags(["test":"a"]).arrayTag!("test").get.front == "a"); 164 assert(IRCTags(["test":"a,b"]).arrayTag!("test") == ["a", "b"]); 165 assert(IRCTags(["test":"a:b"]).arrayTag!("test", ":") == ["a", "b"]); 166 assert(IRCTags(["test":"9,1"]).arrayTag!("test", ",", uint[]) == [9, 1]); 167 assert(IRCTags(["test":"9,a"]).arrayTag!("test", ",", uint[]).isNull); 168 } 169 /++ 170 + 171 +/ 172 Nullable!Duration secondDurationTag(string tag)(IRCTags tags) { 173 import std.conv : to; 174 Nullable!Duration output; 175 if (tag in tags) { 176 try { 177 output = tags[tag].to!long.seconds; 178 } catch (Exception) {} //Not a duration. Act like tag is nonexistent. 179 } 180 return output; 181 } 182 /// 183 @safe pure nothrow unittest { 184 import core.time : hours; 185 assert(IRCTags(string[string].init).secondDurationTag!("test").isNull); 186 assert(IRCTags(["test": "a"]).secondDurationTag!("test").isNull); 187 assert(IRCTags(["test": "3600"]).secondDurationTag!("test") == 1.hours); 188 } 189 /++ 190 + 191 +/ 192 auto parseTime(string[string] tags) { 193 enforce("time" in tags); 194 return SysTime.fromISOExtString(tags["time"], UTC()); 195 } 196 197 auto parseTagString(string input) { 198 import std.algorithm.comparison : among; 199 IRCTags output; 200 auto splitTags = input.splitter(";").filter!(a => !a.empty); 201 foreach (tag; splitTags) { 202 auto splitKV = tag.findSplit("="); 203 auto key = splitKV[0]; 204 if (!splitKV[1].empty) { 205 auto value = splitKV[2]; 206 if ((value.length > 0) && (value[$-1] == '\\')) { 207 value = value[0..$-1]; 208 } 209 if (value.length > 0) { 210 for (int i = 0; i < value.length-1; i++) { 211 if ((value[i] == '\\') && !value[i+1].among('\\', ':', 'r', 'n', 's')) { 212 value = value[0 .. i] ~ value[i +1 .. $]; 213 } 214 } 215 } 216 output[key] = value.replaceEscape!(string, only(`\\`, `\`), only(`\:`, `;`), only(`\r`, "\r"), only(`\n`, "\n"), only(`\s`, " ")); 217 } else { 218 output[key] = ""; 219 } 220 } 221 return output; 222 } 223 224 /// 225 @safe pure /+nothrow @nogc+/ unittest { 226 //Example from http://ircv3.net/specs/core/message-tags-3.2.html 227 { 228 immutable tags = parseTagString(""); 229 assert(tags.length == 0); 230 } 231 //ditto 232 { 233 immutable tags = parseTagString("aaa=bbb;ccc;example.com/ddd=eee"); 234 assert(tags.length == 3); 235 assert(tags["aaa"] == "bbb"); 236 assert(tags["ccc"] == ""); 237 assert(tags["example.com/ddd"] == "eee"); 238 } 239 //escape test 240 { 241 immutable tags = parseTagString(`whatevs=\\s`); 242 assert(tags.length == 1); 243 assert("whatevs" in tags); 244 assert(tags["whatevs"] == `\s`); 245 } 246 //Example from http://ircv3.net/specs/extensions/batch-3.2.html 247 { 248 immutable tags = parseTagString(`batch=yXNAbvnRHTRBv`); 249 assert(tags.length == 1); 250 assert("batch" in tags); 251 assert(tags["batch"] == "yXNAbvnRHTRBv"); 252 } 253 //Example from http://ircv3.net/specs/extensions/account-tag-3.2.html 254 { 255 immutable tags = parseTagString(`account=hax0r`); 256 assert(tags.length == 1); 257 assert("account" in tags); 258 assert(tags["account"] == "hax0r"); 259 } 260 { 261 immutable tags = parseTagString(`testk=test\`); 262 assert("testk" in tags); 263 assert(tags["testk"] == "test"); 264 } 265 } 266 /// 267 @safe /+pure nothrow @nogc+/ unittest { 268 import std.datetime : DateTime, msecs, SysTime, UTC; 269 //Example from http://ircv3.net/specs/extensions/server-time-3.2.html 270 { 271 auto tags = parseTagString("time=2011-10-19T16:40:51.620Z"); 272 assert(tags.length == 1); 273 assert("time" in tags); 274 assert(tags["time"] == "2011-10-19T16:40:51.620Z"); 275 immutable testTime = SysTime(DateTime(2011,10,19,16,40,51), 620.msecs, UTC()); 276 assert(parseTime(tags) == testTime); 277 } 278 //ditto 279 { 280 immutable tags = parseTagString("time=2012-06-30T23:59:60.419Z"); 281 assert(tags.length == 1); 282 assert("time" in tags); 283 assert(tags["time"] == "2012-06-30T23:59:60.419Z"); 284 //leap seconds not currently representable 285 //assert(parseTime(splitStr.tags) == SysTime(DateTime(2012,06,30,23,59,60), 419.msecs, UTC())); 286 } 287 } 288 /++ 289 + 290 +/ 291 T replaceEscape(T, replacements...)(T input) { 292 static if (replacements.length == 0) { 293 return input; 294 } else { 295 T output; 296 enum findStrs = aliasSeqOf!([replacements].map!((x) => x[0].byCodeUnit)); 297 for (size_t position = 0; position < input.length; position++) { 298 sw: final switch(input[position..$].byCodeUnit.startsWith(findStrs)) { 299 case 0: 300 output ~= input[position]; 301 break; 302 static foreach (index, replacement; replacements) { 303 static assert(replacements[index][0].length >= 1); 304 case index+1: 305 output ~= replacements[index][1]; 306 position += replacements[index][0].length-1; 307 break sw; 308 } 309 } 310 } 311 return output; 312 } 313 } 314 /// 315 @safe pure nothrow unittest { 316 assert(replaceEscape("") == ""); 317 assert(replaceEscape!(string, only("a", "b"))("a") == "b"); 318 assert(replaceEscape!(string, only("a", "b"), only("aa", "b"))("aa") == "bb"); 319 } 320 321 enum TypingState { 322 active = "active", 323 paused = "paused", 324 done = "done", 325 }