1 module matrix.matrix; 2 3 import std.json; 4 import std.conv : to, text; 5 import std.array : array; 6 import std.algorithm : map, countUntil, canFind; 7 import std.exception : enforce; 8 import std.stdio; // for debugging 9 10 import requests; 11 import requests.utils : urlEncoded; 12 13 import matrix.session : Session, Account; 14 import matrix.inbound_group : InboundGroupSession; 15 import matrix.outbound_group : OutboundGroupSession; 16 17 enum RoomPreset { 18 /// Joining requires an invite 19 private_chat, 20 /// Joining requires an invite, everybody is admin 21 trusted_private_chat, 22 /// Anybody can join 23 public_chat 24 } 25 26 enum Presence:string { 27 online = "online", 28 offline = "offline", 29 unavailable = "unavailable" // e.g. idle 30 } 31 32 abstract class Client { 33 private: 34 /// Server, e.g. "https://matrix.org" 35 string server_url; 36 /// Token received after successful login 37 string access_token; 38 /// Generate a transaction ID unique across requests with the same access token 39 long tid; 40 Request rq; 41 /// Account data for encryption with olm 42 Account account = null; 43 /// State of the client, which should be preserved 44 JSONValue state; 45 /// Path where to store the state permanently 46 string state_path; 47 /// Encryption key used for all account and session serializations 48 string key; 49 50 public this(string url, string state_path) { 51 this.server_url = url; 52 this.state_path = state_path; 53 updateFromStatePath(); 54 //this.rq.verbosity = 1; 55 } 56 57 import std.file; 58 59 private void updateFromStatePath() { 60 if (state_path.exists) { 61 string raw = cast(string) read(state_path); 62 this.state = parseJSON(raw); 63 assert("user_id" in state); 64 assert("device_id" in state); 65 assert("next_batch" in state); 66 } else { 67 /* ensure basic fields exist */ 68 /// Identifier of this device by server 69 this.state["user_id"] = ""; 70 /// Matrix user id, known after login 71 this.state["device_id"] = ""; 72 /// ID of the last sync 73 this.state["next_batch"] = ""; 74 /// We must keep track of rooms 75 this.state["rooms"] = parseJSON("{}"); 76 /// We must keep track of other users 77 this.state["users"] = parseJSON("{}"); 78 } 79 } 80 81 public void saveState() const @safe { 82 this.state_path.write(state.toPrettyString()); 83 } 84 85 public string[] versions() { 86 auto res = rq.get(server_url ~ "/_matrix/client/versions"); 87 auto j = parseResponse(res); 88 return j["versions"].array.map!"a.str".array; 89 } 90 91 public void login(string name, string password) { 92 auto payload = "{ \"type\": \"m.login.password\", \"user\": \""~name~"\", \"password\": \""~password~"\" }"; 93 /* Creating a JSONValue object would be safer, but matrix.org 94 * requires a fixed order for the keys. */ 95 auto res = rq.post(server_url ~ "/_matrix/client/r0/login", 96 payload, "application/json"); 97 auto j = parseResponse(res); 98 this.access_token = j["access_token"].str; 99 if (state["device_id"].str == "") 100 this.state["device_id"] = j["device_id"]; 101 this.state["user_id"] = j["user_id"]; 102 // TODO store and use refresh_token 103 } 104 105 public void logout() { 106 setPresence(Presence.offline, "off"); 107 auto res = rq.post(server_url ~ "/_matrix/client/r0/logout" 108 ~ "?access_token=" ~ urlEncoded(this.access_token)); 109 auto j = parseResponse(res); 110 this.access_token = ""; 111 } 112 113 public void setPresence(Presence p, string status_msg) { 114 JSONValue payload = [ 115 "presence": text(p), 116 "status_msg": status_msg, 117 ]; 118 string url = server_url ~ "/_matrix/client/r0/presence/" 119 ~ state["user_id"].str ~ "/status" 120 ~ "?access_token=" ~ urlEncoded(this.access_token); 121 auto res = rq.exec!"PUT"(url, text(payload)); 122 auto j = parseResponse(res); 123 } 124 125 public void sync(int timeout) { 126 auto qp = queryParams("set_presence", "online", 127 "timeout", timeout, 128 "access_token", this.access_token); 129 if (state["next_batch"].str != "") 130 qp = queryParams("set_presence", "online", 131 "since", state["next_batch"].str, 132 "timeout", timeout, 133 "access_token", this.access_token); 134 auto res = rq.get(server_url ~ "/_matrix/client/r0/sync", qp); 135 auto j = parseResponse(res); 136 /* sync room states */ 137 if ("rooms" in j) { 138 syncRoomState(j["rooms"]); 139 } 140 /* sync presence states */ 141 if ("presence" in j && "events" in j["presence"]) { 142 auto events = j["presence"]["events"].array; 143 foreach(JSONValue e; events) { 144 if (e["type"].str == "m.presence") { 145 auto user_id = e["sender"].str; 146 if ("presence" !in state["users"][user_id]) 147 state["users"][user_id]["presence"] = parseJSON("{}"); 148 auto uj = state["users"][user_id]["presence"]; 149 uj["presence"] = e["content"]["presence"]; 150 if ("currently_active" in e["content"]) 151 uj["currently_active"] = e["content"]["currently_active"]; 152 if ("status_msg" in e["content"]) 153 uj["status_msg"] = e["content"]["status_msg"]; 154 continue; 155 } 156 onPresenceEvent(e); 157 } 158 } 159 /* sync account_data states */ 160 if ("account_data" in j && "events" in j["account_data"]) { 161 auto events = j["account_data"]["events"].array; 162 foreach(JSONValue e; events) { 163 onSyncAccountDataEvent(e); 164 } 165 } 166 /* direct to device messsages */ 167 if ("to_device" in j) { 168 auto events = j["to_device"]["events"].array; 169 foreach(JSONValue e; events) { 170 if (e["type"].str == "m.new_device") { 171 if (e["content"]["device_id"].str == state["device_id"].str) 172 continue; 173 writeln("NEW DEVICE: ", e); 174 } 175 writeln("TO_DEVICE ", e); 176 assert(false); 177 // FIXME 178 } 179 } 180 /* No idea what device lists is for ... */ 181 if ("device_lists" in j) { 182 writeln("DEVICE_LISTS? ", j["device_lists"]); 183 // TODO 184 } 185 // Make sure we notice, if another key pops up 186 foreach (key, j2; j.object) { 187 switch (key) { 188 case "next_batch": 189 case "rooms": 190 case "presence": 191 case "account_data": 192 case "to_device": 193 case "device_lists": 194 break; 195 default: 196 writeln("UNKNOWN KEY ", key); 197 assert(false); 198 } 199 } 200 if (state["next_batch"].str == "") { 201 /* announce this device */ 202 JSONValue ann = [ 203 "device_id": state["device_id"], 204 "rooms": parseJSON("[]"), 205 ]; 206 JSONValue payload = parseJSON("{}"); 207 string[] users; 208 foreach (room_id, j; state["rooms"].object) { 209 if ("encrypted" !in j.object) 210 continue; 211 ann["rooms"].array ~= JSONValue(room_id); 212 foreach (user_id, j; state["rooms"][room_id]["members"].object) { 213 if (users.canFind(user_id)) continue; 214 if (user_id !in payload) { 215 payload[user_id] = ["*": parseJSON("{}")]; 216 payload[user_id]["*"]["device_id"] = state["device_id"]; 217 payload[user_id]["*"]["rooms"] = parseJSON("[]"); 218 } 219 payload[user_id]["*"]["rooms"].array ~= JSONValue(room_id); 220 users ~= user_id; 221 } 222 } 223 JSONValue wrap = ["messages": payload]; 224 // see https://github.com/matrix-org/matrix-doc/issues/860 225 sendToDevice(wrap.toString(), "m.new_device"); 226 } 227 state["next_batch"] = j["next_batch"]; 228 } 229 230 private void syncRoomState(JSONValue json) { 231 if ("invite" in json) { 232 foreach (string roomname, JSONValue left_room; json["invite"]) { 233 onInviteRoom(roomname); 234 foreach (JSONValue event; left_room["invite_state"]["events"].array) 235 onInviteEvent(roomname, event); 236 } 237 } 238 if ("leave" in json) { 239 foreach (string roomname, JSONValue left_room; json["leave"]) { 240 assert (roomname in state["rooms"]); 241 state["rooms"][roomname] = null; 242 onLeaveRoom(roomname, left_room); 243 if ("timeline" in left_room) { 244 // TODO limited, prev_batch 245 foreach (event; left_room["timeline"]["events"].array) 246 onLeaveTimelineEvent(roomname, event); 247 } 248 if ("state" in left_room) { 249 foreach (event; left_room["state"]["events"].array) 250 onLeaveStateEvent(roomname, event); 251 } 252 } 253 } 254 if ("join" in json) { 255 foreach (string roomname, JSONValue joined_room; json["join"]) { 256 auto un = joined_room["unread_notifications"]; 257 ulong hc, nc; 258 if ("highlight_count" in un) 259 hc = un["highlight_count"].integer; 260 if ("notification_count" in un) 261 nc = un["notification_count"].integer; 262 if (roomname !in state["rooms"]) 263 state["rooms"][roomname] = parseJSON("{\"members\": {}}"); 264 onJoinRoom(roomname, hc, nc); 265 if ("state" in joined_room) { 266 foreach (event; joined_room["state"]["events"].array) { 267 auto sender = event["sender"].str; 268 if (event["type"].str == "m.room.name") { 269 state["rooms"][roomname]["name"] 270 = event["content"]["name"].str; 271 continue; 272 } 273 if (event["type"].str == "m.room.topic") { 274 state["rooms"][roomname]["topic"] 275 = event["content"]["topic"].str; 276 continue; 277 } 278 if (event["type"].str == "m.room.member") { 279 seenUserIdInRoom(event["sender"], roomname); 280 state["rooms"][roomname]["members"][sender] = parseJSON("{}"); 281 continue; 282 } 283 if (event["type"].str == "m.room.encryption") { 284 state["rooms"][roomname]["encrypted"] = true; 285 // only support megolm 286 assert (event["content"]["algorithm"].str == "m.megolm.v1.aes-sha2"); 287 writeln(sender, " enabled encryption for ", roomname); 288 continue; 289 } 290 onJoinStateEvent(roomname, event); 291 } 292 } 293 if ("timeline" in joined_room) { 294 // TODO limited, prev_batch 295 foreach (event; joined_room["timeline"]["events"].array) { 296 auto sender = event["sender"].str; 297 if (event["type"].str == "m.room.member") { 298 seenUserIdInRoom(event["sender"], roomname); 299 state["rooms"][roomname]["members"][sender] = parseJSON("{}"); 300 continue; 301 } 302 if (event["type"].str == "m.room.encryption") { 303 state["rooms"][roomname]["encrypted"] = true; 304 // only support megolm 305 assert (event["content"]["algorithm"].str == "m.megolm.v1.aes-sha2"); 306 writeln(sender, " enabled encryption for ", roomname); 307 continue; 308 } 309 if (event["type"].str == "m.room.encrypted") { 310 assert(state["rooms"][roomname]["encrypted"].type() == JSON_TYPE.TRUE); 311 // only support megolm 312 assert (event["content"]["algorithm"].str == "m.megolm.v1.aes-sha2"); 313 event = decrypt_room(event, state["rooms"][roomname]); 314 } 315 onJoinTimelineEvent(roomname, event); 316 } 317 } 318 if ("account_data" in joined_room) { 319 foreach (event; joined_room["account_data"]["events"].array) 320 onJoinAccountDataEvent(roomname, event); 321 } 322 if ("ephemeral" in joined_room) { 323 foreach (event; joined_room["ephemeral"]["events"].array) 324 onEphemeralEvent(roomname, event); 325 } 326 } 327 } 328 } 329 330 private JSONValue decrypt_room(JSONValue event, JSONValue roomstate) { 331 auto session_id = event["content"]["session_id"].str; 332 auto cipher = event["content"]["ciphertext"].str; 333 auto sender_key = event["content"]["sender_key"].str; 334 { 335 /* check if session_ids match */ 336 auto inbound2 = InboundGroupSession.init(sender_key); 337 //enforce(inbound2.session_id == session_id); 338 } 339 auto sender_id = event["sender"].str; 340 auto sender_dev_id = event["content"]["device_id"].str; 341 if (sender_dev_id !in roomstate["members"][sender_id]) { 342 roomstate["members"][sender_id][sender_dev_id] = parseJSON("{}"); 343 } 344 auto dev_info = roomstate["members"][sender_id][sender_dev_id]; 345 if ("megolm_sessions" !in state) 346 state["megolm_sessions"] = parseJSON("{}"); 347 if (session_id in state["megolm_sessions"]) { 348 auto s = state["megolm_sessions"][session_id]; 349 if ("enc_inbound" !in s) { 350 writeln("TODO Cannot decrypt. Key missing."); 351 return event; 352 } 353 auto inbound = InboundGroupSession.unpickle(this.key, s["enc_inbound"].str); 354 uint msg_index; 355 auto plain = inbound.decrypt(cipher, &msg_index); 356 dev_info["msg_index"] = msg_index; 357 event["content"] = [ 358 "body": parseJSON(plain)["content"].str, 359 "msgtype": "m.text" ]; 360 event["type"] = "m.room.message"; 361 return event; 362 } else { // Unknown session 363 writeln("Unknown session id: ", session_id); 364 state["megolm_sessions"][session_id] = ["sender_identity_key": sender_key, "initiator": sender_id]; 365 } 366 // Decryption failed. Return still-encrypted event. 367 return event; 368 } 369 370 private void seenUserIdInRoom(JSONValue user_id, string room_id) { 371 if (user_id.str !in state["rooms"][room_id]["members"]) { 372 state["rooms"][room_id]["members"][user_id.str] = parseJSON("{}"); 373 } 374 if (user_id.str !in state["users"]) { 375 state["users"][user_id.str] = parseJSON("{}"); 376 } 377 } 378 379 abstract public void onInviteRoom(const string name); 380 abstract public void onInviteEvent(const string name, const JSONValue v); 381 abstract public void onLeaveRoom(const string name, const JSONValue v); 382 abstract public void onJoinRoom(const string name, ulong highlight_count, ulong notification_count); 383 abstract public void onLeaveTimelineEvent(const string name, const JSONValue v); 384 abstract public void onLeaveStateEvent(const string name, const JSONValue v); 385 abstract public void onJoinTimelineEvent(const string name, const JSONValue v); 386 abstract public void onJoinStateEvent(const string name, const JSONValue v); 387 abstract public void onJoinAccountDataEvent(const string name, const JSONValue v); 388 abstract public void onSyncAccountDataEvent(const JSONValue v); 389 abstract public void onEphemeralEvent(const string name, const JSONValue v); 390 abstract public void onPresenceEvent(const JSONValue v); 391 abstract public void onAccountDataUpdate(const string type, const string key, const JSONValue value); 392 393 private string nextTransactionID() { 394 scope(exit) this.tid += 1; 395 return text(this.tid); 396 } 397 398 private void fetchDeviceKeys(string roomname) { 399 auto q = parseJSON("{\"device_keys\":{}}"); 400 foreach (user_id, j; state["rooms"][roomname]["members"].object) { 401 q["device_keys"][user_id] = parseJSON("{}"); 402 } 403 string url = server_url ~ "/_matrix/client/unstable/keys/query" 404 ~ "?access_token=" ~ urlEncoded(this.access_token); 405 auto res = rq.post(url, text(q)); 406 auto j = parseResponse(res); 407 check_signature(j); 408 foreach(user_id, j2; j["device_keys"].object) { 409 if (user_id !in state["users"]) 410 state["users"][user_id] = parseJSON("{}"); 411 foreach(device_id, j3; j2.object) { 412 check_signature(j3); 413 // FIXME match user_id-device_id against known information 414 // FIXME for known devices match ed25519 key 415 if (device_id !in state["users"][user_id]) { 416 state["users"][user_id][device_id] = parseJSON("{}"); 417 } 418 foreach(method, key; j3["keys"].object) { 419 auto i = method.countUntil(":"); 420 // FIXME what if already in there? 421 state["users"][user_id][device_id][method[0..i]] = key; 422 } 423 } 424 } 425 } 426 427 public void send(string roomname, string msg) { 428 if ("encrypted" in state["rooms"][roomname]) { 429 fetchDeviceKeys(roomname); 430 OutboundGroupSession outbound; 431 if ("enc_outbound" in state["rooms"][roomname]) { 432 outbound = OutboundGroupSession.unpickle(this.key, state["rooms"][roomname]["enc_outbound"].str); 433 } else { 434 outbound = new OutboundGroupSession(); 435 state["rooms"][roomname]["enc_outbound"] = outbound.pickle(this.key); 436 } 437 // FIXME check if outbound requires rotation 438 sendSessionKeyAround(roomname, outbound); 439 JSONValue payload = [ 440 "type": "m.text", 441 "content": msg, 442 "room_id": roomname 443 ]; 444 auto cipher = outbound.encrypt(text(payload)); 445 JSONValue content = [ 446 "algorithm": "m.megolm.v1.aes-sha2", 447 "sender_key": outbound.session_key, 448 "ciphertext": cipher, 449 "session_id": outbound.session_id, 450 "device_id": state["device_id"].str, 451 ]; 452 string url = server_url ~ "/_matrix/client/r0/rooms/" 453 ~ roomname ~ "/send/m.room.encrypted/" ~ nextTransactionID() 454 ~ "?access_token=" ~ urlEncoded(this.access_token); 455 auto res = rq.exec!"PUT"(url, text(content)); 456 auto j = parseResponse(res); 457 } else { /* sending unencrypted */ 458 auto content = parseJSON("{\"msgtype\": \"m.text\"}"); 459 content["body"] = msg; 460 string url = server_url ~ "/_matrix/client/r0/rooms/" ~ roomname 461 ~ "/send/m.room.message/" ~ nextTransactionID() 462 ~ "?access_token=" ~ urlEncoded(this.access_token); 463 auto res = rq.exec!"PUT"(url, text(content)); 464 auto j = parseResponse(res); 465 } 466 } 467 468 private void sendSessionKeyAround(string room_id, OutboundGroupSession outbound) { 469 auto s_id = outbound.session_id; 470 auto s_key = outbound.session_key; 471 auto device_id = state["device_id"].str; 472 /* store these details as an inbound session, just as it would when 473 receiving them via an m.room_key event */ 474 { 475 auto inb = InboundGroupSession.init(s_key); 476 assert (inb.session_id == s_id); 477 JSONValue j = [ 478 "session_key": s_key, 479 "enc_inbound": inb.pickle(this.key), 480 ]; 481 j["msg_index"] = 0; 482 auto uid = state["user_id"].str; 483 state["rooms"][room_id]["members"][uid][device_id] = parseJSON("{}"); 484 if ("megolm_sessions" !in state) 485 state["megolm_sessions"] = [s_id: j]; 486 else 487 state["megolm_sessions"][s_id] = j; 488 } 489 createOlmSessions(state["rooms"][room_id]["members"].object.byKey.array); 490 /* send session key to all other devices in the room */ 491 JSONValue payload = parseJSON("{}"); 492 foreach (user_id, j; state["rooms"][room_id]["members"].object) { 493 payload[user_id] = parseJSON("{}"); 494 foreach (string device_id, j2; j.object) { 495 if (device_id == state["device_id"].str) 496 continue; 497 if (device_id !in state["users"][user_id]) 498 continue; 499 writeln("send session '", s_id, "' key to ", user_id, " ", device_id); 500 JSONValue j = [ 501 "algorithm": "m.megolm.v1.aes-sha2", 502 "room_id": room_id, 503 "session_id": s_id, 504 "session_key": s_key, 505 ]; 506 payload[user_id][device_id] = encrypt_for_device(user_id, device_id, text(j)); 507 } 508 } 509 JSONValue wrap = ["messages": payload]; 510 // see https://github.com/matrix-org/matrix-doc/issues/860 511 sendToDevice(text(wrap), "m.room.encrypted"); 512 } 513 514 private void createOlmSessions(const string[] users) { 515 /* claim one time keys */ 516 JSONValue payload = ["one_time_keys": parseJSON("{}")]; 517 foreach (user_id, j; state["users"].object) { 518 if (!users.canFind(user_id)) 519 continue; 520 payload["one_time_keys"][user_id] = parseJSON("{}"); 521 foreach (dev_id, j2; j.object) { 522 payload["one_time_keys"][user_id][dev_id] = "signed_curve25519"; 523 } 524 } 525 string url = server_url ~ "/_matrix/client/unstable/keys/claim" 526 ~ "?access_token=" ~ urlEncoded(this.access_token); 527 auto res = rq.post(url, text(payload), "application/json"); 528 /* create sessions from one time keys */ 529 auto j = parseResponse(res); 530 foreach (user_id, j; j["one_time_keys"].object) { 531 foreach (device_id, j2; j.object) { 532 foreach (s_key_id, j3; j2.object) { 533 import std.algorithm : startsWith; 534 assert (s_key_id.startsWith("signed_curve25519:")); 535 check_signature(j3); 536 auto dev_key = j3["key"].str; 537 auto identity_key = state["users"][user_id][device_id]["ed25519"].str; 538 auto session = Session.create_outbound(this.account, identity_key, dev_key); 539 state["users"][user_id][device_id]["enc_session"] = session.pickle(this.key); 540 } 541 } 542 } 543 } 544 545 private string encrypt_for_device(string user_id, string device_id, string msg) { 546 if (device_id !in state["users"][user_id]) 547 throw new Exception("Unknown device "~text(device_id)); 548 if ("enc_session" !in state["users"][user_id][device_id]) 549 throw new Exception("No session"); 550 auto session = Session.unpickle(this.key, 551 state["users"][user_id][device_id]["enc_session"].str); 552 ulong msg_typ; 553 auto cipher = session.encrypt(msg, msg_typ); 554 return cipher; 555 } 556 557 private void sendToDevice(string msg, string msg_type) { 558 string url = server_url ~ "/_matrix/client/unstable/sendToDevice/" 559 ~ msg_type ~ "/" ~ nextTransactionID() 560 ~ "?access_token=" ~ urlEncoded(this.access_token); 561 auto res = rq.exec!"PUT"(url, msg); 562 auto j = parseResponse(res); 563 } 564 565 private string[] devicesOfRoom(string room_id) { 566 string[] ret; 567 foreach (user_id, j; state["rooms"][room_id]["members"].object) { 568 foreach (string device_id, j2; j.object) { 569 ret ~= device_id; 570 } 571 } 572 return ret; 573 } 574 575 private void check_signature(JSONValue j) { 576 // FIXME actually implement check 577 /* if signature check fails, mark the failing device as 'evil' */ 578 } 579 580 /** Create a new room on the homeserver 581 * Returns: id of the room 582 */ 583 public string createRoom(RoomPreset p) { 584 JSONValue content = parseJSON("{}"); 585 content["preset"] = text(p); 586 string url = server_url ~ "/_matrix/client/r0/createRoom" 587 ~ "?access_token=" ~ urlEncoded(this.access_token); 588 auto payload = text(content); 589 auto res = rq.post(url, payload, "application/json"); 590 auto j = parseResponse(res); 591 return j["room_id"].str; 592 } 593 594 public void invite(string roomid, string userid) { 595 JSONValue content = parseJSON("{}"); 596 content["user_id"] = userid; 597 string url = server_url ~ "/_matrix/client/r0/rooms/" 598 ~ roomid ~ "/invite" 599 ~ "?access_token=" ~ urlEncoded(this.access_token); 600 auto payload = text(content); 601 auto res = rq.post(url, payload, "application/json"); 602 auto j = parseResponse(res); 603 } 604 605 /** Enables encryption 606 * Requires key because we always store stuff locked up. 607 * Must be logged in, so we know the user id. 608 * If path exist, then load it and decrypt with key. 609 * Otherwise create new keys and store them there encrypted with key. 610 **/ 611 public void enable_encryption(string key) { 612 this.key = key; 613 if ("encrypted_account" in state) { 614 this.account = Account.unpickle(key, state["encrypted_account"].str); 615 } else { 616 this.account = Account.create(); 617 state["encrypted_account"] = account.pickle(key); 618 } 619 /* create payload for publishing keys to homeserver */ 620 assert (this.access_token); // must login first! 621 const keys = parseJSON(this.account.identity_keys); 622 const device_id = state["device_id"].str; 623 JSONValue json = [ 624 "device_id": device_id, 625 "user_id": state["user_id"].str ]; 626 json["algorithms"] = ["m.olm.v1.curve25519-aes-sha2", 627 "m.megolm.v1.aes-sha2"]; 628 json["keys"] = [ 629 "curve25519:"~device_id: keys["curve25519"].str, 630 "ed25519:"~device_id: keys["ed25519"].str 631 ]; 632 sign_json(json); 633 /* actually publish keys */ 634 auto payload = text(json); 635 auto res = rq.post(server_url ~ "/_matrix/client/unstable/keys/upload" 636 ~ "?access_token=" ~ urlEncoded(this.access_token), 637 payload, "application/json"); 638 auto j = parseResponse(res); 639 uploadOneTimeKeys(j); 640 } 641 642 /** Uploads more one time keys, if necessary */ 643 public void uploadOneTimeKeys() { 644 auto res = rq.post(server_url ~ "/_matrix/client/unstable/keys/upload" 645 ~ "?access_token=" ~ urlEncoded(this.access_token), 646 "{}", "application/json"); 647 auto j = parseResponse(res); 648 uploadOneTimeKeys(j); 649 } 650 651 private void uploadOneTimeKeys(JSONValue currently) { 652 ulong otkeys_on_server; 653 ulong max_otkeys_on_server = this.account.max_number_of_one_time_keys/2; 654 if ("one_time_key_counts" in currently) { 655 foreach(k,v; currently["one_time_key_counts"].object) { 656 writeln("counting one time keys", k, v); // TODO 657 } 658 } 659 if (otkeys_on_server >= max_otkeys_on_server) 660 return; 661 /* Generate new keys */ 662 auto diff = max_otkeys_on_server - otkeys_on_server; 663 this.account.generate_one_time_keys(diff); 664 auto keys = parseJSON(this.account.one_time_keys()); 665 JSONValue allkeys = ["one_time_keys": parseJSON("{}")]; 666 foreach(kid,key; keys["curve25519"].object) { 667 JSONValue j = ["key": key]; 668 sign_json(j); 669 allkeys["one_time_keys"]["signed_curve25519:"~kid] = j; 670 } 671 /* upload */ 672 auto payload = text(allkeys); 673 auto res = rq.post(server_url ~ "/_matrix/client/unstable/keys/upload" 674 ~ "?access_token=" ~ urlEncoded(this.access_token), 675 payload, "application/json"); 676 auto j = parseResponse(res); 677 this.account.mark_keys_as_published(); 678 } 679 680 private void sign_json(JSONValue j) { 681 /* D creates Canonical JSON as specified by Matrix */ 682 auto raw = text(j); 683 auto signature = this.account.sign(raw); 684 auto user_id = state["user_id"].str; 685 auto device_id = state["device_id"].str; 686 j["signatures"] = [user_id: ["ed25519:"~device_id: signature]]; 687 } 688 689 public @property string[] rooms() { 690 string[] ret; 691 foreach (roomname, v; state["rooms"].object) 692 ret ~= roomname; 693 return ret; 694 } 695 } 696 697 /* Convert a raw response into JSON 698 * and check for a Matrix error */ 699 JSONValue parseResponse(Response res) { 700 auto r = res.responseBody; 701 auto j = parseJSON(r, JSONOptions.none); 702 if ("error" in j) 703 throw new MatrixError( 704 j["errcode"].str ~" "~ j["error"].str); 705 return j; 706 } 707 708 /* When the Matrix server sends an error message */ 709 class MatrixError : Exception { 710 public this(string msg) { 711 super(msg); 712 } 713 }