1 /++
2 + Module for IRCv3 core features.
3 +/
4 module virc.ircv3.base;
5 
6 /++
7 +
8 +/
9 enum IRCV3Commands {
10 	cap = "CAP",
11 	metadata = "METADATA",
12 	authenticate = "AUTHENTICATE",
13 	account = "ACCOUNT",
14 	starttls = "STARTTLS",
15 	monitor = "MONITOR",
16 	batch = "BATCH",
17 	chghost = "CHGHOST",
18 	fail = "FAIL",
19 	warn = "WARN",
20 	note = "NOTE",
21 	tagmsg = "TAGMSG",
22 }
23 
24 /++
25 +
26 +/
27 enum CapabilityServerSubcommands {
28 	ls = "LS",
29 	acknowledge = "ACK",
30 	notAcknowledge = "NAK",
31 	list = "LIST",
32 	new_ = "NEW",
33 	delete_ = "DEL"
34 }
35 
36 /++
37 +
38 +/
39 enum CapabilityClientSubcommands {
40 	ls = "LS",
41 	list = "LIST",
42 	request = "REQ",
43 	end = "END"
44 }
45 
46 /++
47 +
48 +/
49 struct Capability {
50 	///
51 	string name;
52 	///DEPRECATED - indicates that the capability cannot be disabled
53 	bool isSticky;
54 	///Indicates that the capability is being disabled
55 	bool isDisabled;
56 	///DEPRECATED - indicates that the client must acknowledge this cap via CAP ACK
57 	bool mustAck;
58 	///
59 	string value;
60 
61 	alias name this;
62 
63 	@disable this();
64 
65 	///
66 	this(string str) @safe pure @nogc in {
67 		import std.range : empty;
68 		assert(!str.empty);
69 	} do {
70 		import std.algorithm.comparison : among;
71 		import std.algorithm.searching : findSplit;
72 		import std.range : front, popFront;
73 		import std.utf : byCodeUnit;
74 		auto prefix = str.byCodeUnit.front;
75 		setFlagsByPrefix(prefix);
76 		if (prefix.among('~', '=', '-')) {
77 			str.popFront();
78 		}
79 		auto split = str.findSplit("=");
80 		this(split[0], split[2]);
81 	}
82 	///
83 	this(string key, string val) @safe pure @nogc nothrow {
84 		name = key;
85 		value = val;
86 	}
87 	/++
88 	+ Indicates whether or not this is a vendor-specific capability.
89 	+
90 	+ Often these are draft implementations of not-yet-accepted capabilities.
91 	+/
92 	bool isVendorSpecific() @safe pure @nogc nothrow {
93 		import std.algorithm.searching : canFind;
94 		import std.utf : byCodeUnit;
95 		return name.byCodeUnit.canFind('/');
96 	}
97 	///
98 	string toString() const pure @safe {
99 		import std.range : empty;
100 		return name~(value.empty ? "" : "=")~value;
101 	}
102 	private void setFlagsByPrefix(char chr) @safe pure @nogc nothrow {
103 		switch (chr) {
104 			case '~':
105 				mustAck = true;
106 				break;
107 			case '=':
108 				isSticky = true;
109 				break;
110 			case '-':
111 				isDisabled = true;
112 				break;
113 			default:
114 				break;
115 		}
116 	}
117 }
118 ///
119 @safe pure @nogc unittest {
120 	{
121 		auto cap = Capability("~account-notify");
122 		assert(cap.name == "account-notify");
123 		assert(cap.mustAck);
124 	}
125 	{
126 		auto cap = Capability("=account-notify");
127 		assert(cap.name == "account-notify");
128 		assert(cap.isSticky);
129 	}
130 	{
131 		auto cap = Capability("-account-notify");
132 		assert(cap.name == "account-notify");
133 		assert(cap.isDisabled);
134 	}
135 	{
136 		auto cap = Capability("example.com/cap");
137 		assert(cap.name == "example.com/cap");
138 		assert(cap.isVendorSpecific);
139 	}
140 	{
141 		auto cap = Capability("cap=value");
142 		assert(cap.name == "cap");
143 		assert(cap.value == "value");
144 	}
145 	{
146 		auto cap = Capability("cap=value/notvendor");
147 		assert(cap.name == "cap");
148 		assert(cap.value == "value/notvendor");
149 		assert(!cap.isVendorSpecific);
150 	}
151 }