FurryChat/lib/utils/firebase_controller.dart

373 lines
13 KiB
Dart
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:convert';
import 'dart:io';
import 'package:bot_toast/bot_toast.dart';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:furrychat/components/matrix.dart';
import 'package:furrychat/utils/app_route.dart';
import 'package:furrychat/views/chat.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_gen/gen_l10n/l10n_en.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:path_provider/path_provider.dart';
import '../components/matrix.dart';
import 'famedlysdk_store.dart';
import 'matrix_locals.dart';
abstract class FirebaseController {
static final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
static final FlutterLocalNotificationsPlugin
_flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
static BuildContext context;
static const String CHANNEL_ID = 'message';
static const String CHANNEL_NAME = 'FurryChat push channel';
static const String CHANNEL_DESCRIPTION = 'Push notifications for FurryChat';
static const String APP_ID = 'dev.inex.furrychat';
static const String GATEWAY_URL = 'https://push-gateway.inex.dev:443/';
static const String PUSHER_FORMAT = 'event_id_only';
static Future<void> setupFirebase(
MatrixState matrix, String clientName) async {
final client = matrix.client;
if (Platform.isIOS) iOS_Permission();
String token;
try {
token = await _firebaseMessaging.getToken();
} catch (_) {
token = null;
}
if (token?.isEmpty ?? true) {
final storeItem =
await matrix.store.getItem('chat.fluffy.show_no_google');
final configOptionMissing = storeItem == null || storeItem.isEmpty;
if (configOptionMissing || (!configOptionMissing && storeItem == '1')) {
BotToast.showText(
text: L10n.of(context).noGoogleServicesWarning,
duration: Duration(seconds: 15),
);
if (configOptionMissing) {
await matrix.store.setItem('chat.fluffy.show_no_google', '0');
}
}
return;
}
final pushers = await client.requestPushers().catchError((e) {
debugPrint('[Push] Unable to request pushers: ${e.toString()}');
return [];
});
final currentPushers = pushers.where((pusher) => pusher.pushkey == token);
if (currentPushers.length == 1 &&
currentPushers.first.kind == 'http' &&
currentPushers.first.appId == APP_ID &&
currentPushers.first.appDisplayName == clientName &&
currentPushers.first.deviceDisplayName == client.deviceName &&
currentPushers.first.lang == 'en' &&
currentPushers.first.data.url.toString() == GATEWAY_URL &&
currentPushers.first.data.format == PUSHER_FORMAT) {
debugPrint('[Push] Pusher already set');
} else {
if (currentPushers.isNotEmpty) {
for (final currentPusher in currentPushers) {
currentPusher.pushkey = token;
currentPusher.kind = 'null';
await client.setPusher(
currentPusher,
append: true,
);
debugPrint('[Push] Remove legacy pusher for this device');
}
}
await client
.setPusher(
Pusher(
token,
APP_ID,
clientName,
client.deviceName,
'en',
PusherData(
url: Uri.parse(GATEWAY_URL),
format: PUSHER_FORMAT,
),
kind: 'http',
),
append: false,
)
.catchError((e) {
debugPrint('[Push] Unable to set pushers: ${e.toString()}');
return [];
});
}
Function goToRoom = (dynamic message) async {
try {
String roomId;
if (message is String) {
roomId = message;
} else if (message is Map) {
roomId = (message['data'] ?? message)['room_id'];
}
if (roomId?.isEmpty ?? true) throw ('Bad roomId');
await Navigator.of(context).pushAndRemoveUntil(
AppRoute.defaultRoute(
context,
ChatView(roomId),
),
(r) => r.isFirst);
} catch (_) {
BotToast.showText(text: 'Failed to open chat...');
debugPrint(_);
}
};
// initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project
var initializationSettingsAndroid =
AndroidInitializationSettings('notifications_icon');
var initializationSettingsIOS =
IOSInitializationSettings(onDidReceiveLocalNotification: (i, a, b, c) {
return null;
});
var initializationSettings = InitializationSettings(
android: initializationSettingsAndroid, iOS: initializationSettingsIOS);
await _flutterLocalNotificationsPlugin.initialize(initializationSettings,
onSelectNotification: goToRoom);
_firebaseMessaging.configure(
onMessage: _onMessage,
onBackgroundMessage: _onMessage,
onResume: goToRoom,
onLaunch: goToRoom,
);
debugPrint('[Push] Firebase initialized');
return;
}
static Future<dynamic> _onMessage(Map<String, dynamic> message) async {
try {
final data = message['data'] ?? message;
final String roomId = data['room_id'];
final String eventId = data['event_id'];
final int unread = json.decode(data['counts'])['unread'];
if ((roomId?.isEmpty ?? true) ||
(eventId?.isEmpty ?? true) ||
unread == 0) {
await _flutterLocalNotificationsPlugin.cancelAll();
return null;
}
if (context != null && Matrix.of(context).activeRoomId == roomId) {
debugPrint('[Push] New clearing push');
return null;
}
debugPrint('[Push] New message received');
// FIXME unable to init without context currently https://github.com/flutter/flutter/issues/67092
// Locked on EN until issue resolved
final i18n = context == null ? L10nEn() : L10n.of(context);
// Get the client
Client client;
var tempClient = false;
try {
client = Matrix.of(context).client;
} catch (_) {
client = null;
}
if (client == null) {
tempClient = true;
final platform = kIsWeb ? 'Web' : Platform.operatingSystem;
final clientName = 'FluffyChat $platform';
client = Client(clientName);
client.database = await getDatabase(client);
client.connect();
debugPrint('[Push] Use a temp client');
await client.onLoginStateChanged.stream
.firstWhere((l) => l == LoginState.logged)
.timeout(
Duration(seconds: 5),
);
}
// Get the room
var room = client.getRoomById(roomId);
if (room == null) {
debugPrint('[Push] Wait for the room');
await client.onRoomUpdate.stream
.where((u) => u.id == roomId)
.first
.timeout(Duration(seconds: 5));
debugPrint('[Push] Room found');
room = client.getRoomById(roomId);
if (room == null) return null;
}
// Get the event
var event = await client.database.getEventById(client.id, eventId, room);
if (event == null) {
debugPrint('[Push] Wait for the event');
final eventUpdate = await client.onEvent.stream
.where((u) => u.content['event_id'] == eventId)
.first
.timeout(Duration(seconds: 5));
debugPrint('[Push] Event found');
event = Event.fromJson(eventUpdate.content, room);
if (room == null) return null;
}
// Count all unread events
var unreadEvents = 0;
client.rooms
.forEach((Room room) => unreadEvents += room.notificationCount);
// Calculate title
final title = unread > 1
? i18n.unreadMessagesInChats(
unreadEvents.toString(), unread.toString())
: i18n.unreadMessages(unreadEvents.toString());
// Calculate the body
final body = event.getLocalizedBody(
MatrixLocals(i18n),
withSenderNamePrefix: true,
hideReply: true,
);
// The person object for the android message style notification
final person = Person(
name: room.getLocalizedDisplayname(MatrixLocals(i18n)),
icon: room.avatar == null
? null
: BitmapFilePathAndroidIcon(
await downloadAndSaveAvatar(
room.avatar,
client,
width: 126,
height: 126,
),
),
);
// Show notification
var androidPlatformChannelSpecifics = AndroidNotificationDetails(
CHANNEL_ID, CHANNEL_NAME, CHANNEL_DESCRIPTION,
styleInformation: MessagingStyleInformation(
person,
conversationTitle: title,
messages: [
Message(
body,
event.originServerTs,
person,
)
],
),
importance: Importance.max,
priority: Priority.high,
ticker: i18n.newMessageInFluffyChat);
var iOSPlatformChannelSpecifics = IOSNotificationDetails();
var platformChannelSpecifics = NotificationDetails(
android: androidPlatformChannelSpecifics,
iOS: iOSPlatformChannelSpecifics);
await _flutterLocalNotificationsPlugin.show(
0,
room.getLocalizedDisplayname(MatrixLocals(i18n)),
body,
platformChannelSpecifics,
payload: roomId);
if (tempClient) {
await client.dispose();
client = null;
debugPrint('[Push] Temp client disposed');
}
} catch (exception) {
debugPrint('[Push] Error while processing notification: ' +
exception.toString());
await _showDefaultNotification(message);
}
return null;
}
static Future<dynamic> _showDefaultNotification(
Map<String, dynamic> message) async {
try {
var flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
// Init notifications framework
var initializationSettingsAndroid =
AndroidInitializationSettings('notifications_icon');
var initializationSettingsIOS = IOSInitializationSettings();
var initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOS);
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
// FIXME unable to init without context currently https://github.com/flutter/flutter/issues/67092
// Locked on en for now
//final l10n = L10n(Platform.localeName);
final l10n = L10nEn();
// Notification data and matrix data
Map<dynamic, dynamic> data = message['data'] ?? message;
String eventID = data['event_id'];
String roomID = data['room_id'];
final int unread = data.containsKey('counts')
? json.decode(data['counts'])['unread']
: 1;
await flutterLocalNotificationsPlugin.cancelAll();
if (unread == 0 || roomID == null || eventID == null) {
return;
}
// Display notification
var androidPlatformChannelSpecifics = AndroidNotificationDetails(
CHANNEL_ID, CHANNEL_NAME, CHANNEL_DESCRIPTION,
importance: Importance.max, priority: Priority.high);
var iOSPlatformChannelSpecifics = IOSNotificationDetails();
var platformChannelSpecifics = NotificationDetails(
android: androidPlatformChannelSpecifics,
iOS: iOSPlatformChannelSpecifics);
final title = l10n.unreadChats(unread.toString());
await flutterLocalNotificationsPlugin.show(
1, title, l10n.openAppToReadMessages, platformChannelSpecifics,
payload: roomID);
} catch (exception) {
debugPrint('[Push] Error while processing background notification: ' +
exception.toString());
}
return Future<void>.value();
}
static Future<String> downloadAndSaveAvatar(Uri content, Client client,
{int width, int height}) async {
final thumbnail = width == null && height == null ? false : true;
final tempDirectory = (await getTemporaryDirectory()).path;
final prefix = thumbnail ? 'thumbnail' : '';
var file =
File('$tempDirectory/${prefix}_${content.toString().split("/").last}');
if (!file.existsSync()) {
final url = thumbnail
? content.getThumbnail(client, width: width, height: height)
: content.getDownloadLink(client);
var request = await HttpClient().getUrl(Uri.parse(url));
var response = await request.close();
var bytes = await consolidateHttpClientResponseBytes(response);
await file.writeAsBytes(bytes);
}
return file.path;
}
static void iOS_Permission() {
_firebaseMessaging.requestNotificationPermissions(
IosNotificationSettings(sound: true, badge: true, alert: true));
_firebaseMessaging.onIosSettingsRegistered
.listen((IosNotificationSettings settings) {
debugPrint('Settings registered: $settings');
});
}
}