From 7d5da300c48de6157c265779439292429e4ad1a1 Mon Sep 17 00:00:00 2001 From: Christian Date: Mon, 9 Sep 2019 13:22:02 +0000 Subject: [PATCH] [Connection] Add upload method --- lib/src/Client.dart | 13 +++++ lib/src/Connection.dart | 33 ++++++++++-- lib/src/Room.dart | 114 ++++++++++++++++++++++++++++++++++++---- pubspec.lock | 37 ++++++++++++- pubspec.yaml | 2 + test/Client_test.dart | 13 +++++ test/FakeMatrixApi.dart | 10 ++++ test/Room_test.dart | 36 +++++++++++++ 8 files changed, 244 insertions(+), 14 deletions(-) diff --git a/lib/src/Client.dart b/lib/src/Client.dart index 305538e..8b39bae 100644 --- a/lib/src/Client.dart +++ b/lib/src/Client.dart @@ -23,6 +23,7 @@ import 'dart:async'; import 'dart:core'; +import 'dart:io'; import 'package:famedlysdk/src/AccountData.dart'; import 'package:famedlysdk/src/Presence.dart'; @@ -310,6 +311,18 @@ class Client { return resp["room_id"]; } + /// Uploads a new user avatar for this user. Returns ErrorResponse if something went wrong. + Future setAvatar(File file) async { + final uploadResp = await connection.upload(file); + if (uploadResp is ErrorResponse) return uploadResp; + final setAvatarResp = await connection.jsonRequest( + type: HTTPType.PUT, + action: "/client/r0/profile/$userID/avatar_url", + data: {"avatar_url": uploadResp}); + if (setAvatarResp is ErrorResponse) return setAvatarResp; + return null; + } + /// Fetches the pushrules for the logged in user. /// These are needed for notifications on Android Future getPushrules() async { diff --git a/lib/src/Connection.dart b/lib/src/Connection.dart index b781939..6e632df 100644 --- a/lib/src/Connection.dart +++ b/lib/src/Connection.dart @@ -24,11 +24,13 @@ import 'dart:async'; import 'dart:convert'; import 'dart:core'; +import 'dart:io'; import 'package:famedlysdk/src/Room.dart'; import 'package:famedlysdk/src/RoomList.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; +import 'package:mime_type/mime_type.dart'; import 'Client.dart'; import 'User.dart'; @@ -204,18 +206,23 @@ class Connection { /// ``` /// Future jsonRequest( - {HTTPType type, String action, dynamic data = "", int timeout}) async { + {HTTPType type, + String action, + dynamic data = "", + int timeout, + String contentType = "application/json"}) async { if (client.isLogged() == false && client.homeserver == null) throw ("No homeserver specified."); if (timeout == null) timeout = syncTimeoutSec + 5; dynamic json; if (data is Map) data.removeWhere((k, v) => v == null); (!(data is String)) ? json = jsonEncode(data) : json = data; + if (data is List) json = data; final url = "${client.homeserver}/_matrix${action}"; Map headers = { - "Content-type": "application/json", + "Content-Type": contentType, }; if (client.isLogged()) headers["Authorization"] = "Bearer ${client.accessToken}"; @@ -279,6 +286,24 @@ class Connection { return jsonResp; } + /// Uploads a file with the name [fileName] as base64 encoded to the server + /// and returns the mxc url as a string or an [ErrorResponse]. + Future upload(File file) async { + List fileBytes; + if (client.homeserver != "https://fakeServer.notExisting") + fileBytes = await file.readAsBytes(); + String fileName = file.path.split("/").last; + String mimeType = mime(file.path); + print("[UPLOADING] $fileName, type: $mimeType, size: ${fileBytes?.length}"); + final dynamic resp = await jsonRequest( + type: HTTPType.POST, + action: "/media/r0/upload?filename=$fileName", + data: fileBytes, + contentType: mimeType); + if (resp is ErrorResponse) return resp; + return resp["content_uri"]; + } + Future _syncRequest; Future _sync() async { @@ -487,10 +512,10 @@ class _LifecycleEventHandler extends WidgetsBindingObserver { case AppLifecycleState.inactive: case AppLifecycleState.paused: case AppLifecycleState.suspending: - await suspendingCallBack(); + if (suspendingCallBack != null) await suspendingCallBack(); break; case AppLifecycleState.resumed: - await resumeCallBack(); + if (resumeCallBack != null) await resumeCallBack(); break; } } diff --git a/lib/src/Room.dart b/lib/src/Room.dart index 94a6d91..2c89cd5 100644 --- a/lib/src/Room.dart +++ b/lib/src/Room.dart @@ -21,6 +21,8 @@ * along with famedlysdk. If not, see . */ +import 'dart:io'; + import 'package:famedlysdk/src/Client.dart'; import 'package:famedlysdk/src/Event.dart'; import 'package:famedlysdk/src/RoomAccountData.dart'; @@ -29,6 +31,8 @@ import 'package:famedlysdk/src/responses/ErrorResponse.dart'; import 'package:famedlysdk/src/sync/EventUpdate.dart'; import 'package:famedlysdk/src/utils/ChatTime.dart'; import 'package:famedlysdk/src/utils/MxContent.dart'; +import 'package:image/image.dart'; +import 'package:mime_type/mime_type.dart'; import './User.dart'; import 'Connection.dart'; @@ -223,18 +227,100 @@ class Room { return res; } - /// Call the Matrix API to send a simple text message. - Future sendText(String message, {String txid = null}) async { + Future _sendRawEventNow(Map content, + {String txid = null}) async { if (txid == null) txid = "txid${DateTime.now().millisecondsSinceEpoch}"; final dynamic res = await client.connection.jsonRequest( type: HTTPType.PUT, action: "/client/r0/rooms/${id}/send/m.room.message/$txid", - data: {"msgtype": "m.text", "body": message}); + data: content); if (res is ErrorResponse) client.connection.onError.add(res); return res; } - Future sendTextEvent(String message, {String txid = null}) async { + Future sendTextEvent(String message, {String txid = null}) => + sendEvent({"msgtype": "m.text", "body": message}, txid: txid); + + Future sendFileEvent(File file, String msgType, + {String txid = null}) async { + // Try to get the size of the file + int size; + try { + size = (await file.readAsBytes()).length; + } catch (e) { + print("[UPLOAD] Could not get size. Reason: ${e.toString()}"); + } + + // Upload file + String fileName = file.path.split("/").last; + String mimeType = mime(fileName); + final dynamic uploadResp = await client.connection.upload(file); + if (uploadResp is ErrorResponse) return null; + + // Send event + Map content = { + "msgtype": msgType, + "body": fileName, + "filename": fileName, + "url": uploadResp, + "info": { + "mimetype": mimeType, + } + }; + if (size != null) + content["info"] = { + "size": size, + "mimetype": mimeType, + }; + return await sendEvent(content, txid: txid); + } + + Future sendImageEvent(File file, {String txid = null}) async { + String fileName = file.path.split("/").last; + Map info; + + // Try to manipulate the file size and create a thumbnail + try { + Image image = copyResize(decodeImage(file.readAsBytesSync()), width: 800); + Image thumbnail = copyResize(image, width: 236); + + file = File(fileName)..writeAsBytesSync(encodePng(image)); + File thumbnailFile = File(fileName) + ..writeAsBytesSync(encodePng(thumbnail)); + final dynamic uploadThumbnailResp = + await client.connection.upload(thumbnailFile); + if (uploadThumbnailResp is ErrorResponse) throw (uploadThumbnailResp); + info = { + "size": image.getBytes().length, + "mimetype": mime(file.path), + "w": image.width, + "h": image.height, + "thumbnail_url": uploadThumbnailResp, + "thumbnail_info": { + "w": thumbnail.width, + "h": thumbnail.height, + "mimetype": mime(thumbnailFile.path), + "size": thumbnail.getBytes().length + }, + }; + } catch (e) { + print( + "[UPLOAD] Could not create thumbnail of image. Try to upload the unchanged file..."); + } + + final dynamic uploadResp = await client.connection.upload(file); + if (uploadResp is ErrorResponse) return null; + Map content = { + "msgtype": "m.image", + "body": fileName, + "url": uploadResp, + }; + if (info != null) content["info"] = info; + return await sendEvent(content, txid: txid); + } + + Future sendEvent(Map content, + {String txid = null}) async { final String type = "m.room.message"; // Create new transaction id @@ -253,10 +339,7 @@ class Room { "sender": client.userID, "status": 0, "origin_server_ts": now, - "content": { - "msgtype": "m.text", - "body": message, - } + "content": content }); client.connection.onEvent.add(eventUpdate); await client.store?.transaction(() { @@ -265,7 +348,7 @@ class Room { }); // Send the text and on success, store and display a *sent* event. - final dynamic res = await sendText(message, txid: messageID); + final dynamic res = await _sendRawEventNow(content, txid: messageID); if (res is ErrorResponse || !(res["event_id"] is String)) { // On error, set status to -1 @@ -604,4 +687,17 @@ class Room { return powerLevelState.content["users"]; return null; } + + /// Uploads a new user avatar for this room. Returns ErrorResponse if something went wrong + /// and the event ID otherwise. + Future setAvatar(File file) async { + final uploadResp = await client.connection.upload(file); + if (uploadResp is ErrorResponse) return uploadResp; + final setAvatarResp = await client.connection.jsonRequest( + type: HTTPType.PUT, + action: "/client/r0/rooms/$id/state/m.room.avatar/", + data: {"url": uploadResp}); + if (setAvatarResp is ErrorResponse) return setAvatarResp; + return setAvatarResp["event_id"]; + } } diff --git a/pubspec.lock b/pubspec.lock index ef602eb..f6d9c44 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -8,6 +8,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.36.3" + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.10" args: dependency: transitive description: @@ -200,6 +207,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.1.3" + image: + dependency: "direct main" + description: + name: image + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.4" intl: dependency: "direct main" description: @@ -270,6 +284,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.9.6+3" + mime_type: + dependency: "direct main" + description: + name: mime_type + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.4" package_config: dependency: transitive description: @@ -298,6 +319,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.7.0" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "2.4.0" pool: dependency: transitive description: @@ -450,6 +478,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.13" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "3.5.0" yaml: dependency: transitive description: @@ -458,5 +493,5 @@ packages: source: hosted version: "2.1.16" sdks: - dart: ">=2.3.0-dev.0.1 <3.0.0" + dart: ">=2.4.0 <3.0.0" flutter: ">=1.2.1 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 4a3acb4..d56c0c2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,8 @@ dependencies: # Connection http: ^0.12.0+2 + mime_type: ^0.2.4 + image: ^2.1.4 # Time formatting intl: ^0.15.8 diff --git a/test/Client_test.dart b/test/Client_test.dart index 8239e0b..b8b87ce 100644 --- a/test/Client_test.dart +++ b/test/Client_test.dart @@ -22,6 +22,7 @@ */ import 'dart:async'; +import 'dart:io'; import 'package:famedlysdk/src/AccountData.dart'; import 'package:famedlysdk/src/Client.dart'; @@ -270,6 +271,18 @@ void main() { expect(newID, "!1234:fakeServer.notExisting"); }); + test('upload', () async { + final File testFile = File.fromUri(Uri.parse("fake/path/file.jpeg")); + final dynamic resp = await matrix.connection.upload(testFile); + expect(resp, "mxc://example.com/AQwafuaFswefuhsfAFAgsw"); + }); + + test('setAvatar', () async { + final File testFile = File.fromUri(Uri.parse("fake/path/file.jpeg")); + final dynamic resp = await matrix.setAvatar(testFile); + expect(resp, null); + }); + test('getPushrules', () async { final PushrulesResponse pushrules = await matrix.getPushrules(); final PushrulesResponse awaited_resp = PushrulesResponse.fromJson( diff --git a/test/FakeMatrixApi.dart b/test/FakeMatrixApi.dart index 315645d..094ee1a 100644 --- a/test/FakeMatrixApi.dart +++ b/test/FakeMatrixApi.dart @@ -509,6 +509,8 @@ class FakeMatrixApi extends MockClient { "access_token": "abc123", "device_id": "GHTYAJCE" }, + "/media/r0/upload?filename=file.jpeg": (var req) => + {"content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw"}, "/client/r0/logout": (var reqI) => {}, "/client/r0/pushers/set": (var reqI) => {}, "/client/r0/join/1234": (var reqI) => {"room_id": "1234"}, @@ -523,10 +525,18 @@ class FakeMatrixApi extends MockClient { "/client/r0/rooms/!localpart:server.abc/invite": (var reqI) => {}, }, "PUT": { + "/client/r0/rooms/!localpart:server.abc/send/m.room.message/testtxid": + (var reqI) => { + "event_id": "42", + }, "/client/r0/rooms/!1234:example.com/send/m.room.message/1234": (var reqI) => { "event_id": "42", }, + "/client/r0/profile/@test:fakeServer.notExisting/avatar_url": + (var reqI) => {}, + "/client/r0/rooms/!localpart:server.abc/state/m.room.avatar/": + (var reqI) => {"event_id": "YUwRidLecu:example.com"}, "/client/r0/rooms/!localpart:server.abc/state/m.room.name": (var reqI) => { "event_id": "42", diff --git a/test/Room_test.dart b/test/Room_test.dart index e016f6b..3b2efc8 100644 --- a/test/Room_test.dart +++ b/test/Room_test.dart @@ -21,6 +21,8 @@ * along with famedlysdk. If not, see . */ +import 'dart:io'; + import 'package:famedlysdk/src/Client.dart'; import 'package:famedlysdk/src/Event.dart'; import 'package:famedlysdk/src/Room.dart'; @@ -255,5 +257,39 @@ void main() { expect(user.stateKey, "@getme:example.com"); expect(user.calcDisplayname(), "You got me"); }); + + test('setAvatar', () async { + final File testFile = File.fromUri(Uri.parse("fake/path/file.jpeg")); + final dynamic resp = await room.setAvatar(testFile); + expect(resp, "YUwRidLecu:example.com"); + }); + + test('sendEvent', () async { + final dynamic resp = await room.sendEvent( + {"msgtype": "m.text", "body": "hello world"}, + txid: "testtxid"); + expect(resp, "42"); + }); + + test('sendEvent', () async { + final dynamic resp = + await room.sendTextEvent("Hello world", txid: "testtxid"); + expect(resp, "42"); + }); + + // Not working because there is no real file to test it... + test('sendImageEvent', () async { + final File testFile = File.fromUri(Uri.parse("fake/path/file.jpeg")); + final dynamic resp = + await room.sendImageEvent(testFile, txid: "testtxid"); + expect(resp, "42"); + }); + + test('sendFileEvent', () async { + final File testFile = File.fromUri(Uri.parse("fake/path/file.jpeg")); + final dynamic resp = + await room.sendFileEvent(testFile, "m.file", txid: "testtxid"); + expect(resp, "42"); + }); }); }