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 }