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 }