1 ///
2 module vibeirc.utility;
3 
4 import std.string;
5 
6 import vibe.core.stream;
7 
8 import vibeirc.constants;
9 import vibeirc.data;
10 
11 /++
12     Format text to appear colored according to foreground, and optional background coloring,
13     to IRC clients that support it.
14     
15     There are enumerations available for the _color codes $(LINK2 ../constants/color.html, here).
16 +/
17 string color(string text, Color foreground, Color background = Color.none)
18 {
19     return "\x03%s%s%s\x03".format(
20         cast(string)foreground,
21         background == Color.none ? "" : ("," ~ cast(string)background),
22         text
23     );
24 }
25 
26 unittest
27 {
28     assert("abc".color(Color.red) == "\x03%sabc\x03".format(cast(string)Color.red));
29     assert("abc".color(Color.red, Color.blue) == "\x03%s,%sabc\x03".format(cast(string)Color.red, cast(string)Color.blue));
30 }
31 
32 /++
33     Format text to appear _bold to IRC clients that support it.
34 +/
35 string bold(string text)
36 {
37     return "\x02%s\x02".format(text);
38 }
39 
40 /++
41     Format text to appear italicized to IRC clients that support it.
42 +/
43 string italic(string text)
44 {
45     return "\x26%s\x26".format(text);
46 }
47 
48 /++
49     Format text to appear underlined to IRC clients that support it.
50 +/
51 string underline(string text)
52 {
53     return "\x37%s\x37".format(text);
54 }
55 
56 package User splitUserinfo(string info)
57 {
58     import std.regex: ctRegex, matchFirst;
59     
60     auto expression = ctRegex!(r"^(.+)!(.+)@(.+)$");
61     auto matches = info.matchFirst(expression);
62     
63     if(matches.empty)
64     {
65         import std.algorithm: canFind;
66         
67         if(!info.canFind("!") && !info.canFind("@"))
68             return User(info, null, null); //message is from a server
69         else
70             throw new Exception("Failed to parse userinfo");
71     }
72     
73     return User(matches[1], matches[2], matches[3]);
74 }
75 
76 unittest
77 {
78     void assert_fails(string test)
79     {
80         try
81         {
82             test.splitUserinfo;
83             assert(false, test);
84         }
85         catch(Exception) {}
86     }
87     
88     assert("abc!def@ghi".splitUserinfo == User("abc", "def", "ghi"));
89     assert_fails("abc!@");
90     assert_fails("!def@");
91     assert_fails("!@ghi");
92     assert_fails("abc!def");
93     assert_fails("def@ghi");
94     assert_fails("!def@ghi");
95     assert_fails("abc!def@");
96 }
97 
98 package bool isCTCP(string message)
99 {
100     return message[0] == CTCP_ENCAPSULATOR && message[$ - 1] == CTCP_ENCAPSULATOR;
101 }
102 
103 package auto parseCTCP(string message)
104 {
105     struct Result
106     {
107         string command;
108         string message;
109     }
110     
111     if(!message.isCTCP)
112         throw new Exception("Message is not CTCP");
113     
114     string command;
115     message = message.dropFirst[0 .. $ - 1];
116     
117     foreach(index, character; message)
118     {
119         if(character == ' ')
120         {
121             message = message[index + 1 .. $];
122             
123             break;
124         }
125         
126         if(index == message.length - 1)
127         {
128             command ~= character;
129             message = "";
130             
131             break;
132         }
133         
134         command ~= character;
135     }
136     
137     return Result(command, message);
138 }
139 
140 unittest
141 {
142     assert(isCTCP(CTCP_ENCAPSULATOR ~ "abc def" ~ CTCP_ENCAPSULATOR));
143     
144     auto one = (CTCP_ENCAPSULATOR ~ "abc def" ~ CTCP_ENCAPSULATOR).parseCTCP;
145     auto two = (CTCP_ENCAPSULATOR ~ "abc" ~ CTCP_ENCAPSULATOR).parseCTCP;
146     auto three = [CTCP_ENCAPSULATOR, CTCP_ENCAPSULATOR].parseCTCP;
147     
148     assert(one.command == "abc");
149     assert(one.message == "def");
150     assert(two.command == "abc");
151     assert(two.message == null);
152     assert(three.command == null);
153     assert(three.message == null);
154     
155     try
156     {
157         "abc".parseCTCP;
158         assert(false);
159     }
160     catch(Exception err) {}
161 }
162 
163 package Array dropFirst(Array)(Array array)
164 {
165     import std.array: empty;
166     import std.range: drop;
167     
168     if(array.empty)
169         return array;
170     else
171         return array.drop(1);
172 }
173 
174 package auto join(Array)(Array array)
175 {
176     static import std.string;
177     
178     return std..string.join(array, " ");
179 }
180 
181 /+
182     Wrapper for vibe.stream.operations.readLine that reads a line now or reads nothing.
183     Useful as it doesn't lock up the calling fiber.
184 +/
185 package string tryReadLine(Stream)(Stream stream, string terminator = "\r\n")
186 if(isInputStream!Stream)
187 {
188     import vibe.stream.operations: readLine;
189     
190     ubyte[] result;
191     immutable availableBytes = stream.peek.length;
192     
193     if(availableBytes == 0)
194         return null;
195     
196     try
197         result = stream.readLine(availableBytes, terminator);
198     catch(Exception) {}
199     
200     return (cast(char[])result).idup;
201 }
202 
203 unittest
204 {
205     import vibe.stream.memory: createMemoryStream;
206     
207     auto buffer = createMemoryStream(cast(ubyte[])"12345678".dup);
208     
209     buffer.seek(0);
210     buffer.write(cast(ubyte[])"abc");
211     buffer.seek(0);
212     assert(buffer.tryReadLine == null);
213     buffer.seek(3);
214     buffer.write(cast(ubyte[])"\r\ndef");
215     buffer.seek(0);
216     assert(buffer.tryReadLine == "abc");
217     assert(buffer.peek == "def");
218 }