Line data Source code
1 : /* MIT License
2 : *
3 : * Copyright (C) 2019, 2020, 2021 Famedly GmbH
4 : *
5 : * Permission is hereby granted, free of charge, to any person obtaining a copy
6 : * of this software and associated documentation files (the "Software"), to deal
7 : * in the Software without restriction, including without limitation the rights
8 : * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 : * copies of the Software, and to permit persons to whom the Software is
10 : * furnished to do so, subject to the following conditions:
11 : *
12 : * The above copyright notice and this permission notice shall be included in all
13 : * copies or substantial portions of the Software.
14 : *
15 : * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 : * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 : * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 : * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 : * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 : * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 : * SOFTWARE.
22 : */
23 :
24 : import 'dart:async';
25 : import 'dart:convert';
26 : import 'dart:typed_data';
27 :
28 : import 'package:http/http.dart' as http;
29 :
30 : import 'package:matrix/matrix_api_lite.dart';
31 : import 'package:matrix/matrix_api_lite/generated/api.dart';
32 :
33 : // ignore: constant_identifier_names
34 : enum RequestType { GET, POST, PUT, DELETE }
35 :
36 : class MatrixApi extends Api {
37 : /// The homeserver this client is communicating with.
38 66 : Uri? get homeserver => baseUri;
39 :
40 66 : set homeserver(Uri? uri) => baseUri = uri;
41 :
42 : /// This is the access token for the matrix client. When it is undefined, then
43 : /// the user needs to sign in first.
44 62 : String? get accessToken => bearerToken;
45 :
46 62 : set accessToken(String? token) => bearerToken = token;
47 :
48 5 : @override
49 : Never unexpectedResponse(http.BaseResponse response, Uint8List body) {
50 20 : if (response.statusCode >= 400 && response.statusCode < 500) {
51 10 : final resp = json.decode(utf8.decode(body));
52 5 : if (resp is Map<String, Object?>) {
53 5 : throw MatrixException.fromJson(resp);
54 : }
55 : }
56 1 : super.unexpectedResponse(response, body);
57 : }
58 :
59 2 : @override
60 : Never bodySizeExceeded(int expected, int actual) {
61 2 : throw EventTooLarge(expected, actual);
62 : }
63 :
64 38 : MatrixApi({
65 : Uri? homeserver,
66 : String? accessToken,
67 : super.httpClient,
68 38 : }) : super(baseUri: homeserver, bearerToken: accessToken);
69 :
70 : /// Used for all Matrix json requests using the [c2s API](https://matrix.org/docs/spec/client_server/r0.6.0.html).
71 : ///
72 : /// Throws: FormatException, MatrixException
73 : ///
74 : /// You must first set [this.homeserver] and for some endpoints also
75 : /// [this.accessToken] before you can use this! For example to send a
76 : /// message to a Matrix room with the id '!fjd823j:example.com' you call:
77 : /// ```
78 : /// final resp = await request(
79 : /// RequestType.PUT,
80 : /// '/r0/rooms/!fjd823j:example.com/send/m.room.message/$txnId',
81 : /// data: {
82 : /// 'msgtype': 'm.text',
83 : /// 'body': 'hello'
84 : /// }
85 : /// );
86 : /// ```
87 : ///
88 26 : Future<Map<String, Object?>> request(
89 : RequestType type,
90 : String action, {
91 : dynamic data = '',
92 : String contentType = 'application/json',
93 : Map<String, Object?>? query,
94 : }) async {
95 26 : if (homeserver == null) {
96 : throw ('No homeserver specified.');
97 : }
98 : dynamic json;
99 51 : (data is! String) ? json = jsonEncode(data) : json = data;
100 52 : if (data is List<int> || action.startsWith('/media/v3/upload')) json = data;
101 :
102 26 : final url = homeserver!
103 78 : .resolveUri(Uri(path: '_matrix$action', queryParameters: query));
104 :
105 26 : final headers = <String, String>{};
106 52 : if (type == RequestType.PUT || type == RequestType.POST) {
107 25 : headers['Content-Type'] = contentType;
108 : }
109 26 : if (accessToken != null) {
110 78 : headers['Authorization'] = 'Bearer $accessToken';
111 : }
112 :
113 : late http.Response resp;
114 26 : Map<String, Object?>? jsonResp = <String, Object?>{};
115 :
116 : switch (type) {
117 26 : case RequestType.GET:
118 8 : resp = await httpClient.get(url, headers: headers);
119 : break;
120 25 : case RequestType.POST:
121 50 : resp = await httpClient.post(url, body: json, headers: headers);
122 : break;
123 2 : case RequestType.PUT:
124 4 : resp = await httpClient.put(url, body: json, headers: headers);
125 : break;
126 0 : case RequestType.DELETE:
127 0 : resp = await httpClient.delete(url, headers: headers);
128 : break;
129 : }
130 26 : var respBody = resp.body;
131 : try {
132 52 : respBody = utf8.decode(resp.bodyBytes);
133 : } catch (_) {
134 : // No-OP
135 : }
136 52 : if (resp.statusCode >= 500 && resp.statusCode < 600) {
137 0 : throw Exception(respBody);
138 : }
139 52 : var jsonString = String.fromCharCodes(respBody.runes);
140 26 : if (jsonString.startsWith('[') && jsonString.endsWith(']')) {
141 0 : jsonString = '{"chunk":$jsonString}';
142 : }
143 26 : jsonResp = jsonDecode(jsonString)
144 : as Map<String, Object?>?; // May throw FormatException
145 :
146 52 : if (resp.statusCode >= 400 && resp.statusCode < 500) {
147 0 : throw MatrixException(resp);
148 : }
149 :
150 : return jsonResp!;
151 : }
152 :
153 : /// Publishes end-to-end encryption keys for the device.
154 : /// https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-keys-query
155 24 : Future<Map<String, int>> uploadKeys(
156 : {MatrixDeviceKeys? deviceKeys,
157 : Map<String, Object?>? oneTimeKeys,
158 : Map<String, Object?>? fallbackKeys}) async {
159 24 : final response = await request(
160 : RequestType.POST,
161 : '/client/v3/keys/upload',
162 24 : data: {
163 10 : if (deviceKeys != null) 'device_keys': deviceKeys.toJson(),
164 24 : if (oneTimeKeys != null) 'one_time_keys': oneTimeKeys,
165 24 : if (fallbackKeys != null) ...{
166 : 'fallback_keys': fallbackKeys,
167 : 'org.matrix.msc2732.fallback_keys': fallbackKeys,
168 : },
169 : },
170 : );
171 48 : return Map<String, int>.from(response['one_time_key_counts'] as Map);
172 : }
173 :
174 : /// This endpoint allows the creation, modification and deletion of pushers
175 : /// for this user ID. The behaviour of this endpoint varies depending on the
176 : /// values in the JSON body.
177 : ///
178 : /// See [deletePusher] to issue requests with `kind: null`.
179 : ///
180 : /// https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-pushers-set
181 0 : Future<void> postPusher(Pusher pusher, {bool? append}) async {
182 0 : final data = pusher.toJson();
183 : if (append != null) {
184 0 : data['append'] = append;
185 : }
186 0 : await request(
187 : RequestType.POST,
188 : '/client/v3/pushers/set',
189 : data: data,
190 : );
191 : return;
192 : }
193 :
194 : /// Variant of postPusher operation that deletes pushers by setting `kind: null`.
195 : ///
196 : /// https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-pushers-set
197 0 : Future<void> deletePusher(PusherId pusher) async {
198 0 : final data = PusherData.fromJson(pusher.toJson()).toJson();
199 0 : data['kind'] = null;
200 0 : await request(
201 : RequestType.POST,
202 : '/client/v3/pushers/set',
203 : data: data,
204 : );
205 : return;
206 : }
207 :
208 : /// This API provides credentials for the client to use when initiating
209 : /// calls.
210 2 : @override
211 : Future<TurnServerCredentials> getTurnServer() async {
212 2 : final json = await request(RequestType.GET, '/client/v3/voip/turnServer');
213 :
214 : // fix invalid responses from synapse
215 : // https://github.com/matrix-org/synapse/pull/10922
216 2 : final ttl = json['ttl'];
217 2 : if (ttl is double) {
218 0 : json['ttl'] = ttl.toInt();
219 : }
220 :
221 2 : return TurnServerCredentials.fromJson(json);
222 : }
223 :
224 0 : @Deprecated('Use [deleteRoomKeyBySessionId] instead')
225 : Future<RoomKeysUpdateResponse> deleteRoomKeysBySessionId(
226 : String roomId, String sessionId, String version) async {
227 0 : return deleteRoomKeyBySessionId(roomId, sessionId, version);
228 : }
229 :
230 0 : @Deprecated('Use [deleteRoomKeyBySessionId] instead')
231 : Future<RoomKeysUpdateResponse> putRoomKeysBySessionId(String roomId,
232 : String sessionId, String version, KeyBackupData data) async {
233 0 : return putRoomKeyBySessionId(roomId, sessionId, version, data);
234 : }
235 :
236 0 : @Deprecated('Use [getRoomKeyBySessionId] instead')
237 : Future<KeyBackupData> getRoomKeysBySessionId(
238 : String roomId, String sessionId, String version) async {
239 0 : return getRoomKeyBySessionId(roomId, sessionId, version);
240 : }
241 : }
242 :
243 : class EventTooLarge implements Exception {
244 : int maxSize, actualSize;
245 2 : EventTooLarge(this.maxSize, this.actualSize);
246 : }
|