1 module matrix;
2 import std.json;
3 import std.format : format;
4 import std..string;
5 import std.net.curl : HTTP, CurlCode, ThrowOnError;
6 import std.conv;
7 import std.range;
8 
9 class MatrixClient
10 {
11 private:
12 	static const string[string] NULL_PARAMS;
13 public:
14 	uint transactionId;
15 	string nextBatch;
16 
17 	string buildUrl(string endpoint, const string[string] params = NULL_PARAMS,
18 			string apiVersion = "unstable", string section = "client")
19 	{
20 		string url = "%s/_matrix/%s/%s/%s".format(this.homeserver, section, apiVersion, endpoint);
21 		char concat = '?';
22 
23 		if (this.accessToken.length)
24 		{
25 			url ~= "%caccess_token=%s".format(concat, this.accessToken);
26 			concat = '&';
27 		}
28 
29 		string paramString = this.makeParamString(params, concat);
30 		if (paramString.length)
31 			url ~= paramString;
32 
33 		return url;
34 	}
35 
36 	string makeParamString(const string[string] params, char concat)
37 	{
38 		if (params.length == 0)
39 		{
40 			return "";
41 		}
42 		string result = "%s".format(concat);
43 		foreach (key, value; params)
44 		{
45 			result ~= "%s=%s&".format(key, value);
46 		}
47 		return result[0 .. $ - 1];
48 	}
49 
50 	string translateRoomId(string roomId)
51 	{
52 		return translate(roomId, ['#': "%23", ':': "%3A"]);
53 	}
54 
55 	JSONValue makeHttpRequest(string method)(string url,
56 			JSONValue data = JSONValue(), HTTP http = HTTP())
57 	{
58 		http.url(url);
59 		JSONValue returnbody;
60 		string returnstr = "";
61 
62 		static if (method == "GET")
63 			http.method(HTTP.Method.get);
64 		else static if (method == "POST")
65 			http.method(HTTP.Method.post);
66 		else static if (method == "PUT")
67 		{
68 			// Using the HTTP struct with PUT seems to hang, don't use it
69 			http.method(HTTP.Method.put);
70 		}
71 		else static if (method == "DELETE")
72 			http.method(HTTP.Method.del);
73 
74 		//import std.stdio;
75 		//writeln(method ~ " " ~ url);
76 		//writeln(data.toString);
77 
78 		if (!data.isNull)
79 			http.postData(data.toString);
80 		http.onReceive = (ubyte[] data) {
81 			returnstr ~= cast(string) data;
82 			return data.length;
83 		};
84 		//http.verbose(true);
85 		CurlCode c = http.perform(ThrowOnError.no);
86 		//writeln(c);
87 		//writeln(returnstr);
88 		returnbody = parseJSON(returnstr);
89 		if (c)
90 		{
91 			throw new MatrixException(c, returnbody);
92 		}
93 		return returnbody;
94 	}
95 
96 	JSONValue get(string url)
97 	{
98 		return makeHttpRequest!("GET")(url);
99 	}
100 
101 	JSONValue post(string url, JSONValue data = JSONValue())
102 	{
103 		return makeHttpRequest!("POST")(url, data);
104 	}
105 
106 	JSONValue put(string url, JSONValue data = JSONValue())
107 	{
108 		// Using the HTTP struct with PUT seems to hang
109 		// return makeHttpRequest!("PUT")(url, data);
110 
111 		// std.net.curl.put works fine
112 		import std.net.curl : cput = put;
113 
114 		return parseJSON(cput(url, data.toString()));
115 	}
116 
117 	string homeserver, user_id, accessToken;
118 	bool useNotice = true;
119 
120 	string getTextMessageType()
121 	{
122 		return useNotice ? "m.notice" : "m.text";
123 	}
124 
125 	this(string homeserver = "https://matrix.org")
126 	{
127 		this.homeserver = homeserver;
128 		// Check well known matrix
129 	}
130 
131 	void login(string user, string password)
132 	{
133 		string url = buildUrl("login");
134 		JSONValue req = JSONValue();
135 		req["type"] = "m.login.password";
136 		req["user"] = user;
137 		req["password"] = password;
138 
139 		JSONValue resp = post(url, req);
140 
141 		this.accessToken = resp["access_token"].str;
142 		this.user_id = resp["user_id"].str;
143 	}
144 
145 	string[] getJoinedRooms()
146 	{
147 		string url = buildUrl("joined_rooms");
148 
149 		JSONValue result = get(url);
150 
151 		// TODO: Find a better way to do this 💀
152 		string[] rooms = [];
153 		foreach (r; result["joined_rooms"].array)
154 		{
155 			rooms ~= r.str;
156 		}
157 		return rooms;
158 	}
159 
160 	void joinRoom(string roomId, JSONValue thirdPartySigned = JSONValue())
161 	{
162 		// Why the hell are there 2 endpoints that do the *exact* same thing 
163 		string url = buildUrl("join/%s".format(translateRoomId(roomId)));
164 
165 		post(url);
166 	}
167 
168 	void sync()
169 	{
170 		import std.stdio;
171 
172 		string[string] params;
173 		if (nextBatch)
174 			params["since"] = nextBatch;
175 
176 		string url = buildUrl("sync", params);
177 
178 		JSONValue response = get(url);
179 
180 		nextBatch = response["next_batch"].str;
181 		if ("rooms" in response)
182 		{
183 			JSONValue rooms = response["rooms"];
184 
185 			if ("invite" in rooms)
186 			{
187 				JSONValue invites = rooms["invite"];
188 
189 				// I hate JSON dictionaries
190 				foreach (inv; invites.object.keys)
191 				{
192 					if (inviteDelegate)
193 						inviteDelegate(inv, invites[inv]["invite_state"]["events"][0]["sender"].str);
194 				}
195 
196 			}
197 
198 			if ("join" in rooms)
199 			{
200 				foreach (roomId; rooms["join"].object.keys)
201 				{
202 					if ("timeline" in rooms["join"][roomId])
203 					{
204 						if ("events" in rooms["join"][roomId]["timeline"])
205 						{
206 							foreach (ev; rooms["join"][roomId]["timeline"]["events"].array)
207 							{
208 								switch (ev["type"].str)
209 								{
210 									// New message
211 								case "m.room.message":
212 									auto content = ev["content"];
213 									if (!("msgtype" in content))
214 										break;
215 									string msgtype = ev["content"]["msgtype"].str;
216 									switch (msgtype)
217 									{
218 									case "m.text":
219 									case "m.notice":
220 										if (messageDelegate)
221 										{
222 											MatrixTextMessage text = new MatrixTextMessage();
223 
224 											text.roomId = roomId;
225 											text.type = msgtype;
226 											text.age = ev["unsigned"]["age"].integer;
227 											text.author = ev["sender"].str;
228 											text.eventId = ev["event_id"].str;
229 
230 											if ("body" in content)
231 												text.content = content["body"].str;
232 											if ("format" in content)
233 												text.format = content["format"].str;
234 											if ("formatted_body" in content)
235 												text.formattedContent
236 													= content["formatted_body"].str;
237 
238 											messageDelegate(text);
239 										}
240 										break;
241 
242 										// TODO
243 									default:
244 									case "m.file":
245 									case "m.image":
246 									case "m.audio":
247 									case "m.video":
248 										if (messageDelegate)
249 										{
250 											MatrixMessage msg = new MatrixMessage();
251 
252 											msg.roomId = roomId;
253 											msg.type = msgtype;
254 											msg.age = ev["unsigned"]["age"].integer;
255 											msg.author = ev["sender"].str;
256 											msg.eventId = ev["event_id"].str;
257 
258 											messageDelegate(msg);
259 										}
260 									}
261 
262 									break;
263 									// Membership change
264 								case "m.room.member":
265 									break;
266 								default:
267 									break;
268 								}
269 							}
270 						}
271 					}
272 				}
273 			}
274 		}
275 	}
276 
277 	void markRead(string roomId, string eventId)
278 	{
279 		string url = buildUrl("rooms/%s/read_markers".format(translateRoomId(roomId)));
280 
281 		JSONValue req = JSONValue();
282 		req["m.fully_read"] = eventId;
283 		req["m.read"] = eventId;
284 
285 		post(url, req);
286 	}
287 
288 	void delegate(MatrixMessage) messageDelegate;
289 	void delegate(string, string) inviteDelegate;
290 
291 	void sendHTML(string roomId, string html, string fallback = null)
292 	{
293 		string url = buildUrl("rooms/%s/send/m.room.message/%d".format(translateRoomId(roomId),
294 				transactionId));
295 
296 		if(!fallback) fallback = html;
297 		JSONValue req = JSONValue();
298 		req["msgtype"] = getTextMessageType();
299 		req["format"] = "org.matrix.custom.html";
300 		req["formatted_body"] = html;
301 		req["body"] = fallback;
302 
303 		put(url, req);
304 
305 		transactionId++;
306 	}
307 
308 	void sendString(string roomId, string text)
309 	{
310 		string url = buildUrl("rooms/%s/send/m.room.message/%d".format(translateRoomId(roomId),
311 				transactionId));
312 
313 		JSONValue req = JSONValue();
314 		req["msgtype"] = getTextMessageType();
315 		req["body"] = text;
316 
317 		put(url, req);
318 
319 		transactionId++;
320 	}
321 
322 	void sendFile(string roomId, string filename, string mxc, string msgtype = "m.file")
323 	{
324 		string url = buildUrl("rooms/%s/send/m.room.message/%d".format(translateRoomId(roomId),
325 				transactionId));
326 
327 		JSONValue req = JSONValue();
328 		req["msgtype"] = msgtype;
329 		req["url"] = mxc;
330 		req["body"] = filename;
331 
332 		put(url, req);
333 
334 		transactionId++;
335 	}
336 
337 	void sendImage(string roomId, string filename, string mxc)
338 	{
339 		sendFile(roomId, "m.image", filename, mxc);
340 	}
341 
342 	string uploadFile(const void[] data, string filename, string mimetype)
343 	{
344 		string[string] params = ["filename" : filename];
345 		string url = buildUrl("upload", params, "r0", "media");
346 
347 		// TODO: Ratelimits
348 		HTTP http = HTTP();
349 		http.postData(data);
350 		http.addRequestHeader("Content-Type", mimetype);
351 		JSONValue resp = makeHttpRequest!("POST")(url, JSONValue(), http);
352 
353 		return resp["content_uri"].str;
354 	}
355 
356 	string resolveRoomAlias(string roomalias)
357 	{
358 		string url = buildUrl("directory/room/%s".format(translate(roomalias,
359 				['#': "%23", ':': "%3A"])));
360 
361 		JSONValue resp = get(url);
362 
363 		return resp["room_id"].str;
364 	}
365 }
366 
367 class MatrixException : Exception
368 {
369 	string errcode, error;
370 	int statuscode;
371 	this(int statuscode, JSONValue json)
372 	{
373 		this.statuscode = statuscode;
374 		if ("errcode" in json)
375 			errcode = json["errcode"].str;
376 		if ("error" in json)
377 			error = json["error"].str;
378 
379 		super(statuscode.to!string ~ " - " ~ errcode ~ ":" ~ error);
380 	}
381 }
382 
383 class MatrixMessage
384 {
385 	string author, type, roomId, eventId;
386 	long age;
387 }
388 
389 class MatrixTextMessage : MatrixMessage
390 {
391 	string content, format, formattedContent;
392 }