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 }