feat: realtime user event (#3241)

* feat: update user profile after receiving realtime user event

* chore: logout if other deivce enable encyrption

* test: fix test

* chore: fix checkbox UI

* chore: fix tauri build

* chore: fix device id

* chore: fix duplicate run appflowy
This commit is contained in:
Nathan.fooo 2023-08-20 14:13:54 +08:00 committed by GitHub
parent c5719be7ae
commit a1647bee78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 814 additions and 865 deletions

View File

@ -382,7 +382,8 @@ Widget? _buildHeaderIcon(GroupData customData) {
case FieldType.Checkbox:
final group = customData.asCheckboxGroup()!;
if (group.isCheck) {
widget = const FlowySvg(FlowySvgs.check_filled_s);
widget =
const FlowySvg(FlowySvgs.check_filled_s, blendMode: BlendMode.dst,);
} else {
widget = const FlowySvg(FlowySvgs.uncheck_s);
}

View File

@ -41,7 +41,10 @@ class _CheckboxCardCellState extends State<CheckboxCardCell> {
previous.isSelected != current.isSelected,
builder: (context, state) {
final icon = state.isSelected
? const FlowySvg(FlowySvgs.check_filled_s)
? const FlowySvg(
FlowySvgs.check_filled_s,
blendMode: BlendMode.dst,
)
: const FlowySvg(FlowySvgs.uncheck_s);
return Align(
alignment: Alignment.centerLeft,

View File

@ -89,7 +89,10 @@ class CheckboxCellCheck extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const FlowySvg(FlowySvgs.check_filled_s);
return const FlowySvg(
FlowySvgs.check_filled_s,
blendMode: BlendMode.dst,
);
}
}

View File

@ -109,7 +109,10 @@ class _ChecklistOptionCellState extends State<_ChecklistOptionCell> {
@override
Widget build(BuildContext context) {
final icon = widget.option.isSelected
? const FlowySvg(FlowySvgs.check_filled_s)
? const FlowySvg(
FlowySvgs.check_filled_s,
blendMode: BlendMode.dst,
)
: const FlowySvg(FlowySvgs.uncheck_s);
return _wrapPopover(
SizedBox(

View File

@ -21,7 +21,6 @@ import 'package:appflowy/workspace/application/user/prelude.dart';
import 'package:appflowy/workspace/application/workspace/prelude.dart';
import 'package:appflowy/workspace/application/edit_panel/edit_panel_bloc.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/menu/prelude.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/user/application/prelude.dart';
import 'package:appflowy/user/presentation/router.dart';
@ -116,7 +115,6 @@ void _resolveHomeDeps(GetIt getIt) {
getIt.registerFactoryParam<WelcomeBloc, UserProfilePB, void>(
(user, _) => WelcomeBloc(
userService: UserBackendService(userId: user.id),
userWorkspaceListener: UserWorkspaceListener(userProfile: user),
),
);
@ -141,10 +139,6 @@ void _resolveFolderDeps(GetIt getIt) {
),
);
getIt.registerFactoryParam<MenuUserBloc, UserProfilePB, void>(
(user, _) => MenuUserBloc(user),
);
//Settings
getIt.registerFactoryParam<SettingsDialogBloc, UserProfilePB, void>(
(user, _) => SettingsDialogBloc(user),

View File

@ -40,6 +40,11 @@ class InitSupabaseTask extends LaunchTask {
debug: kDebugMode,
localStorage: const SupabaseLocalStorage(),
);
if (realtimeService != null) {
await realtimeService?.dispose();
realtimeService = null;
}
realtimeService = SupbaseRealtimeService(supabase: initializedSupabase);
supabase = initializedSupabase;

View File

@ -108,7 +108,9 @@ class SupabaseAuthService implements AuthService {
return _appFlowyAuthService.signUpWithOAuth(platform: platform);
}
// Before signing in, sign out any existing users. Otherwise, the callback will be triggered even if the user doesn't click the 'Sign In' button on the website
await _auth.signOut();
if (_auth.currentUser != null) {
await _auth.signOut();
}
final provider = platform.toProvider();
final completer = supabaseLoginCompleter(

View File

@ -1,12 +1,16 @@
import 'dart:async';
import 'dart:convert';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/user_auth_listener.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'auth/auth_service.dart';
/// A service to manage realtime interactions with Supabase.
///
/// `SupbaseRealtimeService` handles subscribing to table changes in Supabase
@ -15,53 +19,73 @@ import 'package:supabase_flutter/supabase_flutter.dart';
/// accordingly.
class SupbaseRealtimeService {
final Supabase supabase;
final _authStateListener = UserAuthStateListener();
bool isLoggingOut = false;
RealtimeChannel? channel;
StreamSubscription<AuthState>? authStateSubscription;
SupbaseRealtimeService({required this.supabase}) {
_subscribeAuthState();
_subscribeTablesChanges();
_authStateListener.start(
didSignIn: () {
_subscribeTablesChanges();
isLoggingOut = false;
},
onForceLogout: (message) async {
await getIt<AuthService>().signOut();
channel?.unsubscribe();
channel = null;
if (!isLoggingOut) {
await runAppFlowy();
}
},
);
}
void _subscribeAuthState() {
final auth = Supabase.instance.client.auth;
authStateSubscription = auth.onAuthStateChange.listen((state) async {
switch (state.event) {
case AuthChangeEvent.signedIn:
_subscribeTablesChanges();
break;
case AuthChangeEvent.signedOut:
channel?.unsubscribe();
break;
case AuthChangeEvent.tokenRefreshed:
_subscribeTablesChanges();
break;
default:
break;
}
Log.info("Supabase auth state change: ${state.event}");
});
}
Future<void> _subscribeTablesChanges() async {
final result = await UserBackendService.getCurrentUserProfile();
result.fold((l) => null, (userProfile) {
Log.info("Start listening to table changes");
Log.info("Start listening supabase table changes");
// https://supabase.com/docs/guides/realtime/postgres-changes
final filters = [
final List<ChannelFilter> filters = [
"document",
"folder",
"database",
"database_row",
"w_database",
].map(
(name) => ChannelFilter(
event: 'INSERT',
]
.map(
(name) => ChannelFilter(
event: 'INSERT',
schema: 'public',
table: "af_collab_update_$name",
filter: 'uid=eq.${userProfile.id}',
),
)
.toList();
filters.add(
ChannelFilter(
event: 'UPDATE',
schema: 'public',
table: "af_collab_update_$name",
table: "af_user",
filter: 'uid=eq.${userProfile.id}',
),
);
const ops = RealtimeChannelConfig(ack: true);
channel?.unsubscribe();
channel = supabase.client.channel("table-db-changes", opts: ops);
for (final filter in filters) {
channel?.on(
@ -88,4 +112,10 @@ class SupbaseRealtimeService {
);
});
}
Future<void> dispose() async {
await _authStateListener.stop();
await authStateSubscription?.cancel();
await channel?.unsubscribe();
}
}

View File

@ -0,0 +1,68 @@
import 'dart:async';
import 'package:appflowy/core/notification/user_notification.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/auth.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'dart:typed_data';
import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-user/notification.pb.dart'
as user;
import 'package:appflowy_backend/rust_stream.dart';
class UserAuthStateListener {
void Function(String)? _onForceLogout;
void Function()? _didSignIn;
StreamSubscription<SubscribeObject>? _subscription;
UserNotificationParser? _userParser;
void start({
void Function(String)? onForceLogout,
void Function()? didSignIn,
}) {
_onForceLogout = onForceLogout;
_didSignIn = didSignIn;
_userParser = UserNotificationParser(
id: "auth_state_change_notification",
callback: _userNotificationCallback,
);
_subscription = RustStreamReceiver.listen((observable) {
_userParser?.parse(observable);
});
}
Future<void> stop() async {
_userParser = null;
await _subscription?.cancel();
_onForceLogout = null;
}
void _userNotificationCallback(
user.UserNotification ty,
Either<Uint8List, FlowyError> result,
) {
switch (ty) {
case user.UserNotification.UserAuthStateChanged:
result.fold(
(payload) {
final pb = AuthStateChangedPB.fromBuffer(payload);
switch (pb.state) {
case AuthStatePB.AuthStateSignIn:
_didSignIn?.call();
break;
case AuthStatePB.AuthStateForceSignOut:
_onForceLogout?.call("");
break;
default:
break;
}
},
(r) => Log.error(r),
);
break;
default:
break;
}
}
}

View File

@ -18,7 +18,6 @@ typedef AuthNotifyValue = Either<Unit, FlowyError>;
class UserListener {
StreamSubscription<SubscribeObject>? _subscription;
PublishNotifier<AuthNotifyValue>? _authNotifier = PublishNotifier();
PublishNotifier<UserProfileNotifyValue>? _profileNotifier = PublishNotifier();
UserNotificationParser? _userParser;
@ -28,17 +27,12 @@ class UserListener {
}) : _userProfile = userProfile;
void start({
void Function(AuthNotifyValue)? onAuthChanged,
void Function(UserProfileNotifyValue)? onProfileUpdated,
}) {
if (onProfileUpdated != null) {
_profileNotifier?.addPublishListener(onProfileUpdated);
}
if (onAuthChanged != null) {
_authNotifier?.addPublishListener(onAuthChanged);
}
_userParser = UserNotificationParser(
id: _userProfile.id.toString(),
callback: _userNotificationCallback,
@ -53,9 +47,6 @@ class UserListener {
await _subscription?.cancel();
_profileNotifier?.dispose();
_profileNotifier = null;
_authNotifier?.dispose();
_authNotifier = null;
}
void _userNotificationCallback(
@ -76,13 +67,9 @@ class UserListener {
}
}
typedef WorkspaceListNotifyValue = Either<List<WorkspacePB>, FlowyError>;
typedef WorkspaceSettingNotifyValue = Either<WorkspaceSettingPB, FlowyError>;
class UserWorkspaceListener {
PublishNotifier<AuthNotifyValue>? _authNotifier = PublishNotifier();
PublishNotifier<WorkspaceListNotifyValue>? _workspacesChangedNotifier =
PublishNotifier();
PublishNotifier<WorkspaceSettingNotifyValue>? _settingChangedNotifier =
PublishNotifier();
@ -93,18 +80,8 @@ class UserWorkspaceListener {
});
void start({
void Function(AuthNotifyValue)? onAuthChanged,
void Function(WorkspaceListNotifyValue)? onWorkspacesUpdated,
void Function(WorkspaceSettingNotifyValue)? onSettingUpdated,
}) {
if (onAuthChanged != null) {
_authNotifier?.addPublishListener(onAuthChanged);
}
if (onWorkspacesUpdated != null) {
_workspacesChangedNotifier?.addPublishListener(onWorkspacesUpdated);
}
if (onSettingUpdated != null) {
_settingChangedNotifier?.addPublishListener(onSettingUpdated);
}
@ -122,7 +99,6 @@ class UserWorkspaceListener {
Either<Uint8List, FlowyError> result,
) {
switch (ty) {
case FolderNotification.DidCreateWorkspace:
case FolderNotification.DidUpdateWorkspaceSetting:
result.fold(
(payload) => _settingChangedNotifier?.value =
@ -137,13 +113,8 @@ class UserWorkspaceListener {
Future<void> stop() async {
await _listener?.stop();
_workspacesChangedNotifier?.dispose();
_workspacesChangedNotifier = null;
_settingChangedNotifier?.dispose();
_settingChangedNotifier = null;
_authNotifier?.dispose();
_authNotifier = null;
}
}

View File

@ -1,24 +1,21 @@
import 'package:appflowy/user/application/user_listener.dart';
import 'package:flowy_infra/time/duration.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'
show WorkspaceSettingPB;
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dartz/dartz.dart';
part 'home_bloc.freezed.dart';
class HomeBloc extends Bloc<HomeEvent, HomeState> {
final UserWorkspaceListener _listener;
final UserWorkspaceListener _workspaceListener;
HomeBloc(
UserProfilePB user,
WorkspaceSettingPB workspaceSetting,
) : _listener = UserWorkspaceListener(userProfile: user),
) : _workspaceListener = UserWorkspaceListener(userProfile: user),
super(HomeState.initial(workspaceSetting)) {
on<HomeEvent>(
(event, emit) async {
@ -30,8 +27,7 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
}
});
_listener.start(
onAuthChanged: (result) => _authDidChanged(result),
_workspaceListener.start(
onSettingUpdated: (result) {
result.fold(
(setting) =>
@ -56,9 +52,6 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
),
);
},
unauthorized: (_Unauthorized value) {
emit(state.copyWith(unauthorized: true));
},
);
},
);
@ -66,17 +59,9 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
@override
Future<void> close() async {
await _listener.stop();
await _workspaceListener.stop();
return super.close();
}
void _authDidChanged(Either<Unit, FlowyError> errorOrNothing) {
errorOrNothing.fold((_) {}, (error) {
if (error.code == ErrorCode.UserUnauthorized.value) {
add(HomeEvent.unauthorized(error.msg));
}
});
}
}
enum MenuResizeType {
@ -102,7 +87,6 @@ class HomeEvent with _$HomeEvent {
const factory HomeEvent.didReceiveWorkspaceSetting(
WorkspaceSettingPB setting,
) = _DidReceiveWorkspaceSetting;
const factory HomeEvent.unauthorized(String msg) = _Unauthorized;
}
@freezed
@ -111,13 +95,11 @@ class HomeState with _$HomeState {
required bool isLoading,
required WorkspaceSettingPB workspaceSetting,
ViewPB? latestView,
required bool unauthorized,
}) = _HomeState;
factory HomeState.initial(WorkspaceSettingPB workspaceSetting) => HomeState(
isLoading: false,
workspaceSetting: workspaceSetting,
latestView: null,
unauthorized: false,
);
}

View File

@ -26,9 +26,6 @@ class MenuUserBloc extends Bloc<MenuUserEvent, MenuUserState> {
await event.when(
initial: () async {
_userListener.start(onProfileUpdated: _profileUpdated);
_userWorkspaceListener.start(
onWorkspacesUpdated: _workspaceListUpdated,
);
await _initUser();
},
fetchWorkspaces: () async {
@ -62,18 +59,16 @@ class MenuUserBloc extends Bloc<MenuUserEvent, MenuUserState> {
}
void _profileUpdated(Either<UserProfilePB, FlowyError> userProfileOrFailed) {
if (isClosed) {
return;
}
userProfileOrFailed.fold(
(newUserProfile) =>
add(MenuUserEvent.didReceiveUserProfile(newUserProfile)),
(newUserProfile) => add(
MenuUserEvent.didReceiveUserProfile(newUserProfile),
),
(err) => Log.error(err),
);
}
void _workspaceListUpdated(
Either<List<WorkspacePB>, FlowyError> workspacesOrFailed,
) {
// Do nothing by now
}
}
@freezed

View File

@ -21,9 +21,8 @@ class SettingsUserViewBloc extends Bloc<SettingsUserEvent, SettingsUserState> {
on<SettingsUserEvent>((event, emit) async {
await event.when(
initial: () async {
_loadUserProfile();
_userListener.start(onProfileUpdated: _profileUpdated);
await _initUser();
_loadHistoricalUsers();
},
didReceiveUserProfile: (UserProfilePB newUserProfile) {
emit(state.copyWith(userProfile: newUserProfile));
@ -68,26 +67,25 @@ class SettingsUserViewBloc extends Bloc<SettingsUserEvent, SettingsUserState> {
super.close();
}
Future<void> _initUser() async {
final result = await _userService.initUser();
result.fold((l) => null, (error) => Log.error(error));
}
void _loadUserProfile() {
UserBackendService.getCurrentUserProfile().then((result) {
if (isClosed) {
return;
}
Future<void> _loadHistoricalUsers() async {
final result = await UserBackendService.loadHistoricalUsers();
result.fold(
(historicalUsers) {
add(SettingsUserEvent.didLoadHistoricalUsers(historicalUsers));
},
(error) => Log.error(error),
);
result.fold(
(err) => Log.error(err),
(userProfile) => add(
SettingsUserEvent.didReceiveUserProfile(userProfile),
),
);
});
}
void _profileUpdated(Either<UserProfilePB, FlowyError> userProfileOrFailed) {
userProfileOrFailed.fold(
(newUserProfile) {
add(SettingsUserEvent.didReceiveUserProfile(newUserProfile));
_loadHistoricalUsers();
},
(err) => Log.error(err),
);

View File

@ -1,4 +1,3 @@
import 'package:appflowy/user/application/user_listener.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
@ -11,18 +10,11 @@ part 'welcome_bloc.freezed.dart';
class WelcomeBloc extends Bloc<WelcomeEvent, WelcomeState> {
final UserBackendService userService;
final UserWorkspaceListener userWorkspaceListener;
WelcomeBloc({required this.userService, required this.userWorkspaceListener})
: super(WelcomeState.initial()) {
WelcomeBloc({required this.userService}) : super(WelcomeState.initial()) {
on<WelcomeEvent>(
(event, emit) async {
await event.map(
initial: (e) async {
userWorkspaceListener.start(
onWorkspacesUpdated: (result) =>
add(WelcomeEvent.workspacesReveived(result)),
);
//
await _fetchWorkspaces(emit);
},
openWorkspace: (e) async {
@ -47,12 +39,6 @@ class WelcomeBloc extends Bloc<WelcomeEvent, WelcomeState> {
);
}
@override
Future<void> close() async {
await userWorkspaceListener.stop();
super.close();
}
Future<void> _fetchWorkspaces(Emitter<WelcomeState> emit) async {
final workspacesOrFailed = await userService.getWorkspaces();
emit(

View File

@ -61,16 +61,6 @@ class _HomeScreenState extends State<HomeScreen> {
child: Scaffold(
body: MultiBlocListener(
listeners: [
BlocListener<HomeBloc, HomeState>(
listenWhen: (p, c) => p.unauthorized != c.unauthorized,
listener: (context, state) {
if (state.unauthorized) {
Log.error(
"Push to login screen when user token was invalid",
);
}
},
),
BlocListener<HomeBloc, HomeState>(
listenWhen: (p, c) => p.latestView != c.latestView,
listener: (context, state) {

View File

@ -26,7 +26,7 @@ class SidebarUser extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider<MenuUserBloc>(
create: (context) => getIt<MenuUserBloc>(param1: user)
create: (context) => MenuUserBloc(user)
..add(
const MenuUserEvent.initial(),
),
@ -34,25 +34,23 @@ class SidebarUser extends StatelessWidget {
builder: (context, state) => Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildAvatar(context),
_buildAvatar(context, state),
const HSpace(10),
Expanded(
child: _buildUserName(context),
child: _buildUserName(context, state),
),
_buildSettingsButton(context),
_buildSettingsButton(context, state),
],
),
),
);
}
Widget _buildAvatar(BuildContext context) {
String iconUrl = context.read<MenuUserBloc>().state.userProfile.iconUrl;
Widget _buildAvatar(BuildContext context, MenuUserState state) {
String iconUrl = state.userProfile.iconUrl;
if (iconUrl.isEmpty) {
iconUrl = defaultUserAvatar;
final String name = _userName(
context.read<MenuUserBloc>().state.userProfile,
);
final String name = _userName(state.userProfile);
final Color color = ColorGenerator().generateColorFromString(name);
const initialsCount = 2;
// Taking the first letters of the name components and limiting to 2 elements
@ -92,10 +90,8 @@ class SidebarUser extends StatelessWidget {
);
}
Widget _buildUserName(BuildContext context) {
final String name = _userName(
context.read<MenuUserBloc>().state.userProfile,
);
Widget _buildUserName(BuildContext context, MenuUserState state) {
final String name = _userName(state.userProfile);
return FlowyText.medium(
name,
overflow: TextOverflow.ellipsis,
@ -103,8 +99,8 @@ class SidebarUser extends StatelessWidget {
);
}
Widget _buildSettingsButton(BuildContext context) {
final userProfile = context.read<MenuUserBloc>().state.userProfile;
Widget _buildSettingsButton(BuildContext context, MenuUserState state) {
final userProfile = state.userProfile;
return Tooltip(
message: LocaleKeys.settings_menu_open.tr(),
child: IconButton(

View File

@ -17,6 +17,41 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aead"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [
"crypto-common",
"generic-array",
]
[[package]]
name = "aes"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "aes-gcm"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "209b47e8954a928e1d72e86eca7000ebb6655fe1436d33eefc2201cad027e237"
dependencies = [
"aead",
"aes",
"cipher",
"ctr",
"ghash",
"subtle",
]
[[package]]
name = "ahash"
version = "0.7.6"
@ -98,14 +133,14 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.71"
version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "appflowy-integrate"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079"
dependencies = [
"anyhow",
"collab",
@ -218,324 +253,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "aws-config"
version = "0.55.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcdcf0d683fe9c23d32cf5b53c9918ea0a500375a9fb20109802552658e576c9"
dependencies = [
"aws-credential-types",
"aws-http",
"aws-sdk-sso",
"aws-sdk-sts",
"aws-smithy-async",
"aws-smithy-client",
"aws-smithy-http",
"aws-smithy-http-tower",
"aws-smithy-json",
"aws-smithy-types",
"aws-types",
"bytes",
"fastrand",
"hex",
"http",
"hyper",
"ring",
"time 0.3.22",
"tokio",
"tower",
"tracing",
"zeroize",
]
[[package]]
name = "aws-credential-types"
version = "0.55.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fcdb2f7acbc076ff5ad05e7864bdb191ca70a6fd07668dc3a1a8bcd051de5ae"
dependencies = [
"aws-smithy-async",
"aws-smithy-types",
"fastrand",
"tokio",
"tracing",
"zeroize",
]
[[package]]
name = "aws-endpoint"
version = "0.55.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cce1c41a6cfaa726adee9ebb9a56fcd2bbfd8be49fd8a04c5e20fd968330b04"
dependencies = [
"aws-smithy-http",
"aws-smithy-types",
"aws-types",
"http",
"regex",
"tracing",
]
[[package]]
name = "aws-http"
version = "0.55.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aadbc44e7a8f3e71c8b374e03ecd972869eb91dd2bc89ed018954a52ba84bc44"
dependencies = [
"aws-credential-types",
"aws-smithy-http",
"aws-smithy-types",
"aws-types",
"bytes",
"http",
"http-body",
"lazy_static",
"percent-encoding",
"pin-project-lite",
"tracing",
]
[[package]]
name = "aws-sdk-dynamodb"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67fb64867fe098cffee7e34352b01bbfa2beb3aa1b2ff0e0a7bf9ff293557852"
dependencies = [
"aws-credential-types",
"aws-endpoint",
"aws-http",
"aws-sig-auth",
"aws-smithy-async",
"aws-smithy-client",
"aws-smithy-http",
"aws-smithy-http-tower",
"aws-smithy-json",
"aws-smithy-types",
"aws-types",
"bytes",
"fastrand",
"http",
"regex",
"tokio-stream",
"tower",
"tracing",
]
[[package]]
name = "aws-sdk-sso"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8b812340d86d4a766b2ca73f740dfd47a97c2dff0c06c8517a16d88241957e4"
dependencies = [
"aws-credential-types",
"aws-endpoint",
"aws-http",
"aws-sig-auth",
"aws-smithy-async",
"aws-smithy-client",
"aws-smithy-http",
"aws-smithy-http-tower",
"aws-smithy-json",
"aws-smithy-types",
"aws-types",
"bytes",
"http",
"regex",
"tokio-stream",
"tower",
"tracing",
]
[[package]]
name = "aws-sdk-sts"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "265fac131fbfc188e5c3d96652ea90ecc676a934e3174eaaee523c6cec040b3b"
dependencies = [
"aws-credential-types",
"aws-endpoint",
"aws-http",
"aws-sig-auth",
"aws-smithy-async",
"aws-smithy-client",
"aws-smithy-http",
"aws-smithy-http-tower",
"aws-smithy-json",
"aws-smithy-query",
"aws-smithy-types",
"aws-smithy-xml",
"aws-types",
"bytes",
"http",
"regex",
"tower",
"tracing",
]
[[package]]
name = "aws-sig-auth"
version = "0.55.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b94acb10af0c879ecd5c7bdf51cda6679a0a4f4643ce630905a77673bfa3c61"
dependencies = [
"aws-credential-types",
"aws-sigv4",
"aws-smithy-http",
"aws-types",
"http",
"tracing",
]
[[package]]
name = "aws-sigv4"
version = "0.55.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d2ce6f507be68e968a33485ced670111d1cbad161ddbbab1e313c03d37d8f4c"
dependencies = [
"aws-smithy-http",
"form_urlencoded",
"hex",
"hmac",
"http",
"once_cell",
"percent-encoding",
"regex",
"sha2",
"time 0.3.22",
"tracing",
]
[[package]]
name = "aws-smithy-async"
version = "0.55.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13bda3996044c202d75b91afeb11a9afae9db9a721c6a7a427410018e286b880"
dependencies = [
"futures-util",
"pin-project-lite",
"tokio",
"tokio-stream",
]
[[package]]
name = "aws-smithy-client"
version = "0.55.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a86aa6e21e86c4252ad6a0e3e74da9617295d8d6e374d552be7d3059c41cedd"
dependencies = [
"aws-smithy-async",
"aws-smithy-http",
"aws-smithy-http-tower",
"aws-smithy-types",
"bytes",
"fastrand",
"http",
"http-body",
"hyper",
"hyper-rustls 0.23.2",
"lazy_static",
"pin-project-lite",
"rustls 0.20.8",
"tokio",
"tower",
"tracing",
]
[[package]]
name = "aws-smithy-http"
version = "0.55.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b3b693869133551f135e1f2c77cb0b8277d9e3e17feaf2213f735857c4f0d28"
dependencies = [
"aws-smithy-types",
"bytes",
"bytes-utils",
"futures-core",
"http",
"http-body",
"hyper",
"once_cell",
"percent-encoding",
"pin-project-lite",
"pin-utils",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "aws-smithy-http-tower"
version = "0.55.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae4f6c5798a247fac98a867698197d9ac22643596dc3777f0c76b91917616b9"
dependencies = [
"aws-smithy-http",
"aws-smithy-types",
"bytes",
"http",
"http-body",
"pin-project-lite",
"tower",
"tracing",
]
[[package]]
name = "aws-smithy-json"
version = "0.55.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23f9f42fbfa96d095194a632fbac19f60077748eba536eb0b9fecc28659807f8"
dependencies = [
"aws-smithy-types",
]
[[package]]
name = "aws-smithy-query"
version = "0.55.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98819eb0b04020a1c791903533b638534ae6c12e2aceda3e6e6fba015608d51d"
dependencies = [
"aws-smithy-types",
"urlencoding",
]
[[package]]
name = "aws-smithy-types"
version = "0.55.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16a3d0bf4f324f4ef9793b86a1701d9700fbcdbd12a846da45eed104c634c6e8"
dependencies = [
"base64-simd",
"itoa 1.0.6",
"num-integer",
"ryu",
"time 0.3.22",
]
[[package]]
name = "aws-smithy-xml"
version = "0.55.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1b9d12875731bd07e767be7baad95700c3137b56730ec9ddeedb52a5e5ca63b"
dependencies = [
"xmlparser",
]
[[package]]
name = "aws-types"
version = "0.55.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dd209616cc8d7bfb82f87811a5c655dc97537f592689b18743bddf5dc5c4829"
dependencies = [
"aws-credential-types",
"aws-smithy-async",
"aws-smithy-client",
"aws-smithy-http",
"aws-smithy-types",
"http",
"rustc_version",
"tracing",
]
[[package]]
name = "backtrace"
version = "0.3.67"
@ -563,16 +280,6 @@ version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
[[package]]
name = "base64-simd"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195"
dependencies = [
"outref",
"vsimd",
]
[[package]]
name = "bincode"
version = "1.3.3"
@ -776,16 +483,6 @@ dependencies = [
"serde",
]
[[package]]
name = "bytes-utils"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e47d3a8076e283f3acd27400535992edb3ba4b5bb72f8891ad8fbe7932a7d4b9"
dependencies = [
"bytes",
"either",
]
[[package]]
name = "bzip2-sys"
version = "0.1.11+1.0.8"
@ -951,6 +648,16 @@ dependencies = [
"phf_codegen 0.11.2",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "clang-sys"
version = "1.6.1"
@ -1021,7 +728,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079"
dependencies = [
"anyhow",
"bytes",
@ -1039,7 +746,7 @@ dependencies = [
[[package]]
name = "collab-client-ws"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079"
dependencies = [
"bytes",
"collab-sync",
@ -1057,7 +764,7 @@ dependencies = [
[[package]]
name = "collab-database"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079"
dependencies = [
"anyhow",
"async-trait",
@ -1081,10 +788,18 @@ dependencies = [
"uuid",
]
[[package]]
name = "collab-define"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079"
dependencies = [
"uuid",
]
[[package]]
name = "collab-derive"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079"
dependencies = [
"proc-macro2",
"quote",
@ -1096,7 +811,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079"
dependencies = [
"anyhow",
"collab",
@ -1115,7 +830,7 @@ dependencies = [
[[package]]
name = "collab-folder"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079"
dependencies = [
"anyhow",
"chrono",
@ -1135,7 +850,7 @@ dependencies = [
[[package]]
name = "collab-persistence"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079"
dependencies = [
"bincode",
"chrono",
@ -1155,15 +870,13 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079"
dependencies = [
"anyhow",
"async-trait",
"aws-config",
"aws-credential-types",
"aws-sdk-dynamodb",
"collab",
"collab-client-ws",
"collab-define",
"collab-persistence",
"collab-sync",
"futures-util",
@ -1178,6 +891,7 @@ dependencies = [
"tokio-retry",
"tokio-stream",
"tracing",
"uuid",
"y-sync",
"yrs",
]
@ -1185,7 +899,7 @@ dependencies = [
[[package]]
name = "collab-sync"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079"
dependencies = [
"bytes",
"collab",
@ -1204,6 +918,21 @@ dependencies = [
"yrs",
]
[[package]]
name = "collab-user"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079"
dependencies = [
"anyhow",
"collab",
"parking_lot 0.12.1",
"serde",
"serde_json",
"tokio",
"tokio-stream",
"tracing",
]
[[package]]
name = "color_quant"
version = "1.1.0"
@ -1361,6 +1090,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"rand_core 0.6.4",
"typenum",
]
@ -1422,6 +1152,15 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "ctr"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
dependencies = [
"cipher",
]
[[package]]
name = "darling"
version = "0.20.1"
@ -1951,6 +1690,19 @@ dependencies = [
"uuid",
]
[[package]]
name = "flowy-encrypt"
version = "0.1.0"
dependencies = [
"aes-gcm",
"anyhow",
"base64 0.21.2",
"hmac",
"pbkdf2",
"rand 0.8.5",
"sha2",
]
[[package]]
name = "flowy-error"
version = "0.1.0"
@ -2050,6 +1802,7 @@ dependencies = [
"config",
"flowy-database-deps",
"flowy-document-deps",
"flowy-encrypt",
"flowy-error",
"flowy-folder-deps",
"flowy-server-config",
@ -2122,11 +1875,13 @@ dependencies = [
"collab",
"collab-document",
"collab-folder",
"collab-user",
"diesel",
"diesel_derives",
"fancy-regex 0.11.0",
"flowy-codegen",
"flowy-derive",
"flowy-encrypt",
"flowy-error",
"flowy-notification",
"flowy-server-config",
@ -2156,11 +1911,13 @@ version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"collab-define",
"flowy-error",
"lib-infra",
"serde",
"serde_json",
"serde_repr",
"tokio",
"uuid",
]
@ -2461,6 +2218,16 @@ dependencies = [
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
name = "ghash"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40"
dependencies = [
"opaque-debug",
"polyval",
]
[[package]]
name = "gimli"
version = "0.27.3"
@ -2813,21 +2580,6 @@ dependencies = [
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c"
dependencies = [
"http",
"hyper",
"log",
"rustls 0.20.8",
"rustls-native-certs",
"tokio",
"tokio-rustls 0.23.4",
]
[[package]]
name = "hyper-rustls"
version = "0.24.0"
@ -2836,9 +2588,9 @@ checksum = "0646026eb1b3eea4cd9ba47912ea5ce9cc07713d105b1a14698f4e6433d348b7"
dependencies = [
"http",
"hyper",
"rustls 0.21.2",
"rustls",
"tokio",
"tokio-rustls 0.24.1",
"tokio-rustls",
]
[[package]]
@ -2963,6 +2715,15 @@ dependencies = [
"cfb",
]
[[package]]
name = "inout"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
dependencies = [
"generic-array",
]
[[package]]
name = "instant"
version = "0.1.12"
@ -3655,6 +3416,12 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "opaque-debug"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "open"
version = "3.2.0"
@ -3719,12 +3486,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "outref"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a"
[[package]]
name = "overload"
version = "0.1.1"
@ -3819,6 +3580,16 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
[[package]]
name = "pbkdf2"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [
"digest",
"hmac",
]
[[package]]
name = "peeking_take_while"
version = "0.1.2"
@ -4087,6 +3858,18 @@ dependencies = [
"miniz_oxide 0.7.1",
]
[[package]]
name = "polyval"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb"
dependencies = [
"cfg-if",
"cpufeatures",
"opaque-debug",
"universal-hash",
]
[[package]]
name = "postgres-protocol"
version = "0.6.5"
@ -4547,7 +4330,7 @@ dependencies = [
"http",
"http-body",
"hyper",
"hyper-rustls 0.24.0",
"hyper-rustls",
"hyper-tls",
"ipnet",
"js-sys",
@ -4557,14 +4340,14 @@ dependencies = [
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls 0.21.2",
"rustls",
"rustls-pemfile",
"serde",
"serde_json",
"serde_urlencoded",
"tokio",
"tokio-native-tls",
"tokio-rustls 0.24.1",
"tokio-rustls",
"tower-service",
"url",
"wasm-bindgen",
@ -4708,18 +4491,6 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "rustls"
version = "0.20.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f"
dependencies = [
"log",
"ring",
"sct",
"webpki",
]
[[package]]
name = "rustls"
version = "0.21.2"
@ -4732,18 +4503,6 @@ dependencies = [
"sct",
]
[[package]]
name = "rustls-native-certs"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
dependencies = [
"openssl-probe",
"rustls-pemfile",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.2"
@ -5849,24 +5608,13 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
dependencies = [
"rustls 0.20.8",
"tokio",
"webpki",
]
[[package]]
name = "tokio-rustls"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
dependencies = [
"rustls 0.21.2",
"rustls",
"tokio",
]
@ -5951,28 +5699,6 @@ dependencies = [
"winnow",
]
[[package]]
name = "tower"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
"futures-core",
"futures-util",
"pin-project",
"pin-project-lite",
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
[[package]]
name = "tower-service"
version = "0.3.2"
@ -6240,6 +5966,16 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "universal-hash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [
"crypto-common",
"subtle",
]
[[package]]
name = "untrusted"
version = "0.7.1"
@ -6258,12 +5994,6 @@ dependencies = [
"serde",
]
[[package]]
name = "urlencoding"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
[[package]]
name = "utf-8"
version = "0.7.6"
@ -6325,12 +6055,6 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "vsimd"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
[[package]]
name = "vswhom"
version = "0.1.0"
@ -6923,12 +6647,6 @@ dependencies = [
"libc",
]
[[package]]
name = "xmlparser"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd"
[[package]]
name = "y-sync"
version = "0.3.1"

View File

@ -4,18 +4,11 @@ import { UserNotificationParser } from './parser';
import { Ok, Result } from 'ts-results';
declare type OnUserProfileUpdate = (result: Result<UserProfilePB, FlowyError>) => void;
declare type OnUserSignIn = (result: Result<UserProfilePB, FlowyError>) => void;
export class UserNotificationListener extends AFNotificationObserver<UserNotification> {
onProfileUpdate?: OnUserProfileUpdate;
onUserSignIn?: OnUserSignIn;
constructor(params: {
userId?: string;
onUserSignIn?: OnUserSignIn;
onProfileUpdate?: OnUserProfileUpdate;
onError?: OnNotificationError;
}) {
constructor(params: { userId?: string; onProfileUpdate?: OnUserProfileUpdate; onError?: OnNotificationError }) {
const parser = new UserNotificationParser({
callback: (notification, result) => {
switch (notification) {
@ -26,13 +19,6 @@ export class UserNotificationListener extends AFNotificationObserver<UserNotific
this.onProfileUpdate?.(result);
}
break;
case UserNotification.DidUserSignIn:
if (result.ok) {
this.onUserSignIn?.(Ok(UserProfilePB.deserializeBinary(result.val)));
} else {
this.onUserSignIn?.(result);
}
break;
default:
break;
}
@ -42,6 +28,5 @@ export class UserNotificationListener extends AFNotificationObserver<UserNotific
});
super(parser);
this.onProfileUpdate = params.onProfileUpdate;
this.onUserSignIn = params.onUserSignIn;
}
}

View File

@ -1791,6 +1791,7 @@ dependencies = [
"serde",
"serde_json",
"serde_repr",
"tokio",
"uuid",
]

View File

@ -4,8 +4,7 @@ use std::sync::{Arc, Weak};
use appflowy_integrate::collab_builder::{CollabStorageProvider, CollabStorageType};
use appflowy_integrate::{CollabObject, CollabType, RemoteCollabStorage, YrsDocAction};
use parking_lot::{Mutex, RwLock};
use serde_json::Value;
use parking_lot::RwLock;
use serde_repr::*;
use flowy_database_deps::cloud::*;
@ -64,11 +63,12 @@ impl Display for ServerProviderType {
pub struct AppFlowyServerProvider {
config: AppFlowyCoreConfig,
provider_type: RwLock<ServerProviderType>,
device_id: Mutex<String>,
device_id: Arc<RwLock<String>>,
providers: RwLock<HashMap<ServerProviderType, Arc<dyn AppFlowyServer>>>,
enable_sync: RwLock<bool>,
encryption: RwLock<Arc<dyn AppFlowyEncryption>>,
store_preferences: Weak<StorePreferences>,
cache_user_service: RwLock<HashMap<ServerProviderType, Arc<dyn UserService>>>,
}
impl AppFlowyServerProvider {
@ -86,11 +86,12 @@ impl AppFlowyServerProvider {
enable_sync: RwLock::new(true),
encryption: RwLock::new(Arc::new(encryption)),
store_preferences,
cache_user_service: Default::default(),
}
}
pub fn set_sync_device(&self, device_id: &str) {
*self.device_id.lock() = device_id.to_string();
*self.device_id.write() = device_id.to_string();
}
pub fn provider_type(&self) -> ServerProviderType {
@ -134,11 +135,11 @@ impl AppFlowyServerProvider {
Ok::<Arc<dyn AppFlowyServer>, FlowyError>(Arc::new(SupabaseServer::new(
config,
*self.enable_sync.read(),
self.device_id.clone(),
encryption,
)))
},
}?;
server.set_sync_device_id(&self.device_id.lock());
self
.providers
@ -146,13 +147,6 @@ impl AppFlowyServerProvider {
.insert(provider_type.clone(), server.clone());
Ok(server)
}
pub fn handle_realtime_event(&self, json: Value) {
let provider_type = self.provider_type.read().clone();
if let Some(server) = self.providers.read().get(&provider_type) {
server.handle_realtime_event(json);
}
}
}
impl UserCloudServiceProvider for AppFlowyServerProvider {
@ -195,17 +189,27 @@ impl UserCloudServiceProvider for AppFlowyServerProvider {
}
fn set_device_id(&self, device_id: &str) {
*self.device_id.lock() = device_id.to_string();
*self.device_id.write() = device_id.to_string();
}
/// Returns the [UserService] base on the current [ServerProviderType].
/// Creates a new [AppFlowyServer] if it doesn't exist.
fn get_user_service(&self) -> Result<Arc<dyn UserService>, FlowyError> {
Ok(
self
.get_provider(&self.provider_type.read())?
.user_service(),
)
if let Some(user_service) = self
.cache_user_service
.read()
.get(&self.provider_type.read())
{
return Ok(user_service.clone());
}
let provider_type = self.provider_type.read().clone();
let user_service = self.get_provider(&provider_type)?.user_service();
self
.cache_user_service
.write()
.insert(provider_type, user_service.clone());
Ok(user_service)
}
fn service_name(&self) -> String {

View File

@ -11,7 +11,6 @@ use std::{
};
use appflowy_integrate::collab_builder::{AppFlowyCollabBuilder, CollabStorageType};
use serde_json::Value;
use tokio::sync::RwLock;
use flowy_database2::DatabaseManager;
@ -268,12 +267,12 @@ fn mk_user_session(
collab_builder: Weak<AppFlowyCollabBuilder>,
) -> Arc<UserManager> {
let user_config = UserSessionConfig::new(&config.name, &config.storage_path);
Arc::new(UserManager::new(
UserManager::new(
user_config,
user_cloud_service_provider,
storage_preference.clone(),
collab_builder,
))
)
}
struct UserStatusCallbackImpl {
@ -439,10 +438,6 @@ impl UserStatusCallback for UserStatusCallbackImpl {
fn did_update_network(&self, reachable: bool) {
self.collab_builder.update_network(reachable);
}
fn receive_realtime_event(&self, json: Value) {
self.server_provider.handle_realtime_event(json);
}
}
impl From<ServerProviderType> for CollabStorageType {

View File

@ -2,7 +2,6 @@ use std::sync::Arc;
use collab_plugins::cloud_storage::{CollabObject, RemoteCollabStorage};
use parking_lot::RwLock;
use serde_json::Value;
use flowy_database_deps::cloud::DatabaseCloudService;
use flowy_document_deps::cloud::DocumentCloudService;
@ -36,13 +35,11 @@ where
pub trait AppFlowyServer: Send + Sync + 'static {
fn set_enable_sync(&self, _enable: bool) {}
fn set_sync_device_id(&self, _device_id: &str) {}
fn user_service(&self) -> Arc<dyn UserService>;
fn folder_service(&self) -> Arc<dyn FolderCloudService>;
fn database_service(&self) -> Arc<dyn DatabaseCloudService>;
fn document_service(&self) -> Arc<dyn DocumentCloudService>;
fn collab_storage(&self, collab_object: &CollabObject) -> Option<Arc<dyn RemoteCollabStorage>>;
fn handle_realtime_event(&self, _json: Value) {}
}
pub struct EncryptionImpl {

View File

@ -16,7 +16,8 @@ use flowy_database_deps::cloud::{CollabObjectUpdate, CollabObjectUpdateByOid};
use lib_infra::util::md5;
use crate::supabase::api::util::{
ExtendedResponse, InsertParamsBuilder, SupabaseBinaryColumnDecoder, SupabaseBinaryColumnEncoder,
BinaryColumnDecoder, ExtendedResponse, InsertParamsBuilder, SupabaseBinaryColumnDecoder,
SupabaseBinaryColumnEncoder,
};
use crate::supabase::api::PostgresWrapper;
use crate::supabase::define::*;
@ -220,7 +221,8 @@ fn parser_snapshot(
.and_then(|value| value.as_str()),
) {
(Some(encrypt), Some(value)) => {
SupabaseBinaryColumnDecoder::decode(value, encrypt as i32, secret).ok()
SupabaseBinaryColumnDecoder::decode::<_, BinaryColumnDecoder>(value, encrypt as i32, secret)
.ok()
},
_ => None,
}?;
@ -364,7 +366,11 @@ fn parser_update_from_json(
json.get("value").and_then(|value| value.as_str()),
) {
(Some(encrypt), Some(value)) => {
match SupabaseBinaryColumnDecoder::decode(value, encrypt as i32, encryption_secret) {
match SupabaseBinaryColumnDecoder::decode::<_, BinaryColumnDecoder>(
value,
encrypt as i32,
encryption_secret,
) {
Ok(value) => Some(value),
Err(err) => {
tracing::error!("Decode value column failed: {:?}", err);

View File

@ -1,8 +1,10 @@
use std::str::FromStr;
use std::sync::Arc;
use std::sync::{Arc, Weak};
use anyhow::Error;
use collab_plugins::cloud_storage::CollabObject;
use parking_lot::RwLock;
use serde_json::Value;
use tokio::sync::oneshot::channel;
use uuid::Uuid;
@ -13,20 +15,34 @@ use lib_infra::box_any::BoxAny;
use lib_infra::future::FutureResult;
use crate::supabase::api::request::FetchObjectUpdateAction;
use crate::supabase::api::util::{ExtendedResponse, InsertParamsBuilder};
use crate::supabase::api::util::{
ExtendedResponse, InsertParamsBuilder, RealtimeBinaryColumnDecoder, SupabaseBinaryColumnDecoder,
};
use crate::supabase::api::{send_update, PostgresWrapper, SupabaseServerService};
use crate::supabase::define::*;
use crate::supabase::entities::GetUserProfileParams;
use crate::supabase::entities::UidResponse;
use crate::supabase::entities::UserProfileResponse;
use crate::supabase::entities::{GetUserProfileParams, RealtimeUserEvent};
use crate::supabase::entities::{RealtimeCollabUpdateEvent, RealtimeEvent, UidResponse};
use crate::supabase::CollabUpdateSenderByOid;
use crate::AppFlowyEncryption;
pub struct SupabaseUserServiceImpl<T> {
server: T,
realtime_event_handlers: Vec<Box<dyn RealtimeEventHandler>>,
user_update_tx: Option<UserUpdateSender>,
}
impl<T> SupabaseUserServiceImpl<T> {
pub fn new(server: T) -> Self {
Self { server }
pub fn new(
server: T,
realtime_event_handlers: Vec<Box<dyn RealtimeEventHandler>>,
user_update_tx: Option<UserUpdateSender>,
) -> Self {
Self {
server,
realtime_event_handlers,
user_update_tx,
}
}
}
@ -67,7 +83,11 @@ where
}
// Query the user profile and workspaces
tracing::debug!("user uuid: {}", params.uuid);
tracing::debug!(
"user uuid: {}, device_id: {}",
params.uuid,
params.device_id
);
let user_profile =
get_user_profile(postgrest.clone(), GetUserProfileParams::Uuid(params.uuid))
.await?
@ -226,6 +246,26 @@ where
FutureResult::new(async { rx.await? })
}
fn receive_realtime_event(&self, json: Value) {
match serde_json::from_value::<RealtimeEvent>(json) {
Ok(event) => {
tracing::trace!("Realtime event: {}", event);
for handler in &self.realtime_event_handlers {
if event.table.as_str().starts_with(handler.table_name()) {
handler.handler_event(&event);
}
}
},
Err(e) => {
tracing::error!("parser realtime event error: {}", e);
},
}
}
fn subscribe_user_update(&self) -> Option<UserUpdateReceiver> {
self.user_update_tx.as_ref().map(|tx| tx.subscribe())
}
fn create_collab_object(
&self,
collab_object: &CollabObject,
@ -384,3 +424,95 @@ async fn check_user(
}
Ok(())
}
pub trait RealtimeEventHandler: Send + Sync + 'static {
fn table_name(&self) -> &str;
fn handler_event(&self, event: &RealtimeEvent);
}
pub struct RealtimeUserHandler(pub UserUpdateSender);
impl RealtimeEventHandler for RealtimeUserHandler {
fn table_name(&self) -> &str {
"af_user"
}
fn handler_event(&self, event: &RealtimeEvent) {
if let Ok(user_event) = serde_json::from_value::<RealtimeUserEvent>(event.new.clone()) {
let _ = self.0.send(UserUpdate {
uid: user_event.uid,
name: user_event.name,
email: user_event.email,
encryption_sign: user_event.encryption_sign,
});
}
}
}
pub struct RealtimeCollabUpdateHandler {
sender_by_oid: Weak<CollabUpdateSenderByOid>,
device_id: Arc<RwLock<String>>,
encryption: Weak<dyn AppFlowyEncryption>,
}
impl RealtimeCollabUpdateHandler {
pub fn new(
sender_by_oid: Weak<CollabUpdateSenderByOid>,
device_id: Arc<RwLock<String>>,
encryption: Weak<dyn AppFlowyEncryption>,
) -> Self {
Self {
sender_by_oid,
device_id,
encryption,
}
}
}
impl RealtimeEventHandler for RealtimeCollabUpdateHandler {
fn table_name(&self) -> &str {
"af_collab_update"
}
fn handler_event(&self, event: &RealtimeEvent) {
if let Ok(collab_update) =
serde_json::from_value::<RealtimeCollabUpdateEvent>(event.new.clone())
{
if let Some(sender_by_oid) = self.sender_by_oid.upgrade() {
if let Some(sender) = sender_by_oid.read().get(collab_update.oid.as_str()) {
tracing::trace!(
"current device: {}, event device: {}",
self.device_id.read(),
collab_update.did.as_str()
);
if *self.device_id.read() != collab_update.did.as_str() {
let encryption_secret = self
.encryption
.upgrade()
.and_then(|encryption| encryption.get_secret());
tracing::trace!(
"Parse collab update with len: {}, encrypt: {}",
collab_update.value.len(),
collab_update.encrypt,
);
match SupabaseBinaryColumnDecoder::decode::<_, RealtimeBinaryColumnDecoder>(
collab_update.value.as_str(),
collab_update.encrypt,
&encryption_secret,
) {
Ok(value) => {
if let Err(e) = sender.send(value) {
tracing::debug!("send realtime update error: {}", e);
}
},
Err(err) => {
tracing::error!("decode collab update error: {}", err);
},
}
}
}
}
}
}
}

View File

@ -171,7 +171,7 @@ impl SupabaseBinaryColumnDecoder {
/// # Returns
/// Returns an `Option` containing the decoded binary data if decoding is successful.
/// Otherwise, returns `None`.
pub fn decode<T: AsRef<str>>(
pub fn decode<T: AsRef<str>, D: HexDecoder>(
value: T,
encrypt: i32,
encryption_secret: &Option<String>,
@ -182,7 +182,7 @@ impl SupabaseBinaryColumnDecoder {
.ok_or(anyhow::anyhow!("Value is not start with: \\x",))?;
if encrypt == 0 {
let bytes = hex::decode(s)?;
let bytes = D::decode(s)?;
Ok(bytes)
} else {
match encryption_secret {
@ -190,7 +190,7 @@ impl SupabaseBinaryColumnDecoder {
"encryption_secret is None, but encrypt is 1"
)),
Some(encryption_secret) => {
let encrypt_data = hex::decode(s)?;
let encrypt_data = D::decode(s)?;
decrypt_bytes(encrypt_data, encryption_secret)
},
}
@ -198,15 +198,24 @@ impl SupabaseBinaryColumnDecoder {
}
}
/// A decoder specifically tailored for realtime event binary columns in Supabase.
///
pub struct SupabaseRealtimeEventBinaryColumnDecoder;
pub trait HexDecoder {
fn decode<T: AsRef<[u8]>>(data: T) -> Result<Vec<u8>, Error>;
}
impl SupabaseRealtimeEventBinaryColumnDecoder {
/// The realtime event binary column string is encoded twice. So it needs to be decoded twice.
pub fn decode<T: AsRef<str>>(value: T) -> Option<Vec<u8>> {
let s = value.as_ref().strip_prefix("\\x")?;
let bytes = hex::decode(s).ok()?;
hex::decode(bytes).ok()
pub struct RealtimeBinaryColumnDecoder;
impl HexDecoder for RealtimeBinaryColumnDecoder {
fn decode<T: AsRef<[u8]>>(data: T) -> Result<Vec<u8>, Error> {
// The realtime event binary column string is encoded twice. So it needs to be decoded twice.
let bytes = hex::decode(data)?;
let bytes = hex::decode(bytes)?;
Ok(bytes)
}
}
pub struct BinaryColumnDecoder;
impl HexDecoder for BinaryColumnDecoder {
fn decode<T: AsRef<[u8]>>(data: T) -> Result<Vec<u8>, Error> {
let bytes = hex::decode(data)?;
Ok(bytes)
}
}

View File

@ -1,11 +1,10 @@
use std::fmt;
use std::fmt::Display;
use serde::de::{Error, Visitor};
use serde::{Deserialize, Deserializer};
use serde::Deserialize;
use serde_json::Value;
use uuid::Uuid;
use crate::supabase::api::util::SupabaseRealtimeEventBinaryColumnDecoder;
use crate::util::deserialize_null_or_default;
pub enum GetUserProfileParams {
@ -40,16 +39,14 @@ pub(crate) struct UidResponse {
}
#[derive(Debug, Deserialize)]
pub struct RealtimeCollabUpdateEvent {
pub struct RealtimeEvent {
pub schema: String,
pub table: String,
#[serde(rename = "eventType")]
pub event_type: String,
#[serde(rename = "new")]
pub payload: RealtimeCollabUpdate,
pub new: Value,
}
impl Display for RealtimeCollabUpdateEvent {
impl Display for RealtimeEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
@ -60,43 +57,23 @@ impl Display for RealtimeCollabUpdateEvent {
}
#[derive(Debug, Deserialize)]
pub struct RealtimeCollabUpdate {
pub struct RealtimeCollabUpdateEvent {
pub oid: String,
pub uid: i64,
pub key: i64,
pub did: String,
#[serde(deserialize_with = "deserialize_value")]
pub value: Vec<u8>,
pub value: String,
#[serde(default)]
pub encrypt: i32,
}
pub fn deserialize_value<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
struct ValueVisitor();
impl<'de> Visitor<'de> for ValueVisitor {
type Value = Vec<u8>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("Expect NodeBody")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error,
{
Ok(SupabaseRealtimeEventBinaryColumnDecoder::decode(v).unwrap_or_default())
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: Error,
{
Ok(SupabaseRealtimeEventBinaryColumnDecoder::decode(v).unwrap_or_default())
}
}
deserializer.deserialize_any(ValueVisitor())
#[derive(Debug, Deserialize)]
pub struct RealtimeUserEvent {
pub uid: i64,
#[serde(deserialize_with = "deserialize_null_or_default")]
pub name: String,
#[serde(deserialize_with = "deserialize_null_or_default")]
pub email: String,
#[serde(deserialize_with = "deserialize_null_or_default")]
pub encryption_sign: String,
}

View File

@ -2,22 +2,19 @@ use std::collections::HashMap;
use std::sync::{Arc, Weak};
use collab_plugins::cloud_storage::{CollabObject, RemoteCollabStorage, RemoteUpdateSender};
use parking_lot::{Mutex, RwLock};
use serde_json::Value;
use parking_lot::RwLock;
use flowy_database_deps::cloud::DatabaseCloudService;
use flowy_document_deps::cloud::DocumentCloudService;
use flowy_encrypt::decrypt_bytes;
use flowy_folder_deps::cloud::FolderCloudService;
use flowy_server_config::supabase_config::SupabaseConfiguration;
use flowy_user_deps::cloud::UserService;
use crate::supabase::api::{
RESTfulPostgresServer, SupabaseCollabStorageImpl, SupabaseDatabaseServiceImpl,
SupabaseDocumentServiceImpl, SupabaseFolderServiceImpl, SupabaseServerServiceImpl,
SupabaseUserServiceImpl,
RESTfulPostgresServer, RealtimeCollabUpdateHandler, RealtimeEventHandler, RealtimeUserHandler,
SupabaseCollabStorageImpl, SupabaseDatabaseServiceImpl, SupabaseDocumentServiceImpl,
SupabaseFolderServiceImpl, SupabaseServerServiceImpl, SupabaseUserServiceImpl,
};
use crate::supabase::entities::RealtimeCollabUpdateEvent;
use crate::{AppFlowyEncryption, AppFlowyServer};
/// https://www.pgbouncer.org/features.html
@ -53,14 +50,16 @@ impl PgPoolMode {
matches!(self, PgPoolMode::Session)
}
}
pub type CollabUpdateSenderByOid = RwLock<HashMap<String, RemoteUpdateSender>>;
/// Supabase server is used to provide the implementation of the [AppFlowyServer] trait.
/// It contains the configuration of the supabase server and the postgres server.
pub struct SupabaseServer {
#[allow(dead_code)]
config: SupabaseConfiguration,
/// did represents as the device id is used to identify the device that is currently using the app.
did: Mutex<String>,
update_tx: RwLock<HashMap<String, RemoteUpdateSender>>,
device_id: Arc<RwLock<String>>,
collab_update_sender: Arc<CollabUpdateSenderByOid>,
restful_postgres: Arc<RwLock<Option<Arc<RESTfulPostgresServer>>>>,
encryption: Weak<dyn AppFlowyEncryption>,
}
@ -69,9 +68,10 @@ impl SupabaseServer {
pub fn new(
config: SupabaseConfiguration,
enable_sync: bool,
device_id: Arc<RwLock<String>>,
encryption: Weak<dyn AppFlowyEncryption>,
) -> Self {
let update_tx = RwLock::new(HashMap::new());
let collab_update_sender = Default::default();
let restful_postgres = if enable_sync {
Some(Arc::new(RESTfulPostgresServer::new(
config.clone(),
@ -82,8 +82,8 @@ impl SupabaseServer {
};
Self {
config,
did: Default::default(),
update_tx,
device_id,
collab_update_sender,
restful_postgres: Arc::new(RwLock::new(restful_postgres)),
encryption,
}
@ -108,14 +108,25 @@ impl AppFlowyServer for SupabaseServer {
self.set_enable_sync(enable);
}
fn set_sync_device_id(&self, device_id: &str) {
*self.did.lock() = device_id.to_string();
}
fn user_service(&self) -> Arc<dyn UserService> {
Arc::new(SupabaseUserServiceImpl::new(SupabaseServerServiceImpl(
self.restful_postgres.clone(),
)))
// handle the realtime collab update event.
let (user_update_tx, _) = tokio::sync::broadcast::channel(100);
let collab_update_handler = Box::new(RealtimeCollabUpdateHandler::new(
Arc::downgrade(&self.collab_update_sender),
self.device_id.clone(),
self.encryption.clone(),
));
// handle the realtime user event.
let user_handler = Box::new(RealtimeUserHandler(user_update_tx.clone()));
let handlers: Vec<Box<dyn RealtimeEventHandler>> = vec![collab_update_handler, user_handler];
Arc::new(SupabaseUserServiceImpl::new(
SupabaseServerServiceImpl(self.restful_postgres.clone()),
handlers,
Some(user_update_tx),
))
}
fn folder_service(&self) -> Arc<dyn FolderCloudService> {
@ -139,53 +150,14 @@ impl AppFlowyServer for SupabaseServer {
fn collab_storage(&self, collab_object: &CollabObject) -> Option<Arc<dyn RemoteCollabStorage>> {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
self
.update_tx
.collab_update_sender
.write()
.insert(collab_object.object_id.clone(), tx);
Some(Arc::new(SupabaseCollabStorageImpl::new(
SupabaseServerServiceImpl(self.restful_postgres.clone()),
Some(rx),
self.encryption.clone(),
)))
}
fn handle_realtime_event(&self, json: Value) {
match serde_json::from_value::<RealtimeCollabUpdateEvent>(json) {
Ok(event) => {
if let Some(tx) = self.update_tx.read().get(event.payload.oid.as_str()) {
tracing::trace!(
"current device: {}, event device: {}",
self.did.lock().as_str(),
event.payload.did.as_str()
);
if self.did.lock().as_str() != event.payload.did.as_str() {
tracing::trace!("Did receive realtime event: {}", event);
let value = if event.payload.encrypt == 1 {
match self
.encryption
.upgrade()
.and_then(|encryption| encryption.get_secret())
{
None => vec![],
Some(secret) => decrypt_bytes(event.payload.value, &secret).unwrap_or_default(),
}
} else {
event.payload.value
};
if !value.is_empty() {
tracing::trace!("Parse payload with len: {} success", value.len());
if let Err(e) = tx.send(value) {
tracing::trace!("send realtime update error: {}", e);
}
}
}
}
},
Err(e) => {
tracing::error!("parser realtime event error: {}", e);
},
}
}
}

View File

@ -48,7 +48,7 @@ pub fn database_service() -> Arc<dyn DatabaseCloudService> {
pub fn user_auth_service() -> Arc<dyn UserService> {
let (server, _encryption_impl) = appflowy_server(None);
Arc::new(SupabaseUserServiceImpl::new(server))
Arc::new(SupabaseUserServiceImpl::new(server, vec![], None))
}
pub fn folder_service() -> Arc<dyn FolderCloudService> {

View File

@ -112,7 +112,7 @@ pub fn database_service() -> Arc<dyn DatabaseCloudService> {
pub fn user_auth_service() -> Arc<dyn UserService> {
let (server, _encryption_impl) = appflowy_server(None);
Arc::new(SupabaseUserServiceImpl::new(server))
Arc::new(SupabaseUserServiceImpl::new(server, vec![], None))
}
pub fn folder_service() -> Arc<dyn FolderCloudService> {

View File

@ -15,3 +15,4 @@ serde_json = {version = "1.0"}
serde_repr = "0.1"
chrono = { version = "0.4.22", default-features = false, features = ["clock"] }
anyhow = "1.0.71"
tokio = { version = "1.26", features = ["sync"] }

View File

@ -5,6 +5,7 @@ use std::str::FromStr;
use anyhow::Error;
use collab_define::CollabObject;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use uuid::Uuid;
use flowy_error::{ErrorCode, FlowyError};
@ -103,6 +104,12 @@ pub trait UserService: Send + Sync {
fn get_user_awareness_updates(&self, uid: i64) -> FutureResult<Vec<Vec<u8>>, Error>;
fn receive_realtime_event(&self, _json: Value) {}
fn subscribe_user_update(&self) -> Option<UserUpdateReceiver> {
None
}
fn create_collab_object(
&self,
collab_object: &CollabObject,
@ -110,6 +117,16 @@ pub trait UserService: Send + Sync {
) -> FutureResult<(), Error>;
}
pub type UserUpdateReceiver = tokio::sync::broadcast::Receiver<UserUpdate>;
pub type UserUpdateSender = tokio::sync::broadcast::Sender<UserUpdate>;
#[derive(Debug, Clone)]
pub struct UserUpdate {
pub uid: i64,
pub name: String,
pub email: String,
pub encryption_sign: String,
}
pub fn third_party_params_from_box_any(any: BoxAny) -> Result<ThirdPartyParams, Error> {
let map: HashMap<String, String> = any.unbox_or_error()?;
let uuid = uuid_from_map(&map)?;

View File

@ -211,15 +211,20 @@ impl EncryptionType {
EncryptionType::SelfEncryption(sign.to_owned())
}
}
}
impl EncryptionType {
pub fn is_need_encrypt_secret(&self) -> bool {
match self {
EncryptionType::NoEncryption => false,
EncryptionType::SelfEncryption(sign) => !sign.is_empty(),
}
}
pub fn sign(&self) -> String {
match self {
EncryptionType::NoEncryption => "".to_owned(),
EncryptionType::SelfEncryption(sign) => sign.to_owned(),
}
}
}
impl FromStr for EncryptionType {

View File

@ -154,3 +154,24 @@ pub struct UserStatePB {
#[pb(index = 1)]
pub auth_type: AuthTypePB,
}
#[derive(ProtoBuf, Debug, Default, Clone)]
pub struct AuthStateChangedPB {
#[pb(index = 1)]
pub state: AuthStatePB,
}
#[derive(ProtoBuf_Enum, Debug, Clone)]
pub enum AuthStatePB {
// adding AuthState prefix to avoid conflict with other enums
AuthStateUnknown = 0,
AuthStateSignIn = 1,
AuthStateSignOut = 2,
AuthStateForceSignOut = 3,
}
impl Default for AuthStatePB {
fn default() -> Self {
Self::AuthStateUnknown
}
}

View File

@ -95,8 +95,9 @@ pub async fn get_user_profile_handler(
) -> DataResult<UserProfilePB, FlowyError> {
let manager = upgrade_manager(manager)?;
let uid = manager.get_session()?.user_id;
let user_profile: UserProfilePB = manager.get_user_profile(uid, true).await?.into();
data_result_ok(user_profile)
let user_profile = manager.get_user_profile(uid).await?;
let _ = manager.refresh_user_profile(&user_profile).await;
data_result_ok(user_profile.into())
}
#[tracing::instrument(level = "debug", skip(manager))]
@ -222,7 +223,7 @@ pub async fn check_encrypt_secret_handler(
) -> DataResult<UserEncryptionSecretCheckPB, FlowyError> {
let manager = upgrade_manager(manager)?;
let uid = manager.get_session()?.user_id;
let profile = manager.get_user_profile(uid, false).await?;
let profile = manager.get_user_profile(uid).await?;
let is_need_secret = match profile.encryption_type {
EncryptionType::NoEncryption => false,

View File

@ -1,7 +1,6 @@
use std::sync::{Arc, Weak};
use collab_folder::core::FolderData;
use serde_json::Value;
use strum_macros::Display;
use flowy_derive::{Flowy_Event, ProtoBuf_Enum};
@ -97,7 +96,6 @@ pub trait UserStatusCallback: Send + Sync + 'static {
fn did_expired(&self, token: &str, user_id: i64) -> Fut<FlowyResult<()>>;
fn open_workspace(&self, user_id: i64, user_workspace: &UserWorkspace) -> Fut<FlowyResult<()>>;
fn did_update_network(&self, _reachable: bool) {}
fn receive_realtime_event(&self, _json: Value) {}
}
/// The user cloud service provider.

View File

@ -14,10 +14,11 @@ use flowy_sqlite::kv::StorePreferences;
use flowy_sqlite::schema::user_table;
use flowy_sqlite::ConnectionPool;
use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods};
use flowy_user_deps::cloud::UserUpdate;
use flowy_user_deps::entities::*;
use lib_infra::box_any::BoxAny;
use crate::entities::{UserProfilePB, UserSettingPB};
use crate::entities::{AuthStateChangedPB, AuthStatePB, UserProfilePB, UserSettingPB};
use crate::event_map::{
DefaultUserStatusCallback, SignUpContext, UserCloudServiceProvider, UserStatusCallback,
};
@ -61,6 +62,7 @@ pub struct UserManager {
pub(crate) user_status_callback: RwLock<Arc<dyn UserStatusCallback>>,
pub(crate) collab_builder: Weak<AppFlowyCollabBuilder>,
resumable_sign_up: Mutex<Option<ResumableSignUp>>,
current_session: parking_lot::RwLock<Option<Session>>,
}
impl UserManager {
@ -69,11 +71,12 @@ impl UserManager {
cloud_services: Arc<dyn UserCloudServiceProvider>,
store_preferences: Arc<StorePreferences>,
collab_builder: Weak<AppFlowyCollabBuilder>,
) -> Self {
) -> Arc<Self> {
let database = UserDB::new(&session_config.root_dir);
let user_status_callback: RwLock<Arc<dyn UserStatusCallback>> =
RwLock::new(Arc::new(DefaultUserStatusCallback));
Self {
let user_manager = Arc::new(Self {
database,
session_config,
cloud_services,
@ -82,7 +85,25 @@ impl UserManager {
user_status_callback,
collab_builder,
resumable_sign_up: Default::default(),
current_session: Default::default(),
});
let weak_user_manager = Arc::downgrade(&user_manager);
if let Ok(user_service) = user_manager.cloud_services.get_user_service() {
if let Some(mut rx) = user_service.subscribe_user_update() {
tokio::spawn(async move {
while let Ok(update) = rx.recv().await {
if let Some(user_manager) = weak_user_manager.upgrade() {
if let Err(err) = user_manager.handler_user_update(update).await {
tracing::error!("handler_user_update failed: {:?}", err);
}
}
}
});
}
}
user_manager
}
pub fn get_store_preferences(&self) -> Weak<StorePreferences> {
@ -121,6 +142,7 @@ impl UserManager {
self
.initialize_user_awareness(&session, UserAwarenessDataSource::Local)
.await;
let cloud_config = get_cloud_config(session.user_id, &self.store_preferences);
if let Err(e) = user_status_callback
.did_init(
@ -191,9 +213,10 @@ impl UserManager {
{
tracing::error!("Failed to call did_sign_in callback: {:?}", e);
}
send_sign_in_notification()
.payload::<UserProfilePB>(user_profile.clone().into())
.send();
send_auth_state_notification(AuthStateChangedPB {
state: AuthStatePB::AuthStateSignIn,
})
.send();
Ok(user_profile)
}
@ -322,6 +345,11 @@ impl UserManager {
self
.save_auth_data(&response, auth_type, &new_session)
.await?;
send_auth_state_notification(AuthStateChangedPB {
state: AuthStatePB::AuthStateSignIn,
})
.send();
Ok(())
}
@ -329,7 +357,7 @@ impl UserManager {
pub async fn sign_out(&self) -> Result<(), FlowyError> {
let session = self.get_session()?;
self.database.close(session.user_id)?;
self.set_current_session(None)?;
self.set_session(None)?;
let server = self.cloud_services.get_user_service()?;
tokio::spawn(async move {
@ -352,27 +380,10 @@ impl UserManager {
&self,
params: UpdateUserProfileParams,
) -> Result<(), FlowyError> {
let old_user_profile = self.get_user_profile(params.uid, false).await?;
let auth_type = old_user_profile.auth_type.clone();
let session = self.get_session()?;
let changeset = UserTableChangeset::new(params.clone());
diesel_update_table!(
user_table,
changeset,
&*self.db_connection(session.user_id)?
);
let session = self.get_session()?;
let new_user_profile = self.get_user_profile(session.user_id, false).await?;
send_notification(
&session.user_id.to_string(),
UserNotification::DidUpdateUserProfile,
)
.payload(UserProfilePB::from(new_user_profile))
.send();
self
.update_user(&auth_type, session.user_id, None, params)
.await?;
save_user_profile_change(session.user_id, self.db_pool(session.user_id)?, changeset)?;
self.update_user(session.user_id, None, params).await?;
Ok(())
}
@ -396,44 +407,38 @@ impl UserManager {
}
/// Fetches the user profile for the given user ID.
///
/// This function retrieves the user profile from the local database. If the `refresh` flag is set to `true`,
/// it also attempts to update the user profile from a cloud service, and then sends a notification about the
/// profile update.
pub async fn get_user_profile(&self, uid: i64, refresh: bool) -> Result<UserProfile, FlowyError> {
let user_id = uid.to_string();
let user = user_table::dsl::user_table
.filter(user_table::id.eq(&user_id))
.first::<UserTable>(&*(self.db_connection(uid)?))?;
pub async fn get_user_profile(&self, uid: i64) -> Result<UserProfile, FlowyError> {
let user: UserProfile = user_table::dsl::user_table
.filter(user_table::id.eq(&uid.to_string()))
.first::<UserTable>(&*(self.db_connection(uid)?))?
.into();
if refresh {
let weak_auth_service = Arc::downgrade(&self.cloud_services.get_user_service()?);
let weak_pool = Arc::downgrade(&self.database.get_pool(uid)?);
tokio::spawn(async move {
if let (Some(auth_service), Some(pool)) = (weak_auth_service.upgrade(), weak_pool.upgrade())
{
if let Ok(Some(user_profile)) = auth_service
.get_user_profile(UserCredentials::from_uid(uid))
.await
{
let changeset = UserTableChangeset::from_user_profile(user_profile.clone());
if let Ok(conn) = pool.get() {
let filter =
user_table::dsl::user_table.filter(user_table::dsl::id.eq(changeset.id.clone()));
let _ = diesel::update(filter).set(changeset).execute(&*conn);
Ok(user)
}
// Send notification to the client
let user_profile_pb: UserProfilePB = user_profile.into();
send_notification(&uid.to_string(), UserNotification::DidUpdateUserProfile)
.payload(user_profile_pb)
.send();
}
}
}
});
#[tracing::instrument(level = "info", skip_all)]
pub async fn refresh_user_profile(
&self,
old_user_profile: &UserProfile,
) -> FlowyResult<UserProfile> {
let uid = old_user_profile.uid;
let new_user_profile: UserProfile = self
.cloud_services
.get_user_service()?
.get_user_profile(UserCredentials::from_uid(uid))
.await?
.ok_or_else(|| FlowyError::new(ErrorCode::RecordNotFound, "User not found"))?;
if !is_user_encryption_sign_valid(old_user_profile, &new_user_profile.encryption_type.sign()) {
return Err(FlowyError::new(
ErrorCode::InvalidEncryptSecret,
"Invalid encryption sign",
));
}
Ok(user.into())
let changeset = UserTableChangeset::from_user_profile(new_user_profile.clone());
let _ = save_user_profile_change(uid, self.database.get_pool(uid)?, changeset);
Ok(new_user_profile)
}
pub fn user_dir(&self, uid: i64) -> String {
@ -458,7 +463,6 @@ impl UserManager {
async fn update_user(
&self,
_auth_type: &AuthType,
uid: i64,
token: Option<String>,
params: UpdateUserProfileParams,
@ -490,32 +494,18 @@ impl UserManager {
Ok(())
}
pub(crate) fn set_current_session(&self, session: Option<Session>) -> Result<(), FlowyError> {
tracing::debug!("Set current user: {:?}", session);
match &session {
None => self
.store_preferences
.remove(&self.session_config.session_cache_key),
Some(session) => {
self
.store_preferences
.set_object(&self.session_config.session_cache_key, session.clone())
.map_err(internal_error)?;
},
}
Ok(())
}
pub async fn receive_realtime_event(&self, json: Value) {
self
.user_status_callback
.read()
.await
.receive_realtime_event(json);
if let Ok(user_service) = self.cloud_services.get_user_service() {
user_service.receive_realtime_event(json)
}
}
/// Returns the current user session.
pub fn get_session(&self) -> Result<Session, FlowyError> {
if let Some(session) = (self.current_session.read()).clone() {
return Ok(session);
}
match self
.store_preferences
.get_object::<Session>(&self.session_config.session_cache_key)
@ -524,10 +514,33 @@ impl UserManager {
ErrorCode::RecordNotFound,
"User is not logged in",
)),
Some(session) => Ok(session),
Some(session) => {
self.current_session.write().replace(session.clone());
Ok(session)
},
}
}
pub(crate) fn set_session(&self, session: Option<Session>) -> Result<(), FlowyError> {
tracing::debug!("Set current user: {:?}", session);
match &session {
None => {
self.current_session.write().take();
self
.store_preferences
.remove(&self.session_config.session_cache_key)
},
Some(session) => {
self.current_session.write().replace(session.clone());
self
.store_preferences
.set_object(&self.session_config.session_cache_key, session.clone())
.map_err(internal_error)?;
},
}
Ok(())
}
async fn save_auth_data(
&self,
response: &impl UserAuthResponse,
@ -547,7 +560,7 @@ impl UserManager {
self
.save_user(uid, (user_profile, auth_type.clone()).into())
.await?;
self.set_current_session(Some(session.clone()))?;
self.set_session(Some(session.clone()))?;
Ok(())
}
@ -558,6 +571,27 @@ impl UserManager {
self.cloud_services.set_device_id(&session.device_id);
}
async fn handler_user_update(&self, user_update: UserUpdate) -> FlowyResult<()> {
let session = self.get_session()?;
if session.user_id == user_update.uid {
tracing::debug!("Receive user update: {:?}", user_update);
let user_profile = self.get_user_profile(user_update.uid).await?;
if !is_user_encryption_sign_valid(&user_profile, &user_update.encryption_sign) {
return Ok(());
}
// Save the user profile change
save_user_profile_change(
user_update.uid,
self.db_pool(user_update.uid)?,
UserTableChangeset::from(user_update),
)?;
}
Ok(())
}
async fn migrate_local_user_to_cloud(
&self,
old_user: &MigrationUser,
@ -575,3 +609,33 @@ impl UserManager {
Ok(folder_data)
}
}
fn is_user_encryption_sign_valid(user_profile: &UserProfile, encryption_sign: &str) -> bool {
// If the local user profile's encryption sign is not equal to the user update's encryption sign,
// which means the user enable encryption in another device, we should logout the current user.
let is_valid = user_profile.encryption_type.sign() == encryption_sign;
if !is_valid {
send_auth_state_notification(AuthStateChangedPB {
state: AuthStatePB::AuthStateForceSignOut,
})
.send();
}
is_valid
}
fn save_user_profile_change(
uid: i64,
pool: Arc<ConnectionPool>,
changeset: UserTableChangeset,
) -> FlowyResult<()> {
let conn = pool.get()?;
diesel_update_table!(user_table, changeset, &*conn);
let user: UserProfile = user_table::dsl::user_table
.filter(user_table::id.eq(&uid.to_string()))
.first::<UserTable>(&*conn)?
.into();
send_notification(&uid.to_string(), UserNotification::DidUpdateUserProfile)
.payload(UserProfilePB::from(user))
.send();
Ok(())
}

View File

@ -1,13 +1,15 @@
use flowy_derive::ProtoBuf_Enum;
use flowy_notification::NotificationBuilder;
use crate::entities::AuthStateChangedPB;
const USER_OBSERVABLE_SOURCE: &str = "User";
#[derive(ProtoBuf_Enum, Debug, Default)]
pub(crate) enum UserNotification {
#[default]
Unknown = 0,
DidUserSignIn = 1,
UserAuthStateChanged = 1,
DidUpdateUserProfile = 2,
DidUpdateUserWorkspaces = 3,
DidUpdateCloudConfig = 4,
@ -23,6 +25,11 @@ pub(crate) fn send_notification(id: &str, ty: UserNotification) -> NotificationB
NotificationBuilder::new(id, ty, USER_OBSERVABLE_SOURCE)
}
pub(crate) fn send_sign_in_notification() -> NotificationBuilder {
NotificationBuilder::new("", UserNotification::DidUserSignIn, USER_OBSERVABLE_SOURCE)
pub(crate) fn send_auth_state_notification(payload: AuthStateChangedPB) -> NotificationBuilder {
NotificationBuilder::new(
"auth_state_change_notification",
UserNotification::UserAuthStateChanged,
USER_OBSERVABLE_SOURCE,
)
.payload(payload)
}

View File

@ -17,7 +17,7 @@ impl UserManager {
// Only migrate the data if the user is login in as a guest and sign up as a new user if the current
// auth type is not [AuthType::Local].
let session = self.get_session().ok()?;
let user_profile = self.get_user_profile(session.user_id, false).await.ok()?;
let user_profile = self.get_user_profile(session.user_id).await.ok()?;
if user_profile.auth_type == AuthType::Local && !auth_type.is_local() {
Some(MigrationUser {
user_profile,
@ -100,7 +100,7 @@ impl UserManager {
device_id,
user_workspace,
};
self.set_current_session(Some(session))?;
self.set_session(Some(session))?;
Ok(())
}
}

View File

@ -1,6 +1,7 @@
use std::str::FromStr;
use flowy_sqlite::schema::user_table;
use flowy_user_deps::cloud::UserUpdate;
use flowy_user_deps::entities::*;
/// The order of the fields in the struct must be the same as the order of the fields in the table.
@ -102,3 +103,14 @@ impl UserTableChangeset {
}
}
}
impl From<UserUpdate> for UserTableChangeset {
fn from(value: UserUpdate) -> Self {
UserTableChangeset {
id: value.uid.to_string(),
name: Some(value.name),
email: Some(value.email),
..Default::default()
}
}
}