1 module matrix; 2 3 import std.json; 4 import std.conv : to, text; 5 import std.array : array; 6 import std.algorithm : map; 7 8 import requests; 9 10 import requests.utils : urlEncoded; 11 12 enum RoomPreset { 13 /// Joining requires an invite 14 private_chat, 15 /// Joining requires an invite, everybody is admin 16 trusted_private_chat, 17 /// Anybody can join 18 public_chat 19 } 20 21 abstract class Client { 22 private: 23 /// Server, e.g. "https://matrix.org" 24 string server_url; 25 /// Token received after successful login 26 string access_token; 27 /// Generate a transaction ID unique across requests with the same access token 28 long tid; 29 /// IDentifier of this device by server 30 string device_id; 31 /// Matrix user id, known after login 32 string user_id; 33 /// ID of the last sync 34 string next_batch; 35 Request rq; 36 37 public this(string url) { 38 this.server_url = url; 39 //this.rq.verbosity = 2; 40 } 41 42 public string[] versions() { 43 auto res = rq.get(server_url ~ "/_matrix/client/versions"); 44 auto j = parseResponse(res); 45 return j["versions"].array.map!"a.str".array; 46 } 47 48 public void login(string name, string password) { 49 auto payload = "{ \"type\": \"m.login.password\", \"user\": \""~name~"\", \"password\": \""~password~"\" }"; 50 /* Creating a JSONValue object would be safer, but matrix.org 51 * requires a fixed order for the keys. */ 52 auto res = rq.post(server_url ~ "/_matrix/client/r0/login", 53 payload, "application/json"); 54 auto j = parseResponse(res); 55 this.access_token = j["access_token"].str; 56 this.device_id = j["device_id"].str; 57 this.user_id = j["user_id"].str; 58 } 59 60 public void sync(int timeout) { 61 auto qp = queryParams("set_presence", "offline", 62 "timeout", timeout, 63 "access_token", this.access_token); 64 if (this.next_batch) 65 qp = queryParams("set_presence", "offline", 66 "since", this.next_batch, 67 "timeout", timeout, 68 "access_token", this.access_token); 69 auto res = rq.get(server_url ~ "/_matrix/client/r0/sync", qp); 70 auto j = parseResponse(res); 71 /* sync room states */ 72 if ("rooms" in j) { 73 foreach(string k, JSONValue v; j["rooms"]) { 74 // TODO events inside rooms 75 switch (k) { 76 case "invite": 77 auto iv = j["rooms"]["invite"]; 78 foreach (string roomname, JSONValue v; iv) 79 onInviteRoom(roomname, v); 80 break; 81 case "leave": 82 auto iv = j["rooms"]["leave"]; 83 foreach (string roomname, JSONValue v; iv) 84 onLeaveRoom(roomname, v); 85 break; 86 case "join": 87 auto iv = j["rooms"]["join"]; 88 foreach (string roomname, JSONValue v; iv) 89 onJoinRoom(roomname, v); 90 break; 91 default: 92 throw new Exception("unknown room event: "~k); 93 } 94 } 95 } 96 /* sync presence states */ 97 if ("presence" in j && "events" in j["presence"]) { 98 auto events = j["presence"]["events"].array; 99 if (events.length > 0) 100 foreach(JSONValue v; events) { 101 auto sender = v["sender"].str; 102 auto presence = v["content"]["presence"].str; 103 onPresenceUpdate(sender, presence); 104 } 105 } 106 /* sync account_data states */ 107 if ("account_data" in j && "events" in j["account_data"]) { 108 auto events = j["account_data"]["events"].array; 109 if (events.length > 0) 110 foreach(JSONValue e; events) { 111 auto type = e["type"].str; 112 foreach(string k, JSONValue v; e["content"]) { 113 onAccountDataUpdate(type, k, v); 114 } 115 } 116 } 117 //import std.stdio; 118 //foreach (string k, JSONValue v; j) 119 // writeln(k); 120 this.next_batch = j["next_batch"].str; 121 } 122 123 abstract public void onInviteRoom(const string name, const JSONValue v); 124 abstract public void onLeaveRoom(const string name, const JSONValue v); 125 abstract public void onJoinRoom(const string name, const JSONValue v); 126 abstract public void onPresenceUpdate(const string name, const string state); 127 abstract public void onAccountDataUpdate(const string type, const string key, const JSONValue value); 128 129 private string nextTransactionID() { 130 scope(exit) this.tid += 1; 131 return text(this.tid); 132 } 133 134 public void send(string roomname, string msg) { 135 auto content = parseJSON("{\"msgtype\": \"m.text\"}"); 136 content["body"] = msg; 137 string url = server_url ~ "/_matrix/client/r0/rooms/" ~ roomname 138 ~ "/send/m.room.message/" ~ nextTransactionID() 139 ~ "?access_token=" ~ urlEncoded(this.access_token); 140 auto res = rq.exec!"PUT"(url, text(content)); 141 auto j = parseResponse(res); 142 } 143 144 /** Create a new room on the homeserver 145 * Returns: id of the room 146 */ 147 public string createRoom(RoomPreset p) { 148 JSONValue content = parseJSON("{}"); 149 content["preset"] = text(p); 150 string url = server_url ~ "/_matrix/client/r0/createRoom" 151 ~ "?access_token=" ~ urlEncoded(this.access_token); 152 auto payload = text(content); 153 auto res = rq.post(url, payload, "application/json"); 154 auto j = parseResponse(res); 155 return j["room_id"].str; 156 } 157 158 public void invite(string roomid, string userid) { 159 JSONValue content = parseJSON("{}"); 160 content["user_id"] = userid; 161 string url = server_url ~ "/_matrix/client/r0/rooms/" 162 ~ roomid ~ "/invite" 163 ~ "?access_token=" ~ urlEncoded(this.access_token); 164 auto payload = text(content); 165 auto res = rq.post(url, payload, "application/json"); 166 auto j = parseResponse(res); 167 } 168 } 169 170 final class DummyClient : Client { 171 import std.stdio; 172 public this(string url) { super(url); } 173 override public void onInviteRoom(const string name, const JSONValue v) 174 { 175 writeln("invite "~name~" "~text(v)); 176 } 177 override public void onLeaveRoom(const string name, const JSONValue v) 178 { 179 writeln("leave "~name~" "~text(v)); 180 } 181 override public void onJoinRoom(const string name, const JSONValue v) 182 { 183 writeln("join "~name~" "~text(v)); 184 } 185 override public void onPresenceUpdate(const string name, const string state) 186 { 187 writeln("presence update "~name~" "~state); 188 } 189 override public void onAccountDataUpdate(const string type, const string key, const JSONValue value) 190 { 191 writeln("account data update "~type~" "~key~": "~text(value)); 192 } 193 } 194 195 /* Convert a raw response into JSON 196 * and check for a Matrix error */ 197 JSONValue parseResponse(Response res) { 198 auto r = res.responseBody; 199 auto j = parseJSON(r, JSONOptions.none); 200 if ("error" in j) 201 throw new MatrixError( 202 j["errcode"].str ~" "~ j["error"].str); 203 return j; 204 } 205 206 /* When the Matrix server sends an error message */ 207 class MatrixError : Exception { 208 public this(string msg) { 209 super(msg); 210 } 211 }