1 /** 2 * A simple Matrix client. Experimental! 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <ae@cy.md> 12 */ 13 14 module ae.net.matrix.client; 15 16 import core.time; 17 18 import std.conv : to; 19 import std.exception; 20 import std.json : parseJSON, JSONValue; 21 22 import ae.net.http.client; 23 import ae.net.matrix.common; 24 import ae.sys.data; 25 import ae.sys.dataset; 26 import ae.sys.log; 27 import ae.sys.timing; 28 import ae.utils.array; 29 import ae.utils.json : JSONFragment; 30 import ae.utils.promise; 31 import ae.utils.text : randomString; 32 33 final class MatrixClient 34 { 35 private: 36 string host; 37 string clientAccessToken; 38 Promise!string serverBaseURL; 39 40 Promise!string getServerHost() 41 { 42 return serverBaseURL.require({ 43 auto p = new Promise!string; 44 httpGet("https://" ~ host ~ "/.well-known/matrix/client", 45 (string s) { p.fulfill(s.parseJSON()["m.homeserver"]["base_url"].str); }, 46 (string err) { p.reject(new Exception(err)); }, 47 ); 48 return p; 49 }()); 50 } 51 52 enum timeout = 5.minutes; 53 54 /// Send a request (with retry). 55 Promise!JSONValue send(string path, string method = "GET", JSONFragment data = JSONFragment.init) 56 { 57 auto p = new Promise!JSONValue; 58 getServerHost().then( 59 (string baseURL) 60 { 61 int retries; 62 void request() 63 { 64 void retry(Duration backoff, string msg) 65 { 66 if (retries++ >= 10) 67 return p.reject(new Exception(msg)); 68 setTimeout(&request, backoff); 69 } 70 71 auto url = baseURL ~ path; 72 auto r = new HttpRequest(url); 73 r.headers["Authorization"] = "Bearer " ~ clientAccessToken; 74 r.method = method; 75 if (data) 76 { 77 r.headers["Content-Type"] = "application/json"; 78 r.data = DataVec(Data(data.json.asBytes)); 79 } 80 auto c = new HttpsClient(timeout); 81 c.handleResponse = (HttpResponse response, string disconnectReason) 82 { 83 try 84 { 85 if (!response) 86 return retry(10.seconds, disconnectReason); 87 enforce(response.headers.get("Content-Type", null) == "application/json", "Expected application/json"); 88 auto j = response.getContent().asDataOf!char.toGC.parseJSON(); 89 if (response.status == HttpStatusCode.TooManyRequests) 90 return retry( 91 j.object.get("retry_after_ms", JSONValue(10_000)).integer.msecs, 92 j.object.get("error", JSONValue(response.statusMessage)).str, 93 ); 94 if (response.status != HttpStatusCode.OK) 95 throw new Exception(j.object.get("error", JSONValue(response.statusMessage)).str); 96 p.fulfill(j); 97 } 98 catch (Exception e) 99 p.reject(e); 100 }; 101 c.request(r); 102 } 103 request(); 104 } 105 ); 106 return p; 107 } 108 109 alias Subscription = void delegate(JSONValue); 110 Subscription[] subscriptions; 111 112 void sync(string since = null) 113 { 114 auto queryParameters = [ 115 "timeout": timeout.total!"msecs".to!string, 116 ]; 117 if (since) 118 queryParameters["since"] = since; 119 send("/_matrix/client/v3/sync?" ~ encodeUrlParameters(queryParameters)) 120 .then((JSONValue j) { 121 if (since) 122 foreach (subscription; subscriptions) 123 subscription(j); 124 sync(j["next_batch"].str); 125 }) 126 .except((Exception e) { 127 if (log) log("Sync error: " ~ e.msg); 128 setTimeout(&sync, 10.seconds, since); 129 }); 130 } 131 132 public: 133 Logger log; 134 135 this(string host, string clientAccessToken) 136 { 137 this.host = host; 138 this.clientAccessToken = clientAccessToken; 139 } 140 141 Promise!EventId send(RoomId roomId, MessageEventType eventType, RoomMessage roomMessage) 142 { 143 auto txnId = randomString(); 144 return send("/_matrix/client/r0/rooms/" ~ roomId.value ~ "/send/" ~ eventType ~ "/" ~ txnId, "PUT", roomMessage.fragment) 145 .then((JSONValue response) 146 { 147 return response["event_id"].str.EventId; 148 }) 149 ; 150 } 151 152 void subscribe(void delegate(JSONValue) subscription) 153 { 154 if (!subscriptions) 155 sync(); 156 subscriptions ~= subscription; 157 } 158 }