1 /++ 2 + Module for parsing IRC mode strings. 3 +/ 4 module virc.modes; 5 6 import std.algorithm : among, splitter; 7 import std.range.primitives : isInputRange, isOutputRange; 8 import std.range : put; 9 import std.typecons : Nullable, Tuple; 10 11 12 /++ 13 + IRC modes. These are settings for channels and users on an IRC network, 14 + responsible for things ranging from user bans, flood control and colour 15 + stripping to registration status. 16 + Consists of a single character and (often) an argument string to go along 17 + with it. 18 +/ 19 struct Mode { 20 /// 21 ModeType type = ModeType.d; 22 /// 23 char mode; 24 /// 25 Nullable!string arg; 26 invariant() { 27 assert((type != ModeType.d) || arg.isNull); 28 } 29 /// 30 auto opEquals(Mode b) const { 31 return (mode == b.mode); 32 } 33 /// 34 auto toHash() const { 35 return mode.hashOf; 36 } 37 } 38 @safe pure nothrow /+@nogc+/ unittest { 39 assert(Mode(ModeType.d, 'a').toHash == Mode(ModeType.d, 'a').toHash); 40 } 41 /++ 42 + Mode classification. 43 +/ 44 enum ModeType { 45 ///Adds/removes nick/address to a list. always has a parameter. 46 a, 47 ///Mode that changes a setting and always has a parameter. 48 b, 49 ///Mode that changes a setting and only has a parameter when set. 50 c , 51 ///Mode that changes a setting and never has a parameter. 52 d 53 } 54 /++ 55 + Whether a mode was set or unset. 56 +/ 57 enum Change { 58 ///Mode was set. 59 set, 60 ///Mode was unset. 61 unset 62 } 63 64 /++ 65 + Full metadata associated with a mode change. 66 +/ 67 struct ModeChange { 68 /// 69 Mode mode; 70 /// 71 Change change; 72 void toString(T)(T sink) const if (isOutputRange!(T, const(char))) { 73 final switch(change) { 74 case Change.set: 75 put(sink, '+'); 76 break; 77 case Change.unset: 78 put(sink, '-'); 79 break; 80 } 81 put(sink, mode.mode); 82 } 83 } 84 85 /++ 86 + Parse a mode string into individual mode changes. 87 +/ 88 auto parseModeString(T)(T input, ModeType[char] channelModeTypes) if (isInputRange!T) { 89 ModeChange[] changes; 90 bool unsetMode = false; 91 auto modeList = input.front; 92 input.popFront(); 93 foreach (mode; modeList) { 94 if (mode == '+') { 95 unsetMode = false; 96 } else if (mode == '-') { 97 unsetMode = true; 98 } else { 99 if (unsetMode) { 100 auto modeType = mode in channelModeTypes ? channelModeTypes[mode] : ModeType.d; 101 if (modeType.among(ModeType.a, ModeType.b)) { 102 if (input.empty) { 103 changes = []; 104 break; 105 } 106 auto arg = input.front; 107 input.popFront(); 108 changes ~= ModeChange(Mode(modeType, mode, Nullable!string(arg)), Change.unset); 109 } else { 110 changes ~= ModeChange(Mode(modeType, mode), Change.unset); 111 } 112 } else { 113 auto modeType = mode in channelModeTypes ? channelModeTypes[mode] : ModeType.d; 114 if (modeType.among(ModeType.a, ModeType.b, ModeType.c)) { 115 if (input.empty) { 116 changes = []; 117 break; 118 } 119 auto arg = input.front; 120 input.popFront(); 121 changes ~= ModeChange(Mode(modeType, mode, Nullable!string(arg)), Change.set); 122 } else { 123 changes ~= ModeChange(Mode(modeType, mode), Change.set); 124 } 125 } 126 } 127 } 128 return changes; 129 } 130 ///ditto 131 auto parseModeString(string input, ModeType[char] channelModeTypes) { 132 return parseModeString(input.splitter(" "), channelModeTypes); 133 } 134 /// 135 @safe pure nothrow unittest { 136 import std.algorithm : canFind, filter, map; 137 import std.range : empty; 138 { 139 const testParsed = parseModeString("+s", null); 140 assert(testParsed.filter!(x => x.change == Change.set).map!(x => x.mode).canFind(Mode(ModeType.d, 's'))); 141 assert(testParsed.filter!(x => x.change == Change.unset).empty); 142 } 143 { 144 const testParsed = parseModeString("-s", null); 145 assert(testParsed.filter!(x => x.change == Change.set).empty); 146 assert(testParsed.filter!(x => x.change == Change.unset).map!(x => x.mode).canFind(Mode(ModeType.d, 's'))); 147 } 148 { 149 const testParsed = parseModeString("+s-n", null); 150 assert(testParsed.filter!(x => x.change == Change.set).map!(x => x.mode).canFind(Mode(ModeType.d, 's'))); 151 assert(testParsed.filter!(x => x.change == Change.unset).map!(x => x.mode).canFind(Mode(ModeType.d, 'n'))); 152 } 153 { 154 const testParsed = parseModeString("-s+n", null); 155 assert(testParsed.filter!(x => x.change == Change.set).map!(x => x.mode).canFind(Mode(ModeType.d, 'n'))); 156 assert(testParsed.filter!(x => x.change == Change.unset).map!(x => x.mode).canFind(Mode(ModeType.d, 's'))); 157 } 158 { 159 const testParsed = parseModeString("+kp secret", ['k': ModeType.b]); 160 assert(testParsed.filter!(x => x.change == Change.set).map!(x => x.mode).canFind(Mode(ModeType.d, 'p'))); 161 assert(testParsed.filter!(x => x.change == Change.set).map!(x => x.mode).canFind(Mode(ModeType.b, 'k', Nullable!string("secret")))); 162 } 163 { 164 const testParsed = parseModeString("+kp secret", null); 165 assert(testParsed.filter!(x => x.change == Change.set).map!(x => x.mode).canFind(Mode(ModeType.d, 'p'))); 166 assert(testParsed.filter!(x => x.change == Change.set).map!(x => x.mode).canFind(Mode(ModeType.d, 'k'))); 167 } 168 { 169 const testParsed = parseModeString("-s+nk secret", ['k': ModeType.b]); 170 assert(testParsed.filter!(x => x.change == Change.set).map!(x => x.mode).canFind(Mode(ModeType.d, 'n'))); 171 assert(testParsed.filter!(x => x.change == Change.set).map!(x => x.mode).canFind(Mode(ModeType.b, 'k', Nullable!string("secret")))); 172 assert(testParsed.filter!(x => x.change == Change.unset).map!(x => x.mode).canFind(Mode(ModeType.d, 's'))); 173 } 174 { 175 const testParsed = parseModeString("-sk+nl secret 4", ['k': ModeType.b, 'l': ModeType.c]); 176 assert(testParsed.filter!(x => x.change == Change.set).map!(x => x.mode).canFind(Mode(ModeType.d, 'n'))); 177 assert(testParsed.filter!(x => x.change == Change.set).map!(x => x.mode).canFind(Mode(ModeType.b, 'l', Nullable!string("4")))); 178 assert(testParsed.filter!(x => x.change == Change.unset).map!(x => x.mode).canFind(Mode(ModeType.b, 'k', Nullable!string("secret")))); 179 assert(testParsed.filter!(x => x.change == Change.unset).map!(x => x.mode).canFind(Mode(ModeType.d, 's'))); 180 } 181 { 182 const testParsed = parseModeString("-s+nl 3333", ['l': ModeType.c]); 183 assert(testParsed.filter!(x => x.change == Change.set).map!(x => x.mode).canFind(Mode(ModeType.d, 'n'))); 184 assert(testParsed.filter!(x => x.change == Change.set).map!(x => x.mode).canFind(Mode(ModeType.c, 'l', Nullable!string("3333")))); 185 assert(testParsed.filter!(x => x.change == Change.unset).map!(x => x.mode).canFind(Mode(ModeType.d, 's'))); 186 } 187 { 188 const testParsed = parseModeString("+s-nl", ['l': ModeType.c]); 189 assert(testParsed.filter!(x => x.change == Change.unset).map!(x => x.mode).canFind(Mode(ModeType.d, 'n'))); 190 assert(testParsed.filter!(x => x.change == Change.unset).map!(x => x.mode).canFind(Mode(ModeType.c, 'l'))); 191 assert(testParsed.filter!(x => x.change == Change.set).map!(x => x.mode).canFind(Mode(ModeType.d, 's'))); 192 } 193 { 194 const testParsed = parseModeString("+kp", ['k': ModeType.b]); 195 assert(testParsed.empty); 196 } 197 { 198 const testParsed = parseModeString("-kp", ['k': ModeType.b]); 199 assert(testParsed.empty); 200 } 201 } 202 203 auto toModeStringLazy(ModeChange[] changes) { 204 static struct Result { 205 ModeChange[] changes; 206 void toString(S)(ref S sink) { 207 import std.algorithm : joiner, map; 208 import std.format : formattedWrite; 209 import std.range : put; 210 Nullable!Change last; 211 foreach(change; changes) { 212 if (last.isNull || (change.change != last)) { 213 put(sink, change.change == Change.set ? '+' : '-'); 214 last = change.change; 215 } 216 put(sink, change.mode.mode); 217 } 218 sink.formattedWrite!"%-( %s%)"(changes.map!(x => x.mode.arg).joiner); 219 } 220 } 221 return Result(changes); 222 } 223 224 @safe pure unittest { 225 import std.conv : text; 226 assert(toModeStringLazy([ModeChange(Mode(ModeType.d, 's'), Change.set)]).text == "+s"); 227 assert(toModeStringLazy([ModeChange(Mode(ModeType.d, 's'), Change.unset)]).text == "-s"); 228 assert(toModeStringLazy([ModeChange(Mode(ModeType.d, 's'), Change.set), ModeChange(Mode(ModeType.d, 's'), Change.unset)]).text == "+s-s"); 229 assert(toModeStringLazy([ModeChange(Mode(ModeType.d, 's'), Change.set), ModeChange(Mode(ModeType.b, 'k', Nullable!string("pass")), Change.set)]).text == "+sk pass"); 230 assert(toModeStringLazy([ModeChange(Mode(ModeType.d, 's'), Change.set), ModeChange(Mode(ModeType.b, 'k', Nullable!string("pass")), Change.set), ModeChange(Mode(ModeType.c, 'l', Nullable!string("3")), Change.set)]).text == "+skl pass 3"); 231 } 232 233 string toModeString(ModeChange[] changes) @safe pure { 234 import std.conv : text; 235 return changes.toModeStringLazy().text; 236 }