Initial commit
This commit is contained in:
commit
77be6102f6
70
.gitignore
vendored
Normal file
70
.gitignore
vendored
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Visual Studio Code related
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins
|
||||||
|
.packages
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Android related
|
||||||
|
**/android/**/gradle-wrapper.jar
|
||||||
|
**/android/.gradle
|
||||||
|
**/android/captures/
|
||||||
|
**/android/gradlew
|
||||||
|
**/android/gradlew.bat
|
||||||
|
**/android/local.properties
|
||||||
|
**/android/**/GeneratedPluginRegistrant.java
|
||||||
|
|
||||||
|
# iOS/XCode related
|
||||||
|
**/ios/**/*.mode1v3
|
||||||
|
**/ios/**/*.mode2v3
|
||||||
|
**/ios/**/*.moved-aside
|
||||||
|
**/ios/**/*.pbxuser
|
||||||
|
**/ios/**/*.perspectivev3
|
||||||
|
**/ios/**/*sync/
|
||||||
|
**/ios/**/.sconsign.dblite
|
||||||
|
**/ios/**/.tags*
|
||||||
|
**/ios/**/.vagrant/
|
||||||
|
**/ios/**/DerivedData/
|
||||||
|
**/ios/**/Icon?
|
||||||
|
**/ios/**/Pods/
|
||||||
|
**/ios/**/.symlinks/
|
||||||
|
**/ios/**/profile
|
||||||
|
**/ios/**/xcuserdata
|
||||||
|
**/ios/.generated/
|
||||||
|
**/ios/Flutter/App.framework
|
||||||
|
**/ios/Flutter/Flutter.framework
|
||||||
|
**/ios/Flutter/Generated.xcconfig
|
||||||
|
**/ios/Flutter/app.flx
|
||||||
|
**/ios/Flutter/app.zip
|
||||||
|
**/ios/Flutter/flutter_assets/
|
||||||
|
**/ios/ServiceDefinitions.json
|
||||||
|
**/ios/Runner/GeneratedPluginRegistrant.*
|
||||||
|
|
||||||
|
# Exceptions to above rules.
|
||||||
|
!**/ios/**/default.mode1v3
|
||||||
|
!**/ios/**/default.mode2v3
|
||||||
|
!**/ios/**/default.pbxuser
|
||||||
|
!**/ios/**/default.perspectivev3
|
||||||
|
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
18
.gitlab-ci.yml
Normal file
18
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
image: cirrusci/flutter
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- coverage
|
||||||
|
|
||||||
|
variables:
|
||||||
|
LC_ALL: "en_US.UTF-8"
|
||||||
|
LANG: "en_US.UTF-8"
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
stage: coverage
|
||||||
|
coverage: '/^\s+lines.+: (\d+.\d*%)/'
|
||||||
|
dependencies: []
|
||||||
|
script:
|
||||||
|
- sudo apt-get update -qq && sudo apt-get install -qq apt-transport-https curl gnupg lcov git
|
||||||
|
- ./scripts/test.sh
|
||||||
|
- ./scripts/coverage.sh
|
||||||
|
- flutter pub pub publish --dry-run
|
10
.metadata
Normal file
10
.metadata
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# This file tracks properties of this Flutter project.
|
||||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
|
#
|
||||||
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
|
version:
|
||||||
|
revision: 7a4c33425ddd78c54aba07d86f3f9a4a0051769b
|
||||||
|
channel: stable
|
||||||
|
|
||||||
|
project_type: package
|
84
CHANGELOG.md
Normal file
84
CHANGELOG.md
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
# fluffyfluttermatrix
|
||||||
|
|
||||||
|
Dead simple Flutter widget to use Matrix.org in your Flutter app.
|
||||||
|
|
||||||
|
## How to use this
|
||||||
|
|
||||||
|
1. Use the Matrix widget as root for your widget tree:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fluffyfluttermatrix/fluffyfluttermatrix.dart';
|
||||||
|
|
||||||
|
void main() => runApp(MyApp());
|
||||||
|
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FluffyMatrix(
|
||||||
|
child: MaterialApp(
|
||||||
|
title: 'Welcome to Flutter'
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Access the MatrixState object by calling Matrix.of with your current BuildContext:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Client matrix = Matrix.of(context);
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Connect to a Matrix Homeserver and listen to the streams:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
matrix.homeserver = "https://yourhomeserveraddress";
|
||||||
|
|
||||||
|
matrix.onLoginStateChanged.stream.listen((bool loginState){
|
||||||
|
print("LoginState: ${loginState.toString()}");
|
||||||
|
});
|
||||||
|
|
||||||
|
matrix.onEvent.stream.listen((EventUpdate eventUpdate){
|
||||||
|
print("New event update!");
|
||||||
|
});
|
||||||
|
|
||||||
|
matrix.onRoomUpdate.stream.listen((RoomUpdate eventUpdate){
|
||||||
|
print("New room update!");
|
||||||
|
});
|
||||||
|
|
||||||
|
final loginResp = await matrix.jsonRequest(
|
||||||
|
type: "POST",
|
||||||
|
action: "/client/r0/login",
|
||||||
|
data: {
|
||||||
|
"type": "m.login.password",
|
||||||
|
"user": _usernameController.text,
|
||||||
|
"password": _passwordController.text,
|
||||||
|
"initial_device_display_name": "Fluffy Matrix Client"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
matrix.connect(
|
||||||
|
newToken: loginResp["token"],
|
||||||
|
newUserID: loginResp["user_id"],
|
||||||
|
newHomeserver: matrix.homeserver,
|
||||||
|
newDeviceName: "Fluffy Matrix Client",
|
||||||
|
newDeviceID: loginResp["device_id"],
|
||||||
|
newMatrixVersions: ["r0.4.0"],
|
||||||
|
newLazyLoadMembers: false
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Send a message to a Room:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
final resp = await jsonRequest(
|
||||||
|
type: "PUT",
|
||||||
|
action: "/r0/rooms/!fjd823j:example.com/send/m.room.message/$txnId",
|
||||||
|
data: {
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"body": "hello"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
14
README.md
Normal file
14
README.md
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# famedlysdk
|
||||||
|
|
||||||
|
A new Flutter package.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
This project is a starting point for a Dart
|
||||||
|
[package](https://flutter.dev/developing-packages/),
|
||||||
|
a library module containing code that can be shared easily across
|
||||||
|
multiple Flutter or Dart projects.
|
||||||
|
|
||||||
|
For help getting started with Flutter, view our
|
||||||
|
[online documentation](https://flutter.dev/docs), which offers tutorials,
|
||||||
|
samples, guidance on mobile development, and a full API reference.
|
13
lib/famedlysdk.dart
Normal file
13
lib/famedlysdk.dart
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
library famedlysdk;
|
||||||
|
|
||||||
|
export 'package:famedlysdk/src/responses/ErrorResponse.dart';
|
||||||
|
export 'package:famedlysdk/src/sync/RoomUpdate.dart';
|
||||||
|
export 'package:famedlysdk/src/sync/EventUpdate.dart';
|
||||||
|
export 'package:famedlysdk/src/utils/ChatTime.dart';
|
||||||
|
export 'package:famedlysdk/src/utils/MxContent.dart';
|
||||||
|
export 'package:famedlysdk/src/Client.dart';
|
||||||
|
export 'package:famedlysdk/src/Connection.dart';
|
||||||
|
export 'package:famedlysdk/src/Event.dart';
|
||||||
|
export 'package:famedlysdk/src/Room.dart';
|
||||||
|
export 'package:famedlysdk/src/Store.dart';
|
||||||
|
export 'package:famedlysdk/src/User.dart';
|
169
lib/src/Client.dart
Normal file
169
lib/src/Client.dart
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:core';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'responses/ErrorResponse.dart';
|
||||||
|
import 'Connection.dart';
|
||||||
|
import 'Store.dart';
|
||||||
|
|
||||||
|
/// Represents a Matrix connection to communicate with a
|
||||||
|
/// [Matrix](https://matrix.org) homeserver and is the entry point for this
|
||||||
|
/// SDK.
|
||||||
|
class Client {
|
||||||
|
|
||||||
|
/// Handles the connection for this client.
|
||||||
|
Connection connection;
|
||||||
|
|
||||||
|
/// Optional persistent store for all data.
|
||||||
|
Store store;
|
||||||
|
|
||||||
|
Client(this.clientName) {
|
||||||
|
connection = Connection(this);
|
||||||
|
|
||||||
|
if (this.clientName != "testclient")
|
||||||
|
store = Store(this);
|
||||||
|
connection.onLoginStateChanged.stream.listen((loginState) {
|
||||||
|
print("LoginState: ${loginState.toString()}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The required name for this client.
|
||||||
|
final String clientName;
|
||||||
|
|
||||||
|
/// The homeserver this client is communicating with.
|
||||||
|
String homeserver;
|
||||||
|
|
||||||
|
/// The Matrix ID of the current logged user.
|
||||||
|
String userID;
|
||||||
|
|
||||||
|
/// This is the access token for the matrix client. When it is undefined, then
|
||||||
|
/// the user needs to sign in first.
|
||||||
|
String accessToken;
|
||||||
|
|
||||||
|
/// This points to the position in the synchronization history.
|
||||||
|
String prevBatch;
|
||||||
|
|
||||||
|
/// The device ID is an unique identifier for this device.
|
||||||
|
String deviceID;
|
||||||
|
|
||||||
|
/// The device name is a human readable identifier for this device.
|
||||||
|
String deviceName;
|
||||||
|
|
||||||
|
/// Which version of the matrix specification does this server support?
|
||||||
|
List<String> matrixVersions;
|
||||||
|
|
||||||
|
/// Wheither the server supports lazy load members.
|
||||||
|
bool lazyLoadMembers = false;
|
||||||
|
|
||||||
|
/// Returns the current login state.
|
||||||
|
bool isLogged() => accessToken != null;
|
||||||
|
|
||||||
|
/// Checks the supported versions of the Matrix protocol and the supported
|
||||||
|
/// login types. Returns false if the server is not compatible with the
|
||||||
|
/// client. Automatically sets [matrixVersions] and [lazyLoadMembers].
|
||||||
|
Future<bool> checkServer(serverUrl) async {
|
||||||
|
homeserver = serverUrl;
|
||||||
|
|
||||||
|
final versionResp =
|
||||||
|
await connection.jsonRequest(type: "GET", action: "/client/versions");
|
||||||
|
if (versionResp is ErrorResponse) {
|
||||||
|
connection.onError.add(ErrorResponse(errcode: "NO_RESPONSE", error: ""));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<String> versions = List<String>.from(versionResp["versions"]);
|
||||||
|
|
||||||
|
if (versions == null) {
|
||||||
|
connection.onError.add(ErrorResponse(errcode: "NO_RESPONSE", error: ""));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < versions.length; i++) {
|
||||||
|
if (versions[i] == "r0.4.0")
|
||||||
|
break;
|
||||||
|
else if (i == versions.length - 1) {
|
||||||
|
connection.onError.add(ErrorResponse(errcode: "NO_SUPPORT", error: ""));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
matrixVersions = versions;
|
||||||
|
|
||||||
|
if (versionResp.containsKey("unstable_features") &&
|
||||||
|
versionResp["unstable_features"].containsKey("m.lazy_load_members")) {
|
||||||
|
lazyLoadMembers = versionResp["unstable_features"]["m.lazy_load_members"]
|
||||||
|
? true
|
||||||
|
: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final loginResp =
|
||||||
|
await connection.jsonRequest(type: "GET", action: "/client/r0/login");
|
||||||
|
if (loginResp is ErrorResponse) {
|
||||||
|
connection.onError.add(loginResp);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<dynamic> flows = loginResp["flows"];
|
||||||
|
|
||||||
|
for (int i = 0; i < flows.length; i++) {
|
||||||
|
if (flows[i].containsKey("type") &&
|
||||||
|
flows[i]["type"] == "m.login.password")
|
||||||
|
break;
|
||||||
|
else if (i == flows.length - 1) {
|
||||||
|
connection.onError.add(ErrorResponse(errcode: "NO_SUPPORT", error: ""));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles the login and allows the client to call all APIs which require
|
||||||
|
/// authentication. Returns false if the login was not successful.
|
||||||
|
Future<bool> login(String username, String password) async {
|
||||||
|
|
||||||
|
final loginResp =
|
||||||
|
await connection.jsonRequest(type: "POST", action: "/client/r0/login", data: {
|
||||||
|
"type": "m.login.password",
|
||||||
|
"user": username,
|
||||||
|
"identifier": {
|
||||||
|
"type": "m.id.user",
|
||||||
|
"user": username,
|
||||||
|
},
|
||||||
|
"password": password,
|
||||||
|
"initial_device_display_name": "Famedly Talk"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loginResp is ErrorResponse) {
|
||||||
|
connection.onError.add(loginResp);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final userID = loginResp["user_id"];
|
||||||
|
final accessToken = loginResp["access_token"];
|
||||||
|
if (userID == null || accessToken == null) {
|
||||||
|
connection.onError.add(ErrorResponse(errcode: "NO_SUPPORT", error: ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
await connection.connect(
|
||||||
|
newToken: accessToken,
|
||||||
|
newUserID: userID,
|
||||||
|
newHomeserver: homeserver,
|
||||||
|
newDeviceName: "",
|
||||||
|
newDeviceID: "",
|
||||||
|
newMatrixVersions: matrixVersions,
|
||||||
|
newLazyLoadMembers: lazyLoadMembers);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends a logout command to the homeserver and clears all local data,
|
||||||
|
/// including all persistent data from the store.
|
||||||
|
Future<void> logout() async {
|
||||||
|
final dynamic resp =
|
||||||
|
await connection.jsonRequest(type: "POST", action: "/client/r0/logout/all");
|
||||||
|
if (resp == null) return;
|
||||||
|
|
||||||
|
await connection.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
415
lib/src/Connection.dart
Normal file
415
lib/src/Connection.dart
Normal file
|
@ -0,0 +1,415 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:core';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'responses/ErrorResponse.dart';
|
||||||
|
import 'sync/EventUpdate.dart';
|
||||||
|
import 'sync/RoomUpdate.dart';
|
||||||
|
import 'Client.dart';
|
||||||
|
|
||||||
|
/// Represents a Matrix connection to communicate with a
|
||||||
|
/// [Matrix](https://matrix.org) homeserver.
|
||||||
|
class Connection {
|
||||||
|
final Client client;
|
||||||
|
|
||||||
|
Connection(this.client) {
|
||||||
|
WidgetsBinding.instance
|
||||||
|
.addObserver(_LifecycleEventHandler(resumeCallBack: () {
|
||||||
|
_sync();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
String get _syncFilters =>
|
||||||
|
"{\"room\":{\"state\":{\"lazy_load_members\":${client.lazyLoadMembers ? "1" : "0"}}}";
|
||||||
|
|
||||||
|
/// Handles the connection to the Matrix Homeserver. You can change this to a
|
||||||
|
/// MockClient for testing.
|
||||||
|
http.Client httpClient = http.Client();
|
||||||
|
|
||||||
|
/// The newEvent signal is the most important signal in this concept. Every time
|
||||||
|
/// the app receives a new synchronization, this event is called for every signal
|
||||||
|
/// to update the GUI. For example, for a new message, it is called:
|
||||||
|
/// onRoomEvent( "m.room.message", "!chat_id:server.com", "timeline", {sender: "@bob:server.com", body: "Hello world"} )
|
||||||
|
final StreamController<EventUpdate> onEvent =
|
||||||
|
new StreamController.broadcast();
|
||||||
|
|
||||||
|
/// Outside of the events there are updates for the global chat states which
|
||||||
|
/// are handled by this signal:
|
||||||
|
final StreamController<RoomUpdate> onRoomUpdate =
|
||||||
|
new StreamController.broadcast();
|
||||||
|
|
||||||
|
/// Called when the login state e.g. user gets logged out.
|
||||||
|
final StreamController<LoginState> onLoginStateChanged =
|
||||||
|
new StreamController.broadcast();
|
||||||
|
|
||||||
|
/// Synchronization erros are coming here.
|
||||||
|
final StreamController<ErrorResponse> onError =
|
||||||
|
new StreamController.broadcast();
|
||||||
|
|
||||||
|
/// This is called once, when the first sync has received.
|
||||||
|
final StreamController<bool> onFirstSync = new StreamController.broadcast();
|
||||||
|
|
||||||
|
/// When a new sync response is coming in, this gives the complete payload.
|
||||||
|
final StreamController<dynamic> onSync = new StreamController.broadcast();
|
||||||
|
|
||||||
|
/// Matrix synchronisation is done with https long polling. This needs a
|
||||||
|
/// timeout which is usually 30 seconds.
|
||||||
|
int syncTimeoutSec = 30;
|
||||||
|
|
||||||
|
/// How long should the app wait until it retrys the synchronisation after
|
||||||
|
/// an error?
|
||||||
|
int syncErrorTimeoutSec = 3;
|
||||||
|
|
||||||
|
/// Sets the user credentials and starts the synchronisation.
|
||||||
|
///
|
||||||
|
/// Before you can connect you need at least an [accessToken], a [homeserver],
|
||||||
|
/// a [userID], a [deviceID], and a [deviceName].
|
||||||
|
///
|
||||||
|
/// You get this informations
|
||||||
|
/// by logging in to your Matrix account, using the [login API](https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-login).
|
||||||
|
///
|
||||||
|
/// To log in you can use [jsonRequest()] after you have set the [homeserver]
|
||||||
|
/// to a valid url. For example:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// final resp = await matrix
|
||||||
|
/// .jsonRequest(type: "POST", action: "/client/r0/login", data: {
|
||||||
|
/// "type": "m.login.password",
|
||||||
|
/// "user": "test",
|
||||||
|
/// "password": "1234",
|
||||||
|
/// "initial_device_display_name": "Fluffy Matrix Client"
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// {
|
||||||
|
/// "user_id": "@cheeky_monkey:matrix.org",
|
||||||
|
/// "access_token": "abc123",
|
||||||
|
/// "device_id": "GHTYAJCE"
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Sends [LoginState.logged] to [onLoginStateChanged].
|
||||||
|
void connect(
|
||||||
|
{@required String newToken,
|
||||||
|
@required String newHomeserver,
|
||||||
|
@required String newUserID,
|
||||||
|
@required String newDeviceName,
|
||||||
|
@required String newDeviceID,
|
||||||
|
List<String> newMatrixVersions,
|
||||||
|
bool newLazyLoadMembers,
|
||||||
|
String newPrevBatch}) async {
|
||||||
|
client.accessToken = newToken;
|
||||||
|
client.homeserver = newHomeserver;
|
||||||
|
client.userID = newUserID;
|
||||||
|
client.deviceID = newDeviceID;
|
||||||
|
client.deviceName = newDeviceName;
|
||||||
|
client.matrixVersions = newMatrixVersions;
|
||||||
|
client.lazyLoadMembers = newLazyLoadMembers;
|
||||||
|
client.prevBatch = newPrevBatch;
|
||||||
|
|
||||||
|
client.store?.storeClient();
|
||||||
|
|
||||||
|
onLoginStateChanged.add(LoginState.logged);
|
||||||
|
|
||||||
|
_sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resets all settings and stops the synchronisation.
|
||||||
|
void clear() {
|
||||||
|
client.store?.clear();
|
||||||
|
client.accessToken = client.homeserver = client.userID = client.deviceID =
|
||||||
|
client.deviceName = client.matrixVersions =
|
||||||
|
client.lazyLoadMembers = client.prevBatch = null;
|
||||||
|
onLoginStateChanged.add(LoginState.loggedOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used for all Matrix json requests using the [c2s API](https://matrix.org/docs/spec/client_server/r0.4.0.html).
|
||||||
|
///
|
||||||
|
/// You must first call [this.connect()] or set [this.homeserver] before you can use
|
||||||
|
/// this! For example to send a message to a Matrix room with the id
|
||||||
|
/// '!fjd823j:example.com' you call:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// final resp = await jsonRequest(
|
||||||
|
/// type: "PUT",
|
||||||
|
/// action: "/r0/rooms/!fjd823j:example.com/send/m.room.message/$txnId",
|
||||||
|
/// data: {
|
||||||
|
/// "msgtype": "m.text",
|
||||||
|
/// "body": "hello"
|
||||||
|
/// }
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
Future<dynamic> jsonRequest(
|
||||||
|
{String type, String action, dynamic data = "", int timeout}) async {
|
||||||
|
if (client.isLogged() == false && client.homeserver == null)
|
||||||
|
throw ("No homeserver specified.");
|
||||||
|
if (timeout == null) timeout = syncTimeoutSec;
|
||||||
|
if (!(data is String)) data = jsonEncode(data);
|
||||||
|
|
||||||
|
final url = "${client.homeserver}/_matrix${action}";
|
||||||
|
|
||||||
|
Map<String, String> headers = {
|
||||||
|
"Content-type": "application/json",
|
||||||
|
};
|
||||||
|
if (client.isLogged())
|
||||||
|
headers["Authorization"] = "Bearer ${client.accessToken}";
|
||||||
|
|
||||||
|
var resp;
|
||||||
|
try {
|
||||||
|
switch (type) {
|
||||||
|
case "GET":
|
||||||
|
resp = await httpClient
|
||||||
|
.get(url, headers: headers)
|
||||||
|
.timeout(Duration(seconds: timeout));
|
||||||
|
break;
|
||||||
|
case "POST":
|
||||||
|
resp = await httpClient
|
||||||
|
.post(url, body: data, headers: headers)
|
||||||
|
.timeout(Duration(seconds: timeout));
|
||||||
|
break;
|
||||||
|
case "PUT":
|
||||||
|
resp = await httpClient
|
||||||
|
.put(url, body: data, headers: headers)
|
||||||
|
.timeout(Duration(seconds: timeout));
|
||||||
|
break;
|
||||||
|
case "DELETE":
|
||||||
|
resp = await httpClient
|
||||||
|
.delete(url, headers: headers)
|
||||||
|
.timeout(Duration(seconds: timeout));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} on TimeoutException catch (_) {
|
||||||
|
return ErrorResponse(
|
||||||
|
error: "No connection possible...", errcode: "TIMEOUT");
|
||||||
|
} catch (e) {
|
||||||
|
return ErrorResponse(
|
||||||
|
error: "No connection possible...", errcode: "NO_CONNECTION");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> jsonResp;
|
||||||
|
try {
|
||||||
|
jsonResp = jsonDecode(resp.body) as Map<String, dynamic>;
|
||||||
|
} catch (e) {
|
||||||
|
return ErrorResponse(
|
||||||
|
error: "No connection possible...", errcode: "MALFORMED");
|
||||||
|
}
|
||||||
|
if (jsonResp.containsKey("errcode") && jsonResp["errcode"] is String) {
|
||||||
|
if (jsonResp["errcode"] == "M_UNKNOWN_TOKEN") clear();
|
||||||
|
return ErrorResponse.fromJson(jsonResp);
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonResp;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> _syncRequest;
|
||||||
|
|
||||||
|
Future<void> _sync() async {
|
||||||
|
if (client.isLogged() == false) return;
|
||||||
|
|
||||||
|
dynamic args = {};
|
||||||
|
|
||||||
|
String action = "/client/r0/sync?filters=${_syncFilters}";
|
||||||
|
|
||||||
|
if (client.prevBatch != null) {
|
||||||
|
action += "&timeout=30000";
|
||||||
|
action += "&since=${client.prevBatch}";
|
||||||
|
}
|
||||||
|
_syncRequest = jsonRequest(type: "GET", action: action);
|
||||||
|
final int hash = _syncRequest.hashCode;
|
||||||
|
final syncResp = await _syncRequest;
|
||||||
|
if (syncResp is ErrorResponse) {
|
||||||
|
onError.add(syncResp);
|
||||||
|
await Future.delayed(Duration(seconds: syncErrorTimeoutSec), () {});
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
if (client.store != null)
|
||||||
|
await client.store.transaction(() {
|
||||||
|
_handleSync(syncResp);
|
||||||
|
client.store.storePrevBatch(syncResp);
|
||||||
|
});
|
||||||
|
else
|
||||||
|
await _handleSync(syncResp);
|
||||||
|
if (client.prevBatch == null) client.connection.onFirstSync.add(true);
|
||||||
|
client.prevBatch = syncResp["next_batch"];
|
||||||
|
} catch (e) {
|
||||||
|
onError
|
||||||
|
.add(ErrorResponse(errcode: "CRITICAL_ERROR", error: e.toString()));
|
||||||
|
await Future.delayed(Duration(seconds: syncErrorTimeoutSec), () {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hash == _syncRequest.hashCode) _sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSync(dynamic sync) {
|
||||||
|
if (sync["rooms"] is Map<String, dynamic>) {
|
||||||
|
if (sync["rooms"]["join"] is Map<String, dynamic>)
|
||||||
|
_handleRooms(sync["rooms"]["join"], "join");
|
||||||
|
if (sync["rooms"]["invite"] is Map<String, dynamic>)
|
||||||
|
_handleRooms(sync["rooms"]["invite"], "invite");
|
||||||
|
if (sync["rooms"]["leave"] is Map<String, dynamic>)
|
||||||
|
_handleRooms(sync["rooms"]["leave"], "leave");
|
||||||
|
}
|
||||||
|
if (sync["presence"] is Map<String, dynamic> &&
|
||||||
|
sync["presence"]["events"] is List<dynamic>) {
|
||||||
|
_handleGlobalEvents(sync["presence"]["events"], "presence");
|
||||||
|
}
|
||||||
|
if (sync["account_data"] is Map<String, dynamic> &&
|
||||||
|
sync["account_data"]["events"] is List<dynamic>) {
|
||||||
|
_handleGlobalEvents(sync["account_data"]["events"], "account_data");
|
||||||
|
}
|
||||||
|
if (sync["to_device"] is Map<String, dynamic> &&
|
||||||
|
sync["to_device"]["events"] is List<dynamic>) {
|
||||||
|
_handleGlobalEvents(sync["to_device"]["events"], "to_device");
|
||||||
|
}
|
||||||
|
onSync.add(sync);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleRooms(Map<String, dynamic> rooms, String membership) {
|
||||||
|
rooms.forEach((String id, dynamic room) async {
|
||||||
|
// calculate the notification counts, the limitedTimeline and prevbatch
|
||||||
|
num highlight_count = 0;
|
||||||
|
num notification_count = 0;
|
||||||
|
String prev_batch = "";
|
||||||
|
bool limitedTimeline = false;
|
||||||
|
|
||||||
|
if (room["unread_notifications"] is Map<String, dynamic>) {
|
||||||
|
if (room["unread_notifications"]["highlight_count"] is num)
|
||||||
|
highlight_count = room["unread_notifications"]["highlight_count"];
|
||||||
|
if (room["unread_notifications"]["notification_count"] is num)
|
||||||
|
notification_count =
|
||||||
|
room["unread_notifications"]["notification_count"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (room["timeline"] is Map<String, dynamic>) {
|
||||||
|
if (room["timeline"]["limited"] is bool)
|
||||||
|
limitedTimeline = room["timeline"]["limited"];
|
||||||
|
if (room["timeline"]["prev_batch"] is String)
|
||||||
|
prev_batch = room["timeline"]["prev_batch"];
|
||||||
|
}
|
||||||
|
|
||||||
|
RoomUpdate update = RoomUpdate(
|
||||||
|
id: id,
|
||||||
|
membership: membership,
|
||||||
|
notification_count: notification_count,
|
||||||
|
highlight_count: highlight_count,
|
||||||
|
limitedTimeline: limitedTimeline,
|
||||||
|
prev_batch: prev_batch,
|
||||||
|
);
|
||||||
|
client.store?.storeRoomUpdate(update);
|
||||||
|
onRoomUpdate.add(update);
|
||||||
|
|
||||||
|
/// Handle now all room events and save them in the database
|
||||||
|
if (room["state"] is Map<String, dynamic> &&
|
||||||
|
room["state"]["events"] is List<dynamic>)
|
||||||
|
_handleRoomEvents(id, room["state"]["events"], "state");
|
||||||
|
|
||||||
|
if (room["invite_state"] is Map<String, dynamic> &&
|
||||||
|
room["invite_state"]["events"] is List<dynamic>)
|
||||||
|
_handleRoomEvents(
|
||||||
|
id, room["invite_state"]["events"], "invite_state");
|
||||||
|
|
||||||
|
if (room["timeline"] is Map<String, dynamic> &&
|
||||||
|
room["timeline"]["events"] is List<dynamic>)
|
||||||
|
_handleRoomEvents(id, room["timeline"]["events"], "timeline");
|
||||||
|
|
||||||
|
if (room["ephemetal"] is Map<String, dynamic> &&
|
||||||
|
room["ephemetal"]["events"] is List<dynamic>)
|
||||||
|
_handleEphemerals(id, room["ephemetal"]["events"]);
|
||||||
|
|
||||||
|
if (room["account_data"] is Map<String, dynamic> &&
|
||||||
|
room["account_data"]["events"] is List<dynamic>)
|
||||||
|
_handleRoomEvents(
|
||||||
|
id, room["account_data"]["events"], "account_data");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleEphemerals(String id, List<dynamic> events) {
|
||||||
|
for (num i = 0; i < events.length; i++) {
|
||||||
|
if (!(events[i]["type"] is String &&
|
||||||
|
events[i]["content"] is Map<String, dynamic>)) continue;
|
||||||
|
if (events[i]["type"] == "m.receipt") {
|
||||||
|
events[i]["content"].forEach((String e, dynamic value) {
|
||||||
|
if (!(events[i]["content"][e] is Map<String, dynamic> &&
|
||||||
|
events[i]["content"][e]["m.read"] is Map<String, dynamic>))
|
||||||
|
return;
|
||||||
|
events[i]["content"][e]["m.read"]
|
||||||
|
.forEach((String user, dynamic value) async {
|
||||||
|
if (!(events[i]["content"][e]["m.read"]["user"]
|
||||||
|
is Map<String, dynamic> &&
|
||||||
|
events[i]["content"][e]["m.read"]["ts"] is num)) return;
|
||||||
|
|
||||||
|
num timestamp = events[i]["content"][e]["m.read"]["ts"];
|
||||||
|
|
||||||
|
_handleEvent(events[i], id, "ephemeral");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (events[i]["type"] == "m.typing") {
|
||||||
|
if (!(events[i]["content"]["user_ids"] is List<String>)) continue;
|
||||||
|
|
||||||
|
List<String> user_ids = events[i]["content"]["user_ids"];
|
||||||
|
|
||||||
|
/// If the user is typing, remove his id from the list of typing users
|
||||||
|
var ownTyping = user_ids.indexOf(client.userID);
|
||||||
|
if (ownTyping != -1) user_ids.removeAt(1);
|
||||||
|
|
||||||
|
_handleEvent(events[i], id, "ephemeral");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleRoomEvents(
|
||||||
|
String chat_id, List<dynamic> events, String type) {
|
||||||
|
for (num i = 0; i < events.length; i++) {
|
||||||
|
_handleEvent(events[i], chat_id, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleGlobalEvents(List<dynamic> events, String type) {
|
||||||
|
for (int i = 0; i < events.length; i++)
|
||||||
|
_handleEvent(events[i], type, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleEvent(
|
||||||
|
Map<String, dynamic> event, String roomID, String type) {
|
||||||
|
if (event["type"] is String && event["content"] is dynamic) {
|
||||||
|
EventUpdate update = EventUpdate(
|
||||||
|
eventType: event["type"],
|
||||||
|
roomID: roomID,
|
||||||
|
type: type,
|
||||||
|
content: event,
|
||||||
|
);
|
||||||
|
client.store?.storeEventUpdate(update);
|
||||||
|
onEvent.add(update);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LifecycleEventHandler extends WidgetsBindingObserver {
|
||||||
|
_LifecycleEventHandler({this.resumeCallBack, this.suspendingCallBack});
|
||||||
|
|
||||||
|
final _FutureVoidCallback resumeCallBack;
|
||||||
|
final _FutureVoidCallback suspendingCallBack;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Null> didChangeAppLifecycleState(AppLifecycleState state) async {
|
||||||
|
switch (state) {
|
||||||
|
case AppLifecycleState.inactive:
|
||||||
|
case AppLifecycleState.paused:
|
||||||
|
case AppLifecycleState.suspending:
|
||||||
|
await suspendingCallBack();
|
||||||
|
break;
|
||||||
|
case AppLifecycleState.resumed:
|
||||||
|
await resumeCallBack();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef _FutureVoidCallback = Future<void> Function();
|
||||||
|
|
||||||
|
enum LoginState { logged, loggedOut }
|
120
lib/src/Event.dart
Normal file
120
lib/src/Event.dart
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import './User.dart';
|
||||||
|
import 'package:famedlysdk/src/utils/ChatTime.dart';
|
||||||
|
import 'package:famedlysdk/src/Client.dart';
|
||||||
|
|
||||||
|
class Event {
|
||||||
|
final String id;
|
||||||
|
final String roomID;
|
||||||
|
final ChatTime time;
|
||||||
|
final User sender;
|
||||||
|
final User stateKey;
|
||||||
|
final String environment;
|
||||||
|
final String text;
|
||||||
|
final String formattedText;
|
||||||
|
final int status;
|
||||||
|
final Map<String,dynamic> content;
|
||||||
|
|
||||||
|
const Event(this.id, this.sender, this.time,{
|
||||||
|
this.roomID,
|
||||||
|
this.stateKey,
|
||||||
|
this.text,
|
||||||
|
this.formattedText,
|
||||||
|
this.status = 2,
|
||||||
|
this.environment = "timeline",
|
||||||
|
this.content,
|
||||||
|
});
|
||||||
|
|
||||||
|
String getBody () => formattedText ?? text ?? "*** Unable to parse Content ***";
|
||||||
|
|
||||||
|
EventTypes get type {
|
||||||
|
switch (environment) {
|
||||||
|
case "m.room.avatar": return EventTypes.RoomAvatar;
|
||||||
|
case "m.room.name": return EventTypes.RoomName;
|
||||||
|
case "m.room.topic": return EventTypes.RoomTopic;
|
||||||
|
case "m.room.Aliases": return EventTypes.RoomAliases;
|
||||||
|
case "m.room.canonical_alias": return EventTypes.RoomCanonicalAlias;
|
||||||
|
case "m.room.create": return EventTypes.RoomCreate;
|
||||||
|
case "m.room.join_rules": return EventTypes.RoomJoinRules;
|
||||||
|
case "m.room.member": return EventTypes.RoomMember;
|
||||||
|
case "m.room.power_levels": return EventTypes.RoomPowerLevels;
|
||||||
|
case "m.room.message":
|
||||||
|
switch(content["msgtype"] ?? "m.text") {
|
||||||
|
case "m.text": return EventTypes.Text;
|
||||||
|
case "m.notice": return EventTypes.Notice;
|
||||||
|
case "m.emote": return EventTypes.Emote;
|
||||||
|
case "m.image": return EventTypes.Image;
|
||||||
|
case "m.video": return EventTypes.Video;
|
||||||
|
case "m.audio": return EventTypes.Audio;
|
||||||
|
case "m.file": return EventTypes.File;
|
||||||
|
case "m.location": return EventTypes.Location;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static Event fromJson(Map<String, dynamic> jsonObj) {
|
||||||
|
Map<String,dynamic> content;
|
||||||
|
try {
|
||||||
|
content = json.decode(jsonObj["content_json"]);
|
||||||
|
} catch(e) {
|
||||||
|
print("jsonObj decode of event content failed: ${e.toString()}");
|
||||||
|
content = {};
|
||||||
|
}
|
||||||
|
return Event(
|
||||||
|
jsonObj["id"],
|
||||||
|
User.fromJson(jsonObj),
|
||||||
|
ChatTime(jsonObj["origin_server_ts"]),
|
||||||
|
stateKey: User(jsonObj["state_key"]),
|
||||||
|
environment: jsonObj["type"],
|
||||||
|
text: jsonObj["content_body"],
|
||||||
|
status: jsonObj["status"],
|
||||||
|
content: content,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<Event>> getEventList(Client matrix, String roomID) async{
|
||||||
|
List<Map<String, dynamic>> eventRes = await matrix.store.db.rawQuery(
|
||||||
|
"SELECT * " +
|
||||||
|
" FROM Events events, Memberships memberships " +
|
||||||
|
" WHERE events.chat_id=?" +
|
||||||
|
" AND events.sender=memberships.matrix_id " +
|
||||||
|
" GROUP BY events.id " +
|
||||||
|
" ORDER BY origin_server_ts DESC",
|
||||||
|
[roomID]);
|
||||||
|
|
||||||
|
List<Event> eventList = [];
|
||||||
|
|
||||||
|
for (num i = 0; i < eventRes.length; i++)
|
||||||
|
eventList.add(Event.fromJson(eventRes[i]));
|
||||||
|
return eventList;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
enum EventTypes {
|
||||||
|
Text,
|
||||||
|
Emote,
|
||||||
|
Notice,
|
||||||
|
Image,
|
||||||
|
Video,
|
||||||
|
Audio,
|
||||||
|
File,
|
||||||
|
Location,
|
||||||
|
RoomAliases,
|
||||||
|
RoomCanonicalAlias,
|
||||||
|
RoomCreate,
|
||||||
|
RoomJoinRules,
|
||||||
|
RoomMember,
|
||||||
|
RoomPowerLevels,
|
||||||
|
RoomName,
|
||||||
|
RoomTopic,
|
||||||
|
RoomAvatar,
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String,int> StatusTypes = {
|
||||||
|
"ERROR": -1,
|
||||||
|
"SENDING": 0,
|
||||||
|
"SENT": 1,
|
||||||
|
"RECEIVED": 2,
|
||||||
|
};
|
197
lib/src/Room.dart
Normal file
197
lib/src/Room.dart
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'package:famedlysdk/src/Client.dart';
|
||||||
|
import 'package:famedlysdk/src/utils/ChatTime.dart';
|
||||||
|
import 'package:famedlysdk/src/utils/MxContent.dart';
|
||||||
|
import 'package:famedlysdk/src/responses/ErrorResponse.dart';
|
||||||
|
import './User.dart';
|
||||||
|
import 'package:famedlysdk/src/Event.dart';
|
||||||
|
|
||||||
|
/// FIXME use actual Matrix Stuff. This is a placeholder
|
||||||
|
class Room {
|
||||||
|
final String roomID;
|
||||||
|
String name;
|
||||||
|
String lastMessage;
|
||||||
|
MxContent avatar;
|
||||||
|
ChatTime timeCreated;
|
||||||
|
int notificationCount;
|
||||||
|
int highlightCount;
|
||||||
|
String topic;
|
||||||
|
User user;
|
||||||
|
final Client matrix;
|
||||||
|
List<Event> events = [];
|
||||||
|
|
||||||
|
Room({
|
||||||
|
this.roomID,
|
||||||
|
this.name,
|
||||||
|
this.lastMessage,
|
||||||
|
this.avatar,
|
||||||
|
this.timeCreated,
|
||||||
|
this.notificationCount,
|
||||||
|
this.highlightCount,
|
||||||
|
this.topic,
|
||||||
|
this.user,
|
||||||
|
this.matrix,
|
||||||
|
this.events,
|
||||||
|
});
|
||||||
|
|
||||||
|
String get status {
|
||||||
|
if (this.user != null) {
|
||||||
|
return this.user.status;
|
||||||
|
}
|
||||||
|
return this.topic;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> setName(String newName) async{
|
||||||
|
dynamic res = await matrix.connection.jsonRequest(
|
||||||
|
type: "PUT",
|
||||||
|
action:
|
||||||
|
"/client/r0/rooms/${roomID}/send/m.room.name/${new DateTime.now()}",
|
||||||
|
data: {"name": newName});
|
||||||
|
if (res is ErrorResponse) matrix.connection.onError.add(res);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> setDescription(String newName) async{
|
||||||
|
dynamic res = await matrix.connection.jsonRequest(
|
||||||
|
type: "PUT",
|
||||||
|
action:
|
||||||
|
"/client/r0/rooms/${roomID}/send/m.room.topic/${new DateTime.now()}",
|
||||||
|
data: {"topic": newName});
|
||||||
|
if (res is ErrorResponse) matrix.connection.onError.add(res);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<Event>> get eventsStream {
|
||||||
|
return Stream<List<Event>>.fromIterable(Iterable<List<Event>>.generate(
|
||||||
|
this.events.length, (int index) => this.events)).asBroadcastStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> sendText(String message) async {
|
||||||
|
dynamic res = await matrix.connection.jsonRequest(
|
||||||
|
type: "PUT",
|
||||||
|
action:
|
||||||
|
"/client/r0/rooms/${roomID}/send/m.room.message/${new DateTime.now()}",
|
||||||
|
data: {"msgtype": "m.text", "body": message});
|
||||||
|
if (res["errcode"] == "M_LIMIT_EXCEEDED") matrix.connection.onError.add(res["error"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> leave() async {
|
||||||
|
dynamic res = await matrix.connection.jsonRequest(
|
||||||
|
type: "POST",
|
||||||
|
action:
|
||||||
|
"/client/r0/rooms/${roomID}/leave");
|
||||||
|
if (res is ErrorResponse) matrix.connection.onError.add(res);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> forget() async {
|
||||||
|
dynamic res = await matrix.connection.jsonRequest(
|
||||||
|
type: "POST",
|
||||||
|
action:
|
||||||
|
"/client/r0/rooms/${roomID}/forget");
|
||||||
|
if (res is ErrorResponse) matrix.connection.onError.add(res);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> kick(String userID) async {
|
||||||
|
dynamic res = await matrix.connection.jsonRequest(
|
||||||
|
type: "POST",
|
||||||
|
action:
|
||||||
|
"/client/r0/rooms/${roomID}/kick",
|
||||||
|
data: {"user_id": userID});
|
||||||
|
if (res is ErrorResponse) matrix.connection.onError.add(res);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> ban(String userID) async {
|
||||||
|
dynamic res = await matrix.connection.jsonRequest(
|
||||||
|
type: "POST",
|
||||||
|
action:
|
||||||
|
"/client/r0/rooms/${roomID}/ban",
|
||||||
|
data: {"user_id": userID});
|
||||||
|
if (res is ErrorResponse) matrix.connection.onError.add(res);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> unban(String userID) async {
|
||||||
|
dynamic res = await matrix.connection.jsonRequest(
|
||||||
|
type: "POST",
|
||||||
|
action:
|
||||||
|
"/client/r0/rooms/${roomID}/unban",
|
||||||
|
data: {"user_id": userID});
|
||||||
|
if (res is ErrorResponse) matrix.connection.onError.add(res);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> invite(String userID) async {
|
||||||
|
dynamic res = await matrix.connection.jsonRequest(
|
||||||
|
type: "POST",
|
||||||
|
action:
|
||||||
|
"/client/r0/rooms/${roomID}/invite",
|
||||||
|
data: {"user_id": userID});
|
||||||
|
if (res is ErrorResponse) matrix.connection.onError.add(res);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Room> getRoomFromTableRow(
|
||||||
|
Map<String, dynamic> row, Client matrix) async {
|
||||||
|
String name = row["topic"];
|
||||||
|
if (name == "") name = await matrix.store.getChatNameFromMemberNames(row["id"]);
|
||||||
|
|
||||||
|
String content_body = row["content_body"];
|
||||||
|
if (content_body == null || content_body == "")
|
||||||
|
content_body = "Keine vorhergehenden Nachrichten";
|
||||||
|
|
||||||
|
String avatarMxcUrl = row["avatar_url"];
|
||||||
|
|
||||||
|
if (avatarMxcUrl == "")
|
||||||
|
avatarMxcUrl = await matrix.store.getAvatarFromSingleChat(row["id"]);
|
||||||
|
|
||||||
|
return Room(
|
||||||
|
roomID: row["id"],
|
||||||
|
name: name,
|
||||||
|
lastMessage: content_body,
|
||||||
|
avatar: MxContent(avatarMxcUrl),
|
||||||
|
timeCreated: ChatTime(row["origin_server_ts"]),
|
||||||
|
notificationCount: row["notification_count"],
|
||||||
|
highlightCount: row["highlight_count"],
|
||||||
|
topic: "",
|
||||||
|
matrix: matrix,
|
||||||
|
events: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Room> getRoomById(String id, Client matrix) async {
|
||||||
|
List<Map<String, dynamic>> res =
|
||||||
|
await matrix.store.db.rawQuery("SELECT * FROM Chats WHERE id=?", [id]);
|
||||||
|
if (res.length != 1) return null;
|
||||||
|
return getRoomFromTableRow(res[0], matrix);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Room> loadRoomEvents(String id, Client matrix) async {
|
||||||
|
Room room = await Room.getRoomById(id, matrix);
|
||||||
|
room.events = await Event.getEventList(matrix, id);
|
||||||
|
return room;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<User>> requestParticipants(Client matrix) async {
|
||||||
|
List<User> participants = [];
|
||||||
|
|
||||||
|
dynamic res = await matrix.connection.jsonRequest(
|
||||||
|
type: "GET", action: "/client/r0/rooms/${roomID}/members");
|
||||||
|
if (res is ErrorResponse || !(res["chunk"] is List<dynamic>))
|
||||||
|
return participants;
|
||||||
|
|
||||||
|
for (num i = 0; i < res["chunk"].length; i++) {
|
||||||
|
User newUser = User(res["chunk"][i]["state_key"],
|
||||||
|
displayName: res["chunk"][i]["content"]["displayname"] ?? "",
|
||||||
|
status: res["chunk"][i]["content"]["membership"] ?? "",
|
||||||
|
directChatRoomId: "",
|
||||||
|
avatar_url:
|
||||||
|
MxContent(res["chunk"][i]["content"]["avatar_url"] ?? ""));
|
||||||
|
if (newUser.status != "leave") participants.add(newUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
return participants;
|
||||||
|
}
|
||||||
|
}
|
516
lib/src/Store.dart
Normal file
516
lib/src/Store.dart
Normal file
|
@ -0,0 +1,516 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:core';
|
||||||
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
import 'sync/EventUpdate.dart';
|
||||||
|
import 'sync/RoomUpdate.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'Client.dart';
|
||||||
|
import 'User.dart';
|
||||||
|
import 'Room.dart';
|
||||||
|
import 'Connection.dart';
|
||||||
|
|
||||||
|
/// Represents a Matrix connection to communicate with a
|
||||||
|
/// [Matrix](https://matrix.org) homeserver.
|
||||||
|
class Store {
|
||||||
|
|
||||||
|
final Client client;
|
||||||
|
|
||||||
|
Store(this.client) {
|
||||||
|
_init();
|
||||||
|
}
|
||||||
|
|
||||||
|
Database _db;
|
||||||
|
|
||||||
|
/// SQLite database for all persistent data. It is recommended to extend this
|
||||||
|
/// SDK instead of writing direct queries to the database.
|
||||||
|
Database get db => _db;
|
||||||
|
|
||||||
|
_init() async{
|
||||||
|
var databasePath = await getDatabasesPath();
|
||||||
|
String path = p.join(databasePath, "FluffyMatrix.db");
|
||||||
|
_db = await openDatabase(path, version: 2,
|
||||||
|
onCreate: (Database db, int version) async {
|
||||||
|
// When creating the db, create the table
|
||||||
|
await db.execute(ClientScheme);
|
||||||
|
await db.execute(RoomScheme);
|
||||||
|
await db.execute(MemberScheme);
|
||||||
|
await db.execute(EventScheme);
|
||||||
|
});
|
||||||
|
|
||||||
|
List<Map> list = await _db
|
||||||
|
.rawQuery("SELECT * FROM Clients WHERE client=?", [client.clientName]);
|
||||||
|
if (list.length == 1) {
|
||||||
|
var clientList = list[0];
|
||||||
|
client.connection.connect(
|
||||||
|
newToken: clientList["token"],
|
||||||
|
newHomeserver: clientList["homeserver"],
|
||||||
|
newUserID: clientList["matrix_id"],
|
||||||
|
newDeviceID: clientList["device_id"],
|
||||||
|
newDeviceName: clientList["device_name"],
|
||||||
|
newLazyLoadMembers: clientList["lazy_load_members"] == 1,
|
||||||
|
newMatrixVersions: clientList["matrix_versions"].toString().split(","),
|
||||||
|
newPrevBatch: clientList["prev_batch"],
|
||||||
|
);
|
||||||
|
print("Restore client credentials of ${client.userID}");
|
||||||
|
} else
|
||||||
|
client.connection.onLoginStateChanged.add(LoginState.loggedOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> queryPrevBatch() async{
|
||||||
|
List<Map> list = await txn.rawQuery("SELECT prev_batch FROM Clients WHERE client=?", [client.clientName]);
|
||||||
|
return list[0]["prev_batch"];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Will be automatically called when the client is logged in successfully.
|
||||||
|
Future<void> storeClient() async{
|
||||||
|
await _db
|
||||||
|
.rawInsert('INSERT OR IGNORE INTO Clients VALUES(?,?,?,?,?,?,?,?,?)', [
|
||||||
|
client.clientName,
|
||||||
|
client.accessToken,
|
||||||
|
client.homeserver,
|
||||||
|
client.userID,
|
||||||
|
client.deviceID,
|
||||||
|
client.deviceName,
|
||||||
|
client.prevBatch,
|
||||||
|
client.matrixVersions.join(","),
|
||||||
|
client.lazyLoadMembers,
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears all tables from the database.
|
||||||
|
Future<void> clear() async{
|
||||||
|
await _db.rawDelete("DELETE FROM Clients WHERE client=?", [client.clientName]);
|
||||||
|
await _db.rawDelete("DELETE FROM Chats");
|
||||||
|
await _db.rawDelete("DELETE FROM Memberships");
|
||||||
|
await _db.rawDelete("DELETE FROM Events");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Transaction txn;
|
||||||
|
|
||||||
|
Future<void> transaction(Future<void> queries()) async{
|
||||||
|
return client.store.db.transaction((txnObj) async {
|
||||||
|
txn = txnObj;
|
||||||
|
await queries();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Will be automatically called on every synchronisation. Must be called inside of
|
||||||
|
// /// [transaction].
|
||||||
|
Future<void> storePrevBatch(dynamic sync) {
|
||||||
|
txn.rawUpdate("UPDATE Clients SET prev_batch=? WHERE client=?",
|
||||||
|
[client.prevBatch, client.clientName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stores a RoomUpdate object in the database. Must be called inside of
|
||||||
|
/// [transaction].
|
||||||
|
Future<void> storeRoomUpdate(RoomUpdate roomUpdate) {
|
||||||
|
// Insert the chat into the database if not exists
|
||||||
|
txn.rawInsert(
|
||||||
|
"INSERT OR IGNORE INTO Chats " +
|
||||||
|
"VALUES(?, ?, '', 0, 0, 0, '', '', '', 0, '', '', '', '', '', '', 0, 50, 50, 0, 50, 50, 0, 50, 100, 50, 50, 50, 100) ",
|
||||||
|
[roomUpdate.id, roomUpdate.membership]);
|
||||||
|
|
||||||
|
// Update the notification counts and the limited timeline boolean
|
||||||
|
txn.rawUpdate(
|
||||||
|
"UPDATE Chats SET highlight_count=?, notification_count=?, membership=?, limitedTimeline=? WHERE id=? ",
|
||||||
|
[
|
||||||
|
roomUpdate.highlight_count,
|
||||||
|
roomUpdate.notification_count,
|
||||||
|
roomUpdate.membership,
|
||||||
|
roomUpdate.limitedTimeline,
|
||||||
|
roomUpdate.id
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Is the timeline limited? Then all previous messages should be
|
||||||
|
// removed from the database!
|
||||||
|
if (roomUpdate.limitedTimeline) {
|
||||||
|
txn.rawDelete("DELETE FROM Events WHERE chat_id=?", [roomUpdate.id]);
|
||||||
|
txn.rawUpdate("UPDATE Chats SET prev_batch=? WHERE id=?",
|
||||||
|
[roomUpdate.prev_batch, roomUpdate.id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stores an EventUpdate object in the database. Must be called inside of
|
||||||
|
// /// [transaction].
|
||||||
|
Future<void> storeEventUpdate(EventUpdate eventUpdate) {
|
||||||
|
dynamic eventContent = eventUpdate.content;
|
||||||
|
String type = eventUpdate.type;
|
||||||
|
String chat_id = eventUpdate.roomID;
|
||||||
|
|
||||||
|
if (type == "timeline" || type == "history") {
|
||||||
|
// calculate the status
|
||||||
|
num status = 2;
|
||||||
|
// Make unsigned part of the content
|
||||||
|
if (eventContent["unsigned"] is Map<String, dynamic>)
|
||||||
|
eventContent["content"]["unsigned"] = eventContent["unsigned"];
|
||||||
|
|
||||||
|
// Get the state_key for m.room.member events
|
||||||
|
String state_key = "";
|
||||||
|
if (eventContent["state_key"] is String) {
|
||||||
|
state_key = eventContent["state_key"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the event in the database
|
||||||
|
|
||||||
|
txn.rawInsert(
|
||||||
|
"INSERT OR REPLACE INTO Events VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)", [
|
||||||
|
eventContent["event_id"],
|
||||||
|
chat_id,
|
||||||
|
eventContent["origin_server_ts"],
|
||||||
|
eventContent["sender"],
|
||||||
|
state_key,
|
||||||
|
eventContent["content"]["body"],
|
||||||
|
eventContent["type"],
|
||||||
|
json.encode(eventContent["content"]),
|
||||||
|
status
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == "history") return null;
|
||||||
|
|
||||||
|
switch (eventUpdate.eventType) {
|
||||||
|
case "m.receipt":
|
||||||
|
if (eventContent["user"] == client.userID) {
|
||||||
|
txn.rawUpdate("UPDATE Chats SET unread=? WHERE id=?",
|
||||||
|
[eventContent["ts"], chat_id]);
|
||||||
|
} else {
|
||||||
|
// Mark all previous received messages as seen
|
||||||
|
txn.rawUpdate(
|
||||||
|
"UPDATE Events SET status=3 WHERE origin_server_ts<=? AND chat_id=? AND status=2",
|
||||||
|
[eventContent["ts"], chat_id]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// This event means, that the name of a room has been changed, so
|
||||||
|
// it has to be changed in the database.
|
||||||
|
case "m.room.name":
|
||||||
|
txn.rawUpdate("UPDATE Chats SET topic=? WHERE id=?",
|
||||||
|
[eventContent["content"]["name"], chat_id]);
|
||||||
|
break;
|
||||||
|
// This event means, that the topic of a room has been changed, so
|
||||||
|
// it has to be changed in the database
|
||||||
|
case "m.room.topic":
|
||||||
|
txn.rawUpdate("UPDATE Chats SET description=? WHERE id=?",
|
||||||
|
[eventContent["content"]["topic"], chat_id]);
|
||||||
|
break;
|
||||||
|
// This event means, that the topic of a room has been changed, so
|
||||||
|
// it has to be changed in the database
|
||||||
|
case "m.room.history_visibility":
|
||||||
|
txn.rawUpdate("UPDATE Chats SET history_visibility=? WHERE id=?",
|
||||||
|
[eventContent["content"]["history_visibility"], chat_id]);
|
||||||
|
break;
|
||||||
|
// This event means, that the topic of a room has been changed, so
|
||||||
|
// it has to be changed in the database
|
||||||
|
case "m.room.redaction":
|
||||||
|
txn.rawDelete(
|
||||||
|
"DELETE FROM Events WHERE id=?", [eventContent["redacts"]]);
|
||||||
|
break;
|
||||||
|
// This event means, that the topic of a room has been changed, so
|
||||||
|
// it has to be changed in the database
|
||||||
|
case "m.room.guest_access":
|
||||||
|
txn.rawUpdate("UPDATE Chats SET guest_access=? WHERE id=?",
|
||||||
|
[eventContent["content"]["guest_access"], chat_id]);
|
||||||
|
break;
|
||||||
|
// This event means, that the topic of a room has been changed, so
|
||||||
|
// it has to be changed in the database
|
||||||
|
case "m.room.join_rules":
|
||||||
|
txn.rawUpdate("UPDATE Chats SET join_rules=? WHERE id=?",
|
||||||
|
[eventContent["content"]["join_rule"], chat_id]);
|
||||||
|
break;
|
||||||
|
// This event means, that the avatar of a room has been changed, so
|
||||||
|
// it has to be changed in the database
|
||||||
|
case "m.room.avatar":
|
||||||
|
txn.rawUpdate("UPDATE Chats SET avatar_url=? WHERE id=?",
|
||||||
|
[eventContent["content"]["url"], chat_id]);
|
||||||
|
break;
|
||||||
|
// This event means, that the aliases of a room has been changed, so
|
||||||
|
// it has to be changed in the database
|
||||||
|
case "m.fully_read":
|
||||||
|
txn.rawUpdate("UPDATE Chats SET fully_read=? WHERE id=?",
|
||||||
|
[eventContent["content"]["event_id"], chat_id]);
|
||||||
|
break;
|
||||||
|
// This event means, that someone joined the room, has left the room
|
||||||
|
// or has changed his nickname
|
||||||
|
case "m.room.member":
|
||||||
|
String membership = eventContent["content"]["membership"];
|
||||||
|
String state_key = eventContent["state_key"];
|
||||||
|
String insertDisplayname = "";
|
||||||
|
String insertAvatarUrl = "";
|
||||||
|
if (eventContent["content"]["displayname"] is String) {
|
||||||
|
insertDisplayname = eventContent["content"]["displayname"];
|
||||||
|
}
|
||||||
|
if (eventContent["content"]["avatar_url"] is String) {
|
||||||
|
insertAvatarUrl = eventContent["content"]["avatar_url"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update membership table
|
||||||
|
txn.rawInsert("INSERT OR IGNORE INTO Memberships VALUES(?,?,?,?,?,0)", [
|
||||||
|
chat_id,
|
||||||
|
state_key,
|
||||||
|
insertDisplayname,
|
||||||
|
insertAvatarUrl,
|
||||||
|
membership
|
||||||
|
]);
|
||||||
|
String queryStr = "UPDATE Memberships SET membership=?";
|
||||||
|
List<String> queryArgs = [membership];
|
||||||
|
|
||||||
|
if (eventContent["content"]["displayname"] is String) {
|
||||||
|
queryStr += " , displayname=?";
|
||||||
|
queryArgs.add(eventContent["content"]["displayname"]);
|
||||||
|
}
|
||||||
|
if (eventContent["content"]["avatar_url"] is String) {
|
||||||
|
queryStr += " , avatar_url=?";
|
||||||
|
queryArgs.add(eventContent["content"]["avatar_url"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
queryStr += " WHERE matrix_id=? AND chat_id=?";
|
||||||
|
queryArgs.add(state_key);
|
||||||
|
queryArgs.add(chat_id);
|
||||||
|
txn.rawUpdate(queryStr, queryArgs);
|
||||||
|
break;
|
||||||
|
// This event changes the permissions of the users and the power levels
|
||||||
|
case "m.room.power_levels":
|
||||||
|
String query = "UPDATE Chats SET ";
|
||||||
|
if (eventContent["content"]["ban"] is num)
|
||||||
|
query += ", power_ban=" + eventContent["content"]["ban"].toString();
|
||||||
|
if (eventContent["content"]["events_default"] is num)
|
||||||
|
query += ", power_events_default=" +
|
||||||
|
eventContent["content"]["events_default"].toString();
|
||||||
|
if (eventContent["content"]["state_default"] is num)
|
||||||
|
query += ", power_state_default=" +
|
||||||
|
eventContent["content"]["state_default"].toString();
|
||||||
|
if (eventContent["content"]["redact"] is num)
|
||||||
|
query +=
|
||||||
|
", power_redact=" + eventContent["content"]["redact"].toString();
|
||||||
|
if (eventContent["content"]["invite"] is num)
|
||||||
|
query +=
|
||||||
|
", power_invite=" + eventContent["content"]["invite"].toString();
|
||||||
|
if (eventContent["content"]["kick"] is num)
|
||||||
|
query += ", power_kick=" + eventContent["content"]["kick"].toString();
|
||||||
|
if (eventContent["content"]["user_default"] is num)
|
||||||
|
query += ", power_user_default=" +
|
||||||
|
eventContent["content"]["user_default"].toString();
|
||||||
|
if (eventContent["content"]["events"] is Map<String, dynamic>) {
|
||||||
|
if (eventContent["content"]["events"]["m.room.avatar"] is num)
|
||||||
|
query += ", power_event_avatar=" +
|
||||||
|
eventContent["content"]["events"]["m.room.avatar"].toString();
|
||||||
|
if (eventContent["content"]["events"]["m.room.history_visibility"]
|
||||||
|
is num)
|
||||||
|
query += ", power_event_history_visibility=" +
|
||||||
|
eventContent["content"]["events"]["m.room.history_visibility"]
|
||||||
|
.toString();
|
||||||
|
if (eventContent["content"]["events"]["m.room.canonical_alias"]
|
||||||
|
is num)
|
||||||
|
query += ", power_event_canonical_alias=" +
|
||||||
|
eventContent["content"]["events"]["m.room.canonical_alias"]
|
||||||
|
.toString();
|
||||||
|
if (eventContent["content"]["events"]["m.room.aliases"] is num)
|
||||||
|
query += ", power_event_aliases=" +
|
||||||
|
eventContent["content"]["events"]["m.room.aliases"].toString();
|
||||||
|
if (eventContent["content"]["events"]["m.room.name"] is num)
|
||||||
|
query += ", power_event_name=" +
|
||||||
|
eventContent["content"]["events"]["m.room.name"].toString();
|
||||||
|
if (eventContent["content"]["events"]["m.room.power_levels"] is num)
|
||||||
|
query += ", power_event_power_levels=" +
|
||||||
|
eventContent["content"]["events"]["m.room.power_levels"]
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
if (query != "UPDATE Chats SET ") {
|
||||||
|
query = query.replaceFirst(",", "");
|
||||||
|
txn.rawUpdate(query + " WHERE id=?", [chat_id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the users power levels:
|
||||||
|
if (eventContent["content"]["users"] is Map<String, dynamic>) {
|
||||||
|
eventContent["content"]["users"]
|
||||||
|
.forEach((String user, dynamic value) async {
|
||||||
|
num power_level = eventContent["content"]["users"][user];
|
||||||
|
txn.rawUpdate(
|
||||||
|
"UPDATE Memberships SET power_level=? WHERE matrix_id=? AND chat_id=?",
|
||||||
|
[power_level, user, chat_id]);
|
||||||
|
txn.rawInsert(
|
||||||
|
"INSERT OR IGNORE INTO Memberships VALUES(?, ?, '', '', ?, ?)",
|
||||||
|
[chat_id, user, "unknown", power_level]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a User object by a given Matrix ID and a Room ID.
|
||||||
|
Future<User> getUser(
|
||||||
|
{String matrixID, String roomID}) async {
|
||||||
|
List<Map<String, dynamic>> res = await db.rawQuery(
|
||||||
|
"SELECT * FROM Memberships WHERE matrix_id=? AND chat_id=?",
|
||||||
|
[matrixID, roomID]);
|
||||||
|
if (res.length != 1) return null;
|
||||||
|
return User.fromJson(res[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads all Users in the database to provide a contact list.
|
||||||
|
Future<List<User>> loadContacts() async {
|
||||||
|
List<Map<String, dynamic>> res = await db.rawQuery(
|
||||||
|
"SELECT * FROM Memberships WHERE matrix_id!=? GROUP BY matrix_id ORDER BY displayname",
|
||||||
|
[client.userID]);
|
||||||
|
List<User> userList = [];
|
||||||
|
for (int i = 0; i < res.length; i++) userList.add(User.fromJson(res[i]));
|
||||||
|
return userList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns all users of a room by a given [roomID].
|
||||||
|
Future<List<User>> loadParticipants(String roomID) async {
|
||||||
|
List<Map<String, dynamic>> res = await db.rawQuery(
|
||||||
|
"SELECT * " +
|
||||||
|
" FROM Memberships " +
|
||||||
|
" WHERE chat_id=? " +
|
||||||
|
" AND membership='join'",
|
||||||
|
[roomID]);
|
||||||
|
|
||||||
|
List<User> participants = [];
|
||||||
|
|
||||||
|
for (num i = 0; i < res.length; i++) {
|
||||||
|
participants.add(User.fromJson(res[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return participants;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns all rooms, the client is participating. Excludes left rooms.
|
||||||
|
Future<List<Room>> getRoomList() async {
|
||||||
|
List<Map<String, dynamic>> res = await db.rawQuery(
|
||||||
|
"SELECT rooms.id, rooms.topic, rooms.membership, rooms.notification_count, rooms.highlight_count, rooms.avatar_url, rooms.unread, " +
|
||||||
|
" events.id AS eventsid, origin_server_ts, events.content_body, events.sender, events.state_key, events.content_json, events.type " +
|
||||||
|
" FROM Chats rooms LEFT JOIN Events events " +
|
||||||
|
" ON rooms.id=events.chat_id " +
|
||||||
|
" WHERE rooms.membership!='leave' " +
|
||||||
|
" GROUP BY rooms.id " +
|
||||||
|
" ORDER BY origin_server_ts DESC ");
|
||||||
|
List<Room> roomList = [];
|
||||||
|
for (num i = 0; i < res.length; i++) {
|
||||||
|
try {
|
||||||
|
Room room = await Room.getRoomFromTableRow(res[i], client);
|
||||||
|
roomList.add(room);
|
||||||
|
} catch (e) {
|
||||||
|
print(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return roomList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates and returns an avatar for a direct chat by a given [roomID].
|
||||||
|
Future<String> getAvatarFromSingleChat(
|
||||||
|
String roomID) async {
|
||||||
|
String avatarStr = "";
|
||||||
|
List<Map<String, dynamic>> res = await db.rawQuery(
|
||||||
|
"SELECT avatar_url FROM Memberships " +
|
||||||
|
" WHERE Memberships.chat_id=? " +
|
||||||
|
" AND (Memberships.membership='join' OR Memberships.membership='invite') " +
|
||||||
|
" AND Memberships.matrix_id!=? ",
|
||||||
|
[roomID, client.userID]);
|
||||||
|
if (res.length == 1) avatarStr = res[0]["avatar_url"];
|
||||||
|
return avatarStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates a chat name for a groupchat without a name. The chat name will
|
||||||
|
/// be the name of all users (excluding the user of this client) divided by
|
||||||
|
/// ','.
|
||||||
|
Future<String> getChatNameFromMemberNames(
|
||||||
|
String roomID) async {
|
||||||
|
String displayname = 'Empty chat';
|
||||||
|
List<Map<String, dynamic>> rs = await db.rawQuery(
|
||||||
|
"SELECT Memberships.displayname, Memberships.matrix_id, Memberships.membership FROM Memberships " +
|
||||||
|
" WHERE Memberships.chat_id=? " +
|
||||||
|
" AND (Memberships.membership='join' OR Memberships.membership='invite') " +
|
||||||
|
" AND Memberships.matrix_id!=? ",
|
||||||
|
[roomID, client.userID]);
|
||||||
|
if (rs.length > 0) {
|
||||||
|
displayname = "";
|
||||||
|
for (var i = 0; i < rs.length; i++) {
|
||||||
|
String username = rs[i]["displayname"];
|
||||||
|
if (username == "" || username == null) username = rs[i]["matrix_id"];
|
||||||
|
if (rs[i]["state_key"] != client.userID) displayname += username + ", ";
|
||||||
|
}
|
||||||
|
if (displayname == "" || displayname == null)
|
||||||
|
displayname = 'Empty chat';
|
||||||
|
else
|
||||||
|
displayname = displayname.substring(0, displayname.length - 2);
|
||||||
|
}
|
||||||
|
return displayname;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The database sheme for the Client class.
|
||||||
|
static final String ClientScheme = 'CREATE TABLE IF NOT EXISTS Clients(' +
|
||||||
|
'client TEXT PRIMARY KEY, ' +
|
||||||
|
'token TEXT, ' +
|
||||||
|
'homeserver TEXT, ' +
|
||||||
|
'matrix_id TEXT, ' +
|
||||||
|
'device_id TEXT, ' +
|
||||||
|
'device_name TEXT, ' +
|
||||||
|
'prev_batch TEXT, ' +
|
||||||
|
'matrix_versions TEXT, ' +
|
||||||
|
'lazy_load_members INTEGER, ' +
|
||||||
|
'UNIQUE(client))';
|
||||||
|
/// The database sheme for the Room class.
|
||||||
|
static final String RoomScheme = 'CREATE TABLE IF NOT EXISTS Chats(' +
|
||||||
|
'id TEXT PRIMARY KEY, ' +
|
||||||
|
'membership TEXT, ' +
|
||||||
|
'topic TEXT, ' +
|
||||||
|
'highlight_count INTEGER, ' +
|
||||||
|
'notification_count INTEGER, ' +
|
||||||
|
'limitedTimeline INTEGER, ' +
|
||||||
|
'prev_batch TEXT, ' +
|
||||||
|
'avatar_url TEXT, ' +
|
||||||
|
'draft TEXT, ' +
|
||||||
|
'unread INTEGER, ' + // Timestamp of when the user has last read the chat
|
||||||
|
'fully_read TEXT, ' + // ID of the fully read marker event
|
||||||
|
'description TEXT, ' +
|
||||||
|
'canonical_alias TEXT, ' + // The address in the form: #roomname:homeserver.org
|
||||||
|
|
||||||
|
// Security rules
|
||||||
|
'guest_access TEXT, ' +
|
||||||
|
'history_visibility TEXT, ' +
|
||||||
|
'join_rules TEXT, ' +
|
||||||
|
|
||||||
|
// Power levels
|
||||||
|
'power_events_default INTEGER, ' +
|
||||||
|
'power_state_default INTEGER, ' +
|
||||||
|
'power_redact INTEGER, ' +
|
||||||
|
'power_invite INTEGER, ' +
|
||||||
|
'power_ban INTEGER, ' +
|
||||||
|
'power_kick INTEGER, ' +
|
||||||
|
'power_user_default INTEGER, ' +
|
||||||
|
|
||||||
|
// Power levels for events
|
||||||
|
'power_event_avatar INTEGER, ' +
|
||||||
|
'power_event_history_visibility INTEGER, ' +
|
||||||
|
'power_event_canonical_alias INTEGER, ' +
|
||||||
|
'power_event_aliases INTEGER, ' +
|
||||||
|
'power_event_name INTEGER, ' +
|
||||||
|
'power_event_power_levels INTEGER, ' +
|
||||||
|
'UNIQUE(id))';
|
||||||
|
|
||||||
|
/// The database sheme for the Event class.
|
||||||
|
static final String EventScheme = 'CREATE TABLE IF NOT EXISTS Events(' +
|
||||||
|
'id TEXT PRIMARY KEY, ' +
|
||||||
|
'chat_id TEXT, ' +
|
||||||
|
'origin_server_ts INTEGER, ' +
|
||||||
|
'sender TEXT, ' +
|
||||||
|
'state_key TEXT, ' +
|
||||||
|
'content_body TEXT, ' +
|
||||||
|
'type TEXT, ' +
|
||||||
|
'content_json TEXT, ' +
|
||||||
|
"status INTEGER, " +
|
||||||
|
'UNIQUE(id))';
|
||||||
|
|
||||||
|
/// The database sheme for the User class.
|
||||||
|
static final String MemberScheme = 'CREATE TABLE IF NOT EXISTS Memberships(' +
|
||||||
|
'chat_id TEXT, ' + // The chat id of this membership
|
||||||
|
'matrix_id TEXT, ' + // The matrix id of this user
|
||||||
|
'displayname TEXT, ' +
|
||||||
|
'avatar_url TEXT, ' +
|
||||||
|
'membership TEXT, ' + // The status of the membership. Must be one of [join, invite, ban, leave]
|
||||||
|
'power_level INTEGER, ' + // The power level of this user. Must be in [0,..,100]
|
||||||
|
'UNIQUE(chat_id, matrix_id))';
|
||||||
|
}
|
33
lib/src/User.dart
Normal file
33
lib/src/User.dart
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import 'package:famedlysdk/src/Client.dart';
|
||||||
|
import 'package:famedlysdk/src/utils/MxContent.dart';
|
||||||
|
import 'package:famedlysdk/src/Room.dart';
|
||||||
|
|
||||||
|
class User {
|
||||||
|
final String status;
|
||||||
|
final String mxid;
|
||||||
|
final String displayName;
|
||||||
|
final MxContent avatar_url;
|
||||||
|
final String directChatRoomId;
|
||||||
|
final Room room;
|
||||||
|
|
||||||
|
const User(
|
||||||
|
this.mxid, {
|
||||||
|
this.status,
|
||||||
|
this.displayName,
|
||||||
|
this.avatar_url,
|
||||||
|
this.directChatRoomId,
|
||||||
|
this.room,
|
||||||
|
});
|
||||||
|
|
||||||
|
String calcDisplayname() => displayName.isEmpty
|
||||||
|
? mxid.replaceFirst("@", "").split(":")[0]
|
||||||
|
: displayName;
|
||||||
|
|
||||||
|
static User fromJson(Map<String, dynamic> json) {
|
||||||
|
return User(json['matrix_id'],
|
||||||
|
displayName: json['displayname'],
|
||||||
|
avatar_url: MxContent(json['avatar_url']),
|
||||||
|
status: "",
|
||||||
|
directChatRoomId: "");
|
||||||
|
}
|
||||||
|
}
|
23
lib/src/responses/ErrorResponse.dart
Normal file
23
lib/src/responses/ErrorResponse.dart
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
/// Represents a special response from the Homeserver for errors.
|
||||||
|
class ErrorResponse {
|
||||||
|
|
||||||
|
/// The unique identifier for this error.
|
||||||
|
String errcode;
|
||||||
|
|
||||||
|
/// A human readable error description.
|
||||||
|
String error;
|
||||||
|
|
||||||
|
ErrorResponse({this.errcode, this.error});
|
||||||
|
|
||||||
|
ErrorResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
errcode = json['errcode'];
|
||||||
|
error = json['error'] ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final Map<String, dynamic> data = new Map<String, dynamic>();
|
||||||
|
data['errcode'] = this.errcode;
|
||||||
|
data['error'] = this.error;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
20
lib/src/sync/EventUpdate.dart
Normal file
20
lib/src/sync/EventUpdate.dart
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
/// Represents a new event (e.g. a message in a room) or an update for an
|
||||||
|
/// already known event.
|
||||||
|
class EventUpdate {
|
||||||
|
|
||||||
|
/// Usually 'timeline', 'state' or whatever.
|
||||||
|
final String eventType;
|
||||||
|
|
||||||
|
/// Most events belong to a room. If not, this equals to eventType.
|
||||||
|
final String roomID;
|
||||||
|
|
||||||
|
/// See (Matrix Room Events)[https://matrix.org/docs/spec/client_server/r0.4.0.html#room-events]
|
||||||
|
/// and (Matrix Events)[https://matrix.org/docs/spec/client_server/r0.4.0.html#id89] for more
|
||||||
|
/// informations.
|
||||||
|
final String type;
|
||||||
|
|
||||||
|
// The json payload of the content of this event.
|
||||||
|
final dynamic content;
|
||||||
|
|
||||||
|
EventUpdate({this.eventType, this.roomID, this.type, this.content});
|
||||||
|
}
|
33
lib/src/sync/RoomUpdate.dart
Normal file
33
lib/src/sync/RoomUpdate.dart
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
/// Represents a new room or an update for an
|
||||||
|
/// already known room.
|
||||||
|
class RoomUpdate {
|
||||||
|
|
||||||
|
/// All rooms have an idea in the format: !uniqueid:server.abc
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
/// The current membership state of the user in this room.
|
||||||
|
final String membership;
|
||||||
|
|
||||||
|
/// Represents the number of unead notifications. This probably doesn't fit the number
|
||||||
|
/// of unread messages.
|
||||||
|
final num notification_count;
|
||||||
|
|
||||||
|
// The number of unread highlighted notifications.
|
||||||
|
final num highlight_count;
|
||||||
|
|
||||||
|
/// If there are too much new messages, the [homeserver] will only send the
|
||||||
|
/// last X (default is 10) messages and set the [limitedTimelinbe] flag to true.
|
||||||
|
final bool limitedTimeline;
|
||||||
|
|
||||||
|
/// Represents the current position of the client in the room history.
|
||||||
|
final String prev_batch;
|
||||||
|
|
||||||
|
RoomUpdate({
|
||||||
|
this.id,
|
||||||
|
this.membership,
|
||||||
|
this.notification_count,
|
||||||
|
this.highlight_count,
|
||||||
|
this.limitedTimeline,
|
||||||
|
this.prev_batch,
|
||||||
|
});
|
||||||
|
}
|
74
lib/src/utils/ChatTime.dart
Normal file
74
lib/src/utils/ChatTime.dart
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
class ChatTime {
|
||||||
|
DateTime dateTime = DateTime.now();
|
||||||
|
|
||||||
|
ChatTime(num ts) {
|
||||||
|
if (ts != null)
|
||||||
|
dateTime = DateTime.fromMicrosecondsSinceEpoch(ts * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatTime.now() {
|
||||||
|
dateTime = DateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
String toString() {
|
||||||
|
DateTime now = DateTime.now();
|
||||||
|
|
||||||
|
bool sameYear = now.year == dateTime.year;
|
||||||
|
|
||||||
|
bool sameDay =
|
||||||
|
sameYear && now.month == dateTime.month && now.day == dateTime.day;
|
||||||
|
|
||||||
|
bool sameWeek = sameYear && !sameDay && now.millisecondsSinceEpoch - dateTime.millisecondsSinceEpoch < 1000*60*60*24*7;
|
||||||
|
|
||||||
|
if (sameDay) {
|
||||||
|
return toTimeString();
|
||||||
|
} else if (sameWeek) {
|
||||||
|
switch (dateTime.weekday) { // TODO: Needs localization
|
||||||
|
case 1:
|
||||||
|
return "Montag";
|
||||||
|
case 2:
|
||||||
|
return "Dienstag";
|
||||||
|
case 3:
|
||||||
|
return "Mittwoch";
|
||||||
|
case 4:
|
||||||
|
return "Donnerstag";
|
||||||
|
case 5:
|
||||||
|
return "Freitag";
|
||||||
|
case 6:
|
||||||
|
return "Samstag";
|
||||||
|
case 7:
|
||||||
|
return "Sonntag";
|
||||||
|
}
|
||||||
|
} else if (sameYear) {
|
||||||
|
return DateFormat('dd.MM').format(dateTime);
|
||||||
|
} else {
|
||||||
|
return DateFormat('dd.MM.yyyy').format(dateTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
num toTimeStamp() {
|
||||||
|
return dateTime.microsecondsSinceEpoch;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool sameEnvironment(ChatTime prevTime) {
|
||||||
|
return toTimeStamp() - prevTime.toTimeStamp() < 1000*60*5;
|
||||||
|
}
|
||||||
|
|
||||||
|
String toTimeString() {
|
||||||
|
return DateFormat('HH:mm').format(dateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toEventTimeString() {
|
||||||
|
DateTime now = DateTime.now();
|
||||||
|
|
||||||
|
bool sameYear = now.year == dateTime.year;
|
||||||
|
|
||||||
|
bool sameDay =
|
||||||
|
sameYear && now.month == dateTime.month && now.day == dateTime.day;
|
||||||
|
|
||||||
|
if (sameDay) return toTimeString();
|
||||||
|
return "${toString()}, ${DateFormat('HH:mm').format(dateTime)}";
|
||||||
|
}
|
||||||
|
}
|
24
lib/src/utils/MxContent.dart
Normal file
24
lib/src/utils/MxContent.dart
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import 'package:famedlysdk/src/Client.dart';
|
||||||
|
import 'dart:core';
|
||||||
|
|
||||||
|
class MxContent {
|
||||||
|
|
||||||
|
final String _mxc;
|
||||||
|
|
||||||
|
MxContent(this._mxc);
|
||||||
|
|
||||||
|
get mxc => _mxc;
|
||||||
|
|
||||||
|
getDownloadLink (Client matrix) => "https://${matrix.homeserver}/_matrix/media/r0/download/${_mxc.replaceFirst("mxc://","")}/";
|
||||||
|
|
||||||
|
getThumbnail (Client matrix, {num width, num height, ThumbnailMethod method}) {
|
||||||
|
String methodStr = "crop";
|
||||||
|
if (method == ThumbnailMethod.scale) methodStr = "scale";
|
||||||
|
width = width.round();
|
||||||
|
height = height.round();
|
||||||
|
return "${matrix.homeserver}/_matrix/media/r0/thumbnail/${_mxc.replaceFirst("mxc://","")}?width=$width&height=$height&method=$methodStr";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ThumbnailMethod {crop, scale}
|
287
pubspec.lock
Normal file
287
pubspec.lock
Normal file
|
@ -0,0 +1,287 @@
|
||||||
|
# Generated by pub
|
||||||
|
# See https://www.dartlang.org/tools/pub/glossary#lockfile
|
||||||
|
packages:
|
||||||
|
analyzer:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: analyzer
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.36.3"
|
||||||
|
args:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: args
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.2"
|
||||||
|
async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: async
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
|
boolean_selector:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: boolean_selector
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.4"
|
||||||
|
charcode:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: charcode
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
|
collection:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: collection
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.14.11"
|
||||||
|
convert:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: convert
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
|
crypto:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: crypto
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.6"
|
||||||
|
csslib:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: csslib
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.16.0"
|
||||||
|
dart_style:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dart_style
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.7"
|
||||||
|
flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
flutter_test:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
front_end:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: front_end
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.18"
|
||||||
|
glob:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: glob
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.7"
|
||||||
|
html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: html
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.14.0+2"
|
||||||
|
http:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: http
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.12.0+2"
|
||||||
|
http_parser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http_parser
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.3"
|
||||||
|
intl:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: intl
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.8"
|
||||||
|
intl_translation:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: intl_translation
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.17.5"
|
||||||
|
kernel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: kernel
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.18"
|
||||||
|
matcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: matcher
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.12.5"
|
||||||
|
meta:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: meta
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.6"
|
||||||
|
package_config:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_config
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.5"
|
||||||
|
path:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.6.2"
|
||||||
|
pedantic:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pedantic
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.0"
|
||||||
|
petitparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: petitparser
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.1"
|
||||||
|
pub_semver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pub_semver
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.2"
|
||||||
|
quiver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: quiver
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.2"
|
||||||
|
sky_engine:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.99"
|
||||||
|
source_span:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_span
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.5"
|
||||||
|
sqflite:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: sqflite
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.5"
|
||||||
|
stack_trace:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stack_trace
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.9.3"
|
||||||
|
stream_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_channel
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
|
string_scanner:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: string_scanner
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.4"
|
||||||
|
synchronized:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: synchronized
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
|
term_glyph:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: term_glyph
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
|
test_api:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: test_api
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.4"
|
||||||
|
typed_data:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: typed_data
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.6"
|
||||||
|
vector_math:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_math
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.8"
|
||||||
|
watcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: watcher
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.7+10"
|
||||||
|
yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: yaml
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.15"
|
||||||
|
sdks:
|
||||||
|
dart: ">=2.2.0 <3.0.0"
|
||||||
|
flutter: ">=1.2.1 <2.0.0"
|
62
pubspec.yaml
Normal file
62
pubspec.yaml
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
name: famedlysdk
|
||||||
|
description: Matrix SDK for the famedly talk app written in dart.
|
||||||
|
version: 0.0.1
|
||||||
|
author: famedly
|
||||||
|
homepage: https://famedly.com
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ">=2.1.0 <3.0.0"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
# Database
|
||||||
|
sqflite: ^1.1.0
|
||||||
|
|
||||||
|
# Connection
|
||||||
|
http: ^0.12.0+2
|
||||||
|
|
||||||
|
# Time formatting
|
||||||
|
intl_translation: ^0.17.1
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
# For information on the generic Dart part of this file, see the
|
||||||
|
# following page: https://www.dartlang.org/tools/pub/pubspec
|
||||||
|
|
||||||
|
# The following section is specific to Flutter.
|
||||||
|
flutter:
|
||||||
|
|
||||||
|
# To add assets to your package, add an assets section, like this:
|
||||||
|
# assets:
|
||||||
|
# - images/a_dot_burr.jpeg
|
||||||
|
# - images/a_dot_ham.jpeg
|
||||||
|
#
|
||||||
|
# For details regarding assets in packages, see
|
||||||
|
# https://flutter.dev/assets-and-images/#from-packages
|
||||||
|
#
|
||||||
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
|
# https://flutter.dev/assets-and-images/#resolution-aware.
|
||||||
|
|
||||||
|
# To add custom fonts to your package, add a fonts section here,
|
||||||
|
# in this "flutter" section. Each entry in this list should have a
|
||||||
|
# "family" key with the font family name, and a "fonts" key with a
|
||||||
|
# list giving the asset and other descriptors for the font. For
|
||||||
|
# example:
|
||||||
|
# fonts:
|
||||||
|
# - family: Schyler
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/Schyler-Regular.ttf
|
||||||
|
# - asset: fonts/Schyler-Italic.ttf
|
||||||
|
# style: italic
|
||||||
|
# - family: Trajan Pro
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/TrajanPro.ttf
|
||||||
|
# - asset: fonts/TrajanPro_Bold.ttf
|
||||||
|
# weight: 700
|
||||||
|
#
|
||||||
|
# For details regarding fonts in packages, see
|
||||||
|
# https://flutter.dev/custom-fonts/#from-packages
|
216
test/Client_test.dart
Normal file
216
test/Client_test.dart
Normal file
|
@ -0,0 +1,216 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:famedlysdk/src/Client.dart';
|
||||||
|
import 'package:famedlysdk/src/Connection.dart';
|
||||||
|
import 'package:famedlysdk/src/sync/EventUpdate.dart';
|
||||||
|
import 'package:famedlysdk/src/sync/RoomUpdate.dart';
|
||||||
|
import 'package:famedlysdk/src/responses/ErrorResponse.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
import 'FakeMatrixApi.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
Client matrix;
|
||||||
|
|
||||||
|
Future<List<RoomUpdate>> roomUpdateListFuture;
|
||||||
|
Future<List<EventUpdate>> eventUpdateListFuture;
|
||||||
|
|
||||||
|
/// All Tests related to the Login
|
||||||
|
group("FluffyMatrix", () {
|
||||||
|
/// Check if all Elements get created
|
||||||
|
|
||||||
|
final create = (WidgetTester tester) {
|
||||||
|
|
||||||
|
matrix = Client("testclient");
|
||||||
|
matrix.connection.httpClient = FakeMatrixApi();
|
||||||
|
matrix.homeserver = "https://fakeServer.notExisting";
|
||||||
|
|
||||||
|
roomUpdateListFuture = matrix.connection.onRoomUpdate.stream.toList();
|
||||||
|
eventUpdateListFuture = matrix.connection.onEvent.stream.toList();
|
||||||
|
};
|
||||||
|
testWidgets('should get created', create);
|
||||||
|
|
||||||
|
test("Get version", () async {
|
||||||
|
final versionResp =
|
||||||
|
await matrix.connection.jsonRequest(type: "GET", action: "/client/versions");
|
||||||
|
expect(versionResp is ErrorResponse, false);
|
||||||
|
expect(versionResp["versions"].indexOf("r0.4.0") != -1, true);
|
||||||
|
matrix.matrixVersions = List<String>.from(versionResp["versions"]);
|
||||||
|
matrix.lazyLoadMembers = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Get login types", () async {
|
||||||
|
final resp =
|
||||||
|
await matrix.connection.jsonRequest(type: "GET", action: "/client/r0/login");
|
||||||
|
expect(resp is ErrorResponse, false);
|
||||||
|
expect(resp["flows"] is List<dynamic>, true);
|
||||||
|
bool hasMLoginType = false;
|
||||||
|
for (int i = 0; i < resp["flows"].length; i++)
|
||||||
|
if (resp["flows"][i]["type"] is String &&
|
||||||
|
resp["flows"][i]["type"] == "m.login.password") {
|
||||||
|
hasMLoginType = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
expect(hasMLoginType, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
final loginText = () async{
|
||||||
|
final resp = await matrix
|
||||||
|
.connection.jsonRequest(type: "POST", action: "/client/r0/login", data: {
|
||||||
|
"type": "m.login.password",
|
||||||
|
"user": "test",
|
||||||
|
"password": "1234",
|
||||||
|
"initial_device_display_name": "Fluffy Matrix Client"
|
||||||
|
});
|
||||||
|
expect(resp is ErrorResponse, false);
|
||||||
|
|
||||||
|
Future<LoginState> loginStateFuture = matrix.connection.onLoginStateChanged.stream.first;
|
||||||
|
Future<bool> firstSyncFuture = matrix.connection.onFirstSync.stream.first;
|
||||||
|
Future<dynamic> syncFuture = matrix.connection.onSync.stream.first;
|
||||||
|
|
||||||
|
matrix.connection.connect(
|
||||||
|
newToken: resp["access_token"],
|
||||||
|
newUserID: resp["user_id"],
|
||||||
|
newHomeserver: matrix.homeserver,
|
||||||
|
newDeviceName: "Text Matrix Client",
|
||||||
|
newDeviceID: resp["device_id"],
|
||||||
|
newMatrixVersions: matrix.matrixVersions,
|
||||||
|
newLazyLoadMembers: matrix.lazyLoadMembers);
|
||||||
|
|
||||||
|
expect(matrix.accessToken == resp["access_token"], true);
|
||||||
|
expect(matrix.deviceName == "Text Matrix Client", true);
|
||||||
|
expect(matrix.deviceID == resp["device_id"], true);
|
||||||
|
expect(matrix.userID == resp["user_id"], true);
|
||||||
|
|
||||||
|
LoginState loginState = await loginStateFuture;
|
||||||
|
bool firstSync = await firstSyncFuture;
|
||||||
|
dynamic sync = await syncFuture;
|
||||||
|
|
||||||
|
expect(loginState, LoginState.logged);
|
||||||
|
expect(firstSync, true);
|
||||||
|
expect(sync["next_batch"] == matrix.prevBatch, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
test('Login', loginText);
|
||||||
|
|
||||||
|
test('Try to get ErrorResponse', () async{
|
||||||
|
final resp = await matrix
|
||||||
|
.connection.jsonRequest(type: "PUT", action: "/non/existing/path");
|
||||||
|
expect(resp is ErrorResponse, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Logout', () async{
|
||||||
|
final dynamic resp = await matrix
|
||||||
|
.connection.jsonRequest(type: "POST", action: "/client/r0/logout");
|
||||||
|
expect(resp is ErrorResponse, false);
|
||||||
|
|
||||||
|
Future<LoginState> loginStateFuture = matrix.connection.onLoginStateChanged.stream.first;
|
||||||
|
|
||||||
|
matrix.connection.clear();
|
||||||
|
|
||||||
|
expect(matrix.accessToken == null, true);
|
||||||
|
expect(matrix.homeserver == null, true);
|
||||||
|
expect(matrix.userID == null, true);
|
||||||
|
expect(matrix.deviceID == null, true);
|
||||||
|
expect(matrix.deviceName == null, true);
|
||||||
|
expect(matrix.matrixVersions == null, true);
|
||||||
|
expect(matrix.lazyLoadMembers == null, true);
|
||||||
|
expect(matrix.prevBatch == null, true);
|
||||||
|
|
||||||
|
LoginState loginState = await loginStateFuture;
|
||||||
|
expect(loginState, LoginState.loggedOut);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Room Update Test', () async{
|
||||||
|
matrix.connection.onRoomUpdate.close();
|
||||||
|
|
||||||
|
List<RoomUpdate> roomUpdateList = await roomUpdateListFuture;
|
||||||
|
|
||||||
|
expect(roomUpdateList.length,3);
|
||||||
|
|
||||||
|
expect(roomUpdateList[0].id=="!726s6s6q:example.com", true);
|
||||||
|
expect(roomUpdateList[0].membership=="join", true);
|
||||||
|
expect(roomUpdateList[0].prev_batch=="t34-23535_0_0", true);
|
||||||
|
expect(roomUpdateList[0].limitedTimeline==true, true);
|
||||||
|
expect(roomUpdateList[0].notification_count==2, true);
|
||||||
|
expect(roomUpdateList[0].highlight_count==2, true);
|
||||||
|
|
||||||
|
expect(roomUpdateList[1].id=="!696r7674:example.com", true);
|
||||||
|
expect(roomUpdateList[1].membership=="invite", true);
|
||||||
|
expect(roomUpdateList[1].prev_batch=="", true);
|
||||||
|
expect(roomUpdateList[1].limitedTimeline==false, true);
|
||||||
|
expect(roomUpdateList[1].notification_count==0, true);
|
||||||
|
expect(roomUpdateList[1].highlight_count==0, true);
|
||||||
|
|
||||||
|
expect(roomUpdateList[2].id=="!5345234234:example.com", true);
|
||||||
|
expect(roomUpdateList[2].membership=="leave", true);
|
||||||
|
expect(roomUpdateList[2].prev_batch=="", true);
|
||||||
|
expect(roomUpdateList[2].limitedTimeline==false, true);
|
||||||
|
expect(roomUpdateList[2].notification_count==0, true);
|
||||||
|
expect(roomUpdateList[2].highlight_count==0, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Event Update Test', () async{
|
||||||
|
matrix.connection.onEvent.close();
|
||||||
|
|
||||||
|
List<EventUpdate> eventUpdateList = await eventUpdateListFuture;
|
||||||
|
|
||||||
|
expect(eventUpdateList.length,10);
|
||||||
|
|
||||||
|
expect(eventUpdateList[0].eventType=="m.room.member", true);
|
||||||
|
expect(eventUpdateList[0].roomID=="!726s6s6q:example.com", true);
|
||||||
|
expect(eventUpdateList[0].type=="state", true);
|
||||||
|
|
||||||
|
expect(eventUpdateList[1].eventType=="m.room.member", true);
|
||||||
|
expect(eventUpdateList[1].roomID=="!726s6s6q:example.com", true);
|
||||||
|
expect(eventUpdateList[1].type=="timeline", true);
|
||||||
|
|
||||||
|
expect(eventUpdateList[2].eventType=="m.room.message", true);
|
||||||
|
expect(eventUpdateList[2].roomID=="!726s6s6q:example.com", true);
|
||||||
|
expect(eventUpdateList[2].type=="timeline", true);
|
||||||
|
|
||||||
|
expect(eventUpdateList[3].eventType=="m.tag", true);
|
||||||
|
expect(eventUpdateList[3].roomID=="!726s6s6q:example.com", true);
|
||||||
|
expect(eventUpdateList[3].type=="account_data", true);
|
||||||
|
|
||||||
|
expect(eventUpdateList[4].eventType=="org.example.custom.room.config", true);
|
||||||
|
expect(eventUpdateList[4].roomID=="!726s6s6q:example.com", true);
|
||||||
|
expect(eventUpdateList[4].type=="account_data", true);
|
||||||
|
|
||||||
|
expect(eventUpdateList[5].eventType=="m.room.name", true);
|
||||||
|
expect(eventUpdateList[5].roomID=="!696r7674:example.com", true);
|
||||||
|
expect(eventUpdateList[5].type=="invite_state", true);
|
||||||
|
|
||||||
|
expect(eventUpdateList[6].eventType=="m.room.member", true);
|
||||||
|
expect(eventUpdateList[6].roomID=="!696r7674:example.com", true);
|
||||||
|
expect(eventUpdateList[6].type=="invite_state", true);
|
||||||
|
|
||||||
|
expect(eventUpdateList[7].eventType=="m.presence", true);
|
||||||
|
expect(eventUpdateList[7].roomID=="presence", true);
|
||||||
|
expect(eventUpdateList[7].type=="presence", true);
|
||||||
|
|
||||||
|
expect(eventUpdateList[8].eventType=="org.example.custom.config", true);
|
||||||
|
expect(eventUpdateList[8].roomID=="account_data", true);
|
||||||
|
expect(eventUpdateList[8].type=="account_data", true);
|
||||||
|
|
||||||
|
expect(eventUpdateList[9].eventType=="m.new_device", true);
|
||||||
|
expect(eventUpdateList[9].roomID=="to_device", true);
|
||||||
|
expect(eventUpdateList[9].type=="to_device", true);
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should get created', create);
|
||||||
|
|
||||||
|
test('Login', loginText);
|
||||||
|
|
||||||
|
test('Logout when token is unknown', () async{
|
||||||
|
Future<LoginState> loginStateFuture = matrix.connection.onLoginStateChanged.stream.first;
|
||||||
|
final resp = await matrix
|
||||||
|
.connection.jsonRequest(type: "DELETE", action: "/unknown/token");
|
||||||
|
|
||||||
|
LoginState state = await loginStateFuture;
|
||||||
|
expect(state, LoginState.loggedOut);
|
||||||
|
expect(matrix.isLogged(), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
192
test/FakeMatrixApi.dart
Normal file
192
test/FakeMatrixApi.dart
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
import 'package:http/testing.dart';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:core';
|
||||||
|
import 'dart:math';
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
|
||||||
|
class FakeMatrixApi extends MockClient {
|
||||||
|
FakeMatrixApi()
|
||||||
|
: super((request) async {
|
||||||
|
// Collect data from Request
|
||||||
|
final String action = request.url.path.split("/_matrix")[1];
|
||||||
|
final String method = request.method;
|
||||||
|
final dynamic data =
|
||||||
|
method == "GET" ? request.url.queryParameters : request.body;
|
||||||
|
var res = {};
|
||||||
|
|
||||||
|
//print("$method request to $action with Data: $data");
|
||||||
|
|
||||||
|
// Sync requests with timeout
|
||||||
|
if (data is Map<String, dynamic> && data["timeout"] is String) {
|
||||||
|
await new Future.delayed(Duration(seconds: 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call API
|
||||||
|
if (api.containsKey(method) && api[method].containsKey(action))
|
||||||
|
res = api[method][action](data);
|
||||||
|
else
|
||||||
|
res = {
|
||||||
|
"errcode": "M_UNRECOGNIZED",
|
||||||
|
"error": "Unrecognized request"
|
||||||
|
};
|
||||||
|
|
||||||
|
return Response(json.encode(res), 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
static final Map<String, Map<String, dynamic>> api = {
|
||||||
|
"GET": {
|
||||||
|
"/client/versions": (var req) => {
|
||||||
|
"versions": ["r0.0.1", "r0.1.0", "r0.2.0", "r0.3.0", "r0.4.0"],
|
||||||
|
"unstable_features": {"m.lazy_load_members": true},
|
||||||
|
},
|
||||||
|
"/client/r0/login": (var req) => {
|
||||||
|
"flows": [
|
||||||
|
{"type": "m.login.password"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"/client/r0/sync": (var req) => {
|
||||||
|
"next_batch": Random().nextDouble().toString(),
|
||||||
|
"presence": {
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"sender": "@alice:example.com",
|
||||||
|
"type": "m.presence",
|
||||||
|
"content": {"presence": "online"}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"account_data": {
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"type": "org.example.custom.config",
|
||||||
|
"content": {"custom_config_key": "custom_config_value"}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"to_device": {
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"sender": "@alice:example.com",
|
||||||
|
"type": "m.new_device",
|
||||||
|
"content": {
|
||||||
|
"device_id": "XYZABCDE",
|
||||||
|
"rooms": ["!726s6s6q:example.com"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"rooms": {
|
||||||
|
"join": {
|
||||||
|
"!726s6s6q:example.com": {
|
||||||
|
"unread_notifications": {
|
||||||
|
"highlight_count": 2,
|
||||||
|
"notification_count": 2,
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"sender": "@alice:example.com",
|
||||||
|
"type": "m.room.member",
|
||||||
|
"state_key": "@alice:example.com",
|
||||||
|
"content": {"membership": "join"},
|
||||||
|
"origin_server_ts": 1417731086795,
|
||||||
|
"event_id": "66697273743031:example.com"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"timeline": {
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"sender": "@bob:example.com",
|
||||||
|
"type": "m.room.member",
|
||||||
|
"state_key": "@bob:example.com",
|
||||||
|
"content": {"membership": "join"},
|
||||||
|
"prev_content": {"membership": "invite"},
|
||||||
|
"origin_server_ts": 1417731086795,
|
||||||
|
"event_id": "7365636s6r6432:example.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sender": "@alice:example.com",
|
||||||
|
"type": "m.room.message",
|
||||||
|
"txn_id": "1234",
|
||||||
|
"content": {"body": "I am a fish", "msgtype": "m.text"},
|
||||||
|
"origin_server_ts": 1417731086797,
|
||||||
|
"event_id": "74686972643033:example.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"limited": true,
|
||||||
|
"prev_batch": "t34-23535_0_0"
|
||||||
|
},
|
||||||
|
"ephemeral": {
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"type": "m.typing",
|
||||||
|
"content": {
|
||||||
|
"user_ids": ["@alice:example.com"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"account_data": {
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"type": "m.tag",
|
||||||
|
"content": {
|
||||||
|
"tags": {
|
||||||
|
"work": {"order": 1}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "org.example.custom.room.config",
|
||||||
|
"content": {"custom_config_key": "custom_config_value"}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"invite": {
|
||||||
|
"!696r7674:example.com": {
|
||||||
|
"invite_state": {
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"sender": "@alice:example.com",
|
||||||
|
"type": "m.room.name",
|
||||||
|
"state_key": "",
|
||||||
|
"content": {"name": "My Room Name"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sender": "@alice:example.com",
|
||||||
|
"type": "m.room.member",
|
||||||
|
"state_key": "@bob:example.com",
|
||||||
|
"content": {"membership": "invite"}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"leave": {
|
||||||
|
"!5345234234:example.com": {
|
||||||
|
"timeline": {"events": []}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"POST": {
|
||||||
|
"/client/r0/login": (var req) => {
|
||||||
|
"user_id": "@test:fakeServer.notExisting",
|
||||||
|
"access_token": "abc123",
|
||||||
|
"device_id": "GHTYAJCE"
|
||||||
|
},
|
||||||
|
"/client/r0/logout": (var reqI) => {},
|
||||||
|
"/client/r0/logout/all": (var reqI) => {},
|
||||||
|
},
|
||||||
|
"PUT": {},
|
||||||
|
"DELETE": {
|
||||||
|
"/unknown/token": (var req) => {
|
||||||
|
"errcode": "M_UNKNOWN_TOKEN"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in a new issue