support for registering pushers
This commit is contained in:
parent
88cdcf9863
commit
fe871217b2
21 changed files with 350 additions and 22 deletions
|
|
@ -2,6 +2,7 @@ import "dart:ffi";
|
|||
import "dart:io";
|
||||
import "dart:isolate";
|
||||
import "dart:math";
|
||||
|
||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||
import "package:ffi/ffi.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
|
|
@ -14,6 +15,7 @@ import "package:nexus/controllers/sync_status.dart";
|
|||
import "package:nexus/controllers/top_level_spaces.dart";
|
||||
import "package:nexus/helpers/extensions/gomuks_buffer.dart";
|
||||
import "package:nexus/main.dart";
|
||||
import "package:nexus/models/capabilities.dart";
|
||||
import "package:nexus/models/content/message.dart";
|
||||
import "package:nexus/models/event.dart";
|
||||
import "package:nexus/models/oauth_auth_code_response.dart";
|
||||
|
|
@ -30,6 +32,7 @@ import "package:nexus/models/requests/oauth/get_auth_url.dart";
|
|||
import "package:nexus/models/requests/oauth/register_client.dart";
|
||||
import "package:nexus/models/requests/paginate.dart";
|
||||
import "package:nexus/models/requests/redact_event.dart";
|
||||
import "package:nexus/models/requests/register_pusher.dart";
|
||||
import "package:nexus/models/requests/report.dart";
|
||||
import "package:nexus/models/requests/send_event.dart";
|
||||
import "package:nexus/models/requests/send_message.dart";
|
||||
|
|
@ -273,6 +276,9 @@ class ClientController extends AsyncNotifier<int> {
|
|||
Future<void> setAccountData(SetAccountDataRequest request) =>
|
||||
_sendCommand("set_account_data", request.toJson());
|
||||
|
||||
Future<void> registerPusher(RegisterPusherRequest request) =>
|
||||
_sendCommand("register_homeserver_push", request.toJson());
|
||||
|
||||
Future<MessageContent> uploadMedia(UploadMediaRequest request) async =>
|
||||
.fromJson(await _sendCommand("upload_media", request.toJson()));
|
||||
|
||||
|
|
@ -310,6 +316,10 @@ class ClientController extends AsyncNotifier<int> {
|
|||
Future<SpecVersionsResponse> getSpecVersions() async =>
|
||||
.fromJson(await _sendCommand("get_versions"));
|
||||
|
||||
Future<Capabilities> getCapabilities() async => Capabilities.fromJson(
|
||||
(await _sendCommand("get_capabilities"))["capabilities"],
|
||||
);
|
||||
|
||||
Future<Uri?> discoverHomeserver(Uri homeserver) async {
|
||||
try {
|
||||
final response = await _sendCommand("discover_homeserver", {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import "package:nexus/controllers/account_data.dart";
|
|||
import "package:nexus/controllers/client.dart";
|
||||
import "package:nexus/controllers/client_state.dart";
|
||||
import "package:nexus/controllers/settings.dart";
|
||||
import "package:nexus/controllers/unified_push.dart";
|
||||
import "package:nexus/controllers/spec_versions.dart";
|
||||
import "package:nexus/models/account_data.dart";
|
||||
import "package:nexus/models/settings_category.dart";
|
||||
|
|
@ -95,6 +96,46 @@ class SettingsSectionsController
|
|||
if (ref.watch(ClientStateController.provider)?.isLoggedIn == true)
|
||||
"Account": .new([
|
||||
.new(title: "Profile", icon: Icons.person, settings: .new([])),
|
||||
.new(
|
||||
title: "Notifications",
|
||||
icon: Icons.notifications,
|
||||
settings: .new([
|
||||
// TODO: Disable if not supported
|
||||
.new(
|
||||
title: "Push notifications",
|
||||
description: "Enable push notifications using Web Push",
|
||||
builder: (title, description, icon) => Consumer(
|
||||
builder: (context, ref, _) {
|
||||
final pusherRegistered = ref.watch(
|
||||
UnifiedPushController.provider,
|
||||
);
|
||||
return SwitchListTile(
|
||||
title: Text(title),
|
||||
subtitle: Text(description),
|
||||
secondary: Icon(icon),
|
||||
value: pusherRegistered.maybeWhen(
|
||||
data: (value) => value,
|
||||
orElse: () => false,
|
||||
),
|
||||
onChanged: pusherRegistered.maybeWhen(
|
||||
data: (_) =>
|
||||
(value) => value
|
||||
? ref
|
||||
.watch(
|
||||
UnifiedPushController.provider.notifier,
|
||||
)
|
||||
.register()
|
||||
.onError(showError)
|
||||
: /*deregeister*/ null,
|
||||
orElse: () => null,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
icon: Icons.notification_add,
|
||||
),
|
||||
]),
|
||||
),
|
||||
.new(
|
||||
title: "Safety",
|
||||
icon: Icons.gpp_good,
|
||||
|
|
@ -166,6 +207,8 @@ class SettingsSectionsController
|
|||
onPressed: () async {
|
||||
Navigator.of(context)
|
||||
.popUntil((route) => route.isFirst);
|
||||
Navigator.of(context)
|
||||
.popUntil((route) => route.isFirst);
|
||||
|
||||
await WidgetsBinding.instance.endOfFrame;
|
||||
|
||||
|
|
|
|||
106
lib/controllers/unified_push.dart
Normal file
106
lib/controllers/unified_push.dart
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import "dart:io";
|
||||
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:intl/intl.dart";
|
||||
import "package:nexus/main.dart";
|
||||
import "package:nexus/controllers/client.dart";
|
||||
import "package:nexus/controllers/client_state.dart";
|
||||
import "package:nexus/models/requests/register_pusher.dart";
|
||||
import "package:unifiedpush/unifiedpush.dart";
|
||||
import "package:unifiedpush_storage_shared_preferences/storage.dart";
|
||||
|
||||
class UnifiedPushController extends AsyncNotifier<bool> {
|
||||
@override
|
||||
Future<bool> build() async {
|
||||
final registered = await UnifiedPush.initialize(
|
||||
linuxOptions: .new(
|
||||
dbusName: "nexus.federated.nexus",
|
||||
storage: UnifiedPushStorageSharedPreferences(),
|
||||
background: isInBackground,
|
||||
),
|
||||
onNewEndpoint: (endpoint, instance) async {
|
||||
final client = ref.watch(ClientController.provider.notifier);
|
||||
await client.registerPusher(
|
||||
.new(
|
||||
appDisplayName: "Nexus",
|
||||
appId: "nexus.federated.nexus",
|
||||
data: PusherData.webPush(
|
||||
url: Uri.parse(endpoint.url),
|
||||
auth: endpoint.pubKeySet!.auth,
|
||||
),
|
||||
deviceDisplayName:
|
||||
"Nexus on ${toBeginningOfSentenceCase(Platform.operatingSystem)}",
|
||||
kind: .webPush,
|
||||
lang: "en",
|
||||
pushKey: endpoint.pubKeySet!.pubKey,
|
||||
),
|
||||
|
||||
// Pusher(
|
||||
// appDisplayName: ,
|
||||
// data: {
|
||||
// "url": endpoint.url,
|
||||
// "auth": endpoint.pubKeySet.auth,
|
||||
// "format": "event_id_only",
|
||||
// },
|
||||
// deviceDisplayName: "Nexus",
|
||||
// kind: "org.matrix.msc4174.webpush",
|
||||
// lang: WidgetsBinding.instance.platformDispatcher.locale
|
||||
// .toLanguageTag(),
|
||||
// pushkey: endpoint.pubKeySet.publicKey,
|
||||
// ),
|
||||
);
|
||||
},
|
||||
onMessage: (message, instance) async {
|
||||
print(message);
|
||||
// TODO: Handle incoming message
|
||||
},
|
||||
onRegistrationFailed: (e, r) {
|
||||
throw "e";
|
||||
},
|
||||
onUnregistered: (_) {
|
||||
throw "r";
|
||||
},
|
||||
onTempUnavailable: (_) {
|
||||
throw "t";
|
||||
},
|
||||
);
|
||||
|
||||
if (registered) {
|
||||
// Needs to be registered every startup
|
||||
await register(true);
|
||||
}
|
||||
|
||||
return registered;
|
||||
}
|
||||
|
||||
Future<void> register([bool early = false]) async {
|
||||
final clientState = ref.watch(ClientStateController.provider);
|
||||
if (clientState?.deviceId == null) ref.invalidateSelf();
|
||||
|
||||
final alreadyRegistered = early ? true : await future;
|
||||
final client = ref.watch(ClientController.provider.notifier);
|
||||
final capabilities = await client.getCapabilities();
|
||||
|
||||
if (capabilities.webpush?.vapid == null) {
|
||||
throw UnsupportedError(
|
||||
"Your homeserver does not support MSC4174 (Web Push), and therefore cannot send notifications to Nexus.",
|
||||
);
|
||||
}
|
||||
|
||||
if (!alreadyRegistered &&
|
||||
!await UnifiedPush.tryUseCurrentOrDefaultDistributor()) {
|
||||
throw Exception("No UnifiedPush distributors found");
|
||||
}
|
||||
|
||||
await UnifiedPush.register(
|
||||
instance: clientState!.deviceId!,
|
||||
vapid: capabilities.webpush?.vapid,
|
||||
);
|
||||
|
||||
if (!alreadyRegistered) ref.invalidateSelf();
|
||||
}
|
||||
|
||||
static final provider = AsyncNotifierProvider<UnifiedPushController, bool>(
|
||||
UnifiedPushController.new,
|
||||
);
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import "package:nexus/controllers/client_state.dart";
|
|||
import "package:nexus/controllers/multi_provider.dart";
|
||||
import "package:nexus/controllers/settings.dart";
|
||||
import "package:nexus/controllers/shared_prefs.dart";
|
||||
import "package:nexus/controllers/unified_push.dart";
|
||||
import "package:nexus/helpers/extensions/better_when.dart";
|
||||
import "package:nexus/helpers/extensions/scheme_to_theme.dart";
|
||||
import "package:nexus/helpers/font_licenses.dart";
|
||||
|
|
@ -22,6 +23,7 @@ import "package:window_manager/window_manager.dart";
|
|||
import "package:material_ui/material_ui.dart";
|
||||
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
late final bool isInBackground;
|
||||
|
||||
final class Logger extends ProviderObserver {
|
||||
@override
|
||||
|
|
@ -62,7 +64,7 @@ void showError(Object error, [StackTrace? stackTrace]) {
|
|||
}
|
||||
}
|
||||
|
||||
void main() async {
|
||||
void main(List<String> args) async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
MediaKit.ensureInitialized();
|
||||
|
||||
|
|
@ -77,25 +79,30 @@ void main() async {
|
|||
await windowManager.setMinimumSize(Size.square(500));
|
||||
}
|
||||
|
||||
isInBackground = args.contains("--unifiedpush-bg");
|
||||
|
||||
LicenseRegistry.addLicense(() => Stream.fromIterable(fontLicenses));
|
||||
|
||||
FlutterError.onError = (FlutterErrorDetails details) =>
|
||||
showError(details.exception.toString(), details.stack);
|
||||
|
||||
runApp(
|
||||
ProviderScope(
|
||||
retry: (_, _) => null,
|
||||
observers: [
|
||||
// Change false to true if you want debug information on provider reloads
|
||||
// ignore: dead_code
|
||||
if (false && kDebugMode) Logger(),
|
||||
],
|
||||
child: const App(),
|
||||
),
|
||||
);
|
||||
if (!isInBackground) {
|
||||
runApp(
|
||||
ProviderScope(
|
||||
retry: (_, _) => null,
|
||||
observers: [
|
||||
// Change false to true if you want debug information on provider reloads
|
||||
// ignore: dead_code
|
||||
if (false && kDebugMode) Logger(),
|
||||
],
|
||||
child: App(isInBackground),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class const App({super.key}) extends StatelessWidget {
|
||||
class const App(final bool isInBackground, {super.key})
|
||||
extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => DynamicColorBuilder(
|
||||
builder: (lightDynamic, darkDynamic) => Consumer(
|
||||
|
|
@ -138,6 +145,7 @@ class const App({super.key}) extends StatelessWidget {
|
|||
IListConst([
|
||||
SharedPrefsController.provider,
|
||||
ClientController.provider,
|
||||
UnifiedPushController.provider,
|
||||
]),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
25
lib/models/capabilities.dart
Normal file
25
lib/models/capabilities.dart
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import "package:freezed_annotation/freezed_annotation.dart";
|
||||
|
||||
part "capabilities.freezed.dart";
|
||||
part "capabilities.g.dart";
|
||||
|
||||
@Freezed(toJson: false, fromJson: false)
|
||||
@JsonSerializable()
|
||||
class const Capabilities({
|
||||
@JsonKey(name: "org.matrix.msc4174.webpush") final WebPush? webpush,
|
||||
}) with _$Capabilities {
|
||||
Map<String, Object?> toJson() => _$CapabilitiesToJson(this);
|
||||
|
||||
factory Capabilities.fromJson(Map<String, Object?> json) =>
|
||||
_$CapabilitiesFromJson(json);
|
||||
}
|
||||
|
||||
@Freezed(toJson: false, fromJson: false)
|
||||
@JsonSerializable()
|
||||
class const WebPush({required final bool enabled, final String? vapid})
|
||||
with _$WebPush {
|
||||
Map<String, Object?> toJson() => _$WebPushToJson(this);
|
||||
|
||||
factory WebPush.fromJson(Map<String, Object?> json) =>
|
||||
_$WebPushFromJson(json);
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ class const ClientState({
|
|||
required final bool isLoggedIn,
|
||||
required final bool isVerified,
|
||||
required final String? userId,
|
||||
required final String? deviceId,
|
||||
required final String? homeserverUrl,
|
||||
}) with _$ClientState {
|
||||
Map<String, Object?> toJson() => _$ClientStateToJson(this);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import "package:freezed_annotation/freezed_annotation.dart";
|
||||
|
||||
@JsonEnum(fieldRename: FieldRename.snake)
|
||||
@JsonEnum(fieldRename: .snake)
|
||||
enum JoinRule { public, knock, invite, private, restricted, knockRestricted }
|
||||
|
|
|
|||
|
|
@ -1,4 +1 @@
|
|||
import "package:freezed_annotation/freezed_annotation.dart";
|
||||
|
||||
@JsonEnum()
|
||||
enum MembershipAction { ban, kick, unban, invite }
|
||||
|
|
|
|||
|
|
@ -1,4 +1 @@
|
|||
import "package:freezed_annotation/freezed_annotation.dart";
|
||||
|
||||
@JsonEnum()
|
||||
enum MembershipStatus { leave, invite, ban, join, knock }
|
||||
|
|
|
|||
56
lib/models/requests/register_pusher.dart
Normal file
56
lib/models/requests/register_pusher.dart
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import "package:freezed_annotation/freezed_annotation.dart";
|
||||
|
||||
part "register_pusher.freezed.dart";
|
||||
part "register_pusher.g.dart";
|
||||
|
||||
@Freezed(toJson: false, fromJson: false)
|
||||
@JsonSerializable()
|
||||
class const RegisterPusherRequest({
|
||||
required final String appDisplayName,
|
||||
required final String appId,
|
||||
final bool append = false,
|
||||
required final PusherData data,
|
||||
required final String deviceDisplayName,
|
||||
required final PusherKind kind,
|
||||
required final String lang,
|
||||
|
||||
/// TODO: What does this do?
|
||||
final String? profileTag,
|
||||
|
||||
@JsonKey(name: "pushkey") required final String pushKey,
|
||||
}) with _$RegisterPusherRequest {
|
||||
Map<String, Object?> toJson() => _$RegisterPusherRequestToJson(this);
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class PusherData with _$PusherData {
|
||||
const factory PusherData.http({
|
||||
required Uri url,
|
||||
@Default(PushFormat.eventIdOnly) PushFormat format,
|
||||
}) = HttpPusherData;
|
||||
|
||||
const factory PusherData.webPush({
|
||||
required Uri url,
|
||||
@Default(PushFormat.eventIdOnly) PushFormat format,
|
||||
|
||||
/// `data.auth`: RFC8291 authentication secret.
|
||||
required String auth,
|
||||
}) = WebPushPusherData;
|
||||
|
||||
factory PusherData.fromJson(Map<String, Object?> json) =>
|
||||
_$PusherDataFromJson(json);
|
||||
}
|
||||
|
||||
@JsonEnum(fieldRename: .snake)
|
||||
enum PushFormat {
|
||||
@JsonValue(null)
|
||||
all,
|
||||
eventIdOnly,
|
||||
}
|
||||
|
||||
enum PusherKind {
|
||||
http,
|
||||
email,
|
||||
@JsonValue("org.matrix.msc4174.webpush")
|
||||
webPush,
|
||||
}
|
||||
Loading…
Reference in a new issue