From 06f9acfaed15c057a4678971a823338a3f62e8e1 Mon Sep 17 00:00:00 2001 From: dshukertjr Date: Wed, 10 Jul 2024 14:04:25 +0900 Subject: [PATCH 1/7] Add private channel support --- .../lib/src/realtime_channel.dart | 28 +++++++++---------- .../lib/src/realtime_client.dart | 3 ++ .../realtime_client/lib/src/transformers.dart | 17 +++++++++++ packages/realtime_client/lib/src/types.dart | 19 +++++++++++++ .../realtime_client/test/channel_test.dart | 27 ++++++++++++++++-- .../test/transformers_test.dart | 15 ++++++++++ 6 files changed, 93 insertions(+), 16 deletions(-) diff --git a/packages/realtime_client/lib/src/realtime_channel.dart b/packages/realtime_client/lib/src/realtime_channel.dart index 3d492bfb..e4a94f0e 100644 --- a/packages/realtime_client/lib/src/realtime_channel.dart +++ b/packages/realtime_client/lib/src/realtime_channel.dart @@ -32,6 +32,8 @@ class RealtimeChannel { @internal final RealtimeClient socket; + late final bool _private; + RealtimeChannel( this.topic, this.socket, { @@ -40,7 +42,8 @@ class RealtimeChannel { params = params.toMap(), subTopic = topic.replaceFirst( RegExp(r"^realtime:", caseSensitive: false), "") { - broadcastEndpointURL = _broadcastEndpointURL; + broadcastEndpointURL = '${httpEndpointURL(socket.endPoint)}/api/broadcast'; + _private = params.private; joinPush = Push( this, @@ -117,6 +120,7 @@ class RealtimeChannel { } else { final broadcast = params['config']['broadcast']; final presence = params['config']['presence']; + final isPrivate = params['config']['private']; _onError((e) { if (callback != null) callback(RealtimeSubscribeStatus.channelError, e); @@ -131,6 +135,7 @@ class RealtimeChannel { 'presence': presence, 'postgres_changes': _bindings['postgres_changes']?.map((r) => r.filter).toList() ?? [], + 'private': isPrivate == true, }; if (socket.accessToken != null) { @@ -483,13 +488,20 @@ class RealtimeChannel { } if (!canPush && type == RealtimeListenTypes.broadcast) { - final headers = {'Content-Type': 'application/json', ...socket.headers}; + final headers = { + 'Content-Type': 'application/json', + 'apikey': socket.params['apikey'] ?? '', + ...socket.headers, + 'Authorization': + socket.accessToken != null ? 'Bearer ${socket.accessToken}' : '', + }; final body = { 'messages': [ { 'topic': subTopic, 'payload': payload, 'event': event, + 'private': _private, } ] }; @@ -595,18 +607,6 @@ class RealtimeChannel { return completer.future; } - String get _broadcastEndpointURL { - var url = socket.endPoint; - url = url.replaceFirst(RegExp(r'^ws', caseSensitive: false), 'http'); - url = url.replaceAll( - RegExp(r'(/socket/websocket|/socket|/websocket)/?$', - caseSensitive: false), - '', - ); - url = '${url.replaceAll(RegExp(r'/+$'), '')}/api/broadcast'; - return url; - } - /// Overridable message hook /// /// Receives all events for specialized message handling before dispatching to the channel callbacks. diff --git a/packages/realtime_client/lib/src/realtime_client.dart b/packages/realtime_client/lib/src/realtime_client.dart index c3f6cd12..295cd259 100644 --- a/packages/realtime_client/lib/src/realtime_client.dart +++ b/packages/realtime_client/lib/src/realtime_client.dart @@ -51,6 +51,7 @@ class RealtimeClient { String? accessToken; List channels = []; final String endPoint; + final String httpEndpoint; final Map headers; final Map params; final Duration timeout; @@ -85,6 +86,7 @@ class RealtimeClient { /// Initializes the Socket /// /// `endPoint` The string WebSocket endpoint, ie, "ws://example.com/socket", "wss://example.com", "/socket" (inherited host & protocol) + /// `httpEndpoint` The string HTTP endpoint, ie, "https://example.com", "/" (inherited host & protocol) /// `transport` The Websocket Transport, for example WebSocket. /// `timeout` The default timeout in milliseconds to trigger push timeouts. /// `params` The optional params to pass when connecting. @@ -115,6 +117,7 @@ class RealtimeClient { logLevel == null ? null : {'log_level': logLevel.name}, ) .toString(), + httpEndpoint = httpEndpointURL(endPoint), headers = { ...Constants.defaultHeaders, if (headers != null) ...headers, diff --git a/packages/realtime_client/lib/src/transformers.dart b/packages/realtime_client/lib/src/transformers.dart index d1e80d1b..383d8757 100644 --- a/packages/realtime_client/lib/src/transformers.dart +++ b/packages/realtime_client/lib/src/transformers.dart @@ -332,3 +332,20 @@ Map> getPayloadRecords( return records; } + +/// Converts a WebSocket URL to an HTTP URL. +String httpEndpointURL(String socketUrl) { + var url = socketUrl; + + // Replace 'ws' or 'wss' with 'http' or 'https' respectively + url = url.replaceFirst(RegExp(r'^ws', caseSensitive: false), 'http'); + + // Remove WebSocket-specific endings + url = url.replaceFirst( + RegExp(r'(/socket/websocket|/socket|/websocket)/?$', caseSensitive: false), + '', + ); + + // Remove trailing slashes + return url.replaceAll(RegExp(r'/+$'), ''); +} diff --git a/packages/realtime_client/lib/src/types.dart b/packages/realtime_client/lib/src/types.dart index 77f2e9de..eedc5e20 100644 --- a/packages/realtime_client/lib/src/types.dart +++ b/packages/realtime_client/lib/src/types.dart @@ -148,10 +148,14 @@ class RealtimeChannelConfig { /// [key] option is used to track presence payload across clients final String key; + /// defines if the channel is private or not and if RLS policies will be used to check data + final bool private; + const RealtimeChannelConfig({ this.ack = false, this.self = false, this.key = '', + this.private = false, }); Map toMap() { @@ -164,6 +168,7 @@ class RealtimeChannelConfig { 'presence': { 'key': key, }, + 'private': private, } }; } @@ -403,3 +408,17 @@ class SinglePresenceState { @override String toString() => 'PresenceState(key: $key, presences: $presences)'; } + +class Channel { + final String name; + final String inserted_at; + final String updated_at; + final int id; + + Channel({ + required this.name, + required this.inserted_at, + required this.updated_at, + required this.id, + }); +} diff --git a/packages/realtime_client/test/channel_test.dart b/packages/realtime_client/test/channel_test.dart index 080f428f..3cb03441 100644 --- a/packages/realtime_client/test/channel_test.dart +++ b/packages/realtime_client/test/channel_test.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:realtime_client/realtime_client.dart'; import 'package:realtime_client/src/constants.dart'; +import 'package:realtime_client/src/push.dart'; import 'package:realtime_client/src/types.dart'; import 'package:test/test.dart'; @@ -33,11 +34,31 @@ void main() { expect(channel.params, { 'config': { 'broadcast': {'ack': false, 'self': false}, - 'presence': {'key': ''} + 'presence': {'key': ''}, + 'private': false, } }); expect(channel.socket, socket); }); + + test('sets up joinPush object with private defined', () { + channel = RealtimeChannel( + 'topic', + socket, + params: RealtimeChannelConfig( + private: true, + ), + ); + final Push joinPush = channel.joinPush; + + expect(joinPush.payload, { + 'config': { + 'broadcast': {'ack': false, 'self': false}, + 'presence': {'key': ''}, + 'private': true, + }, + }); + }); }); group('join', () { @@ -252,7 +273,7 @@ void main() { params: {'apikey': 'supabaseKey'}, ); - channel = socket.channel('myTopic'); + channel = socket.channel('myTopic', RealtimeChannelConfig(private: true)); }); tearDown(() async { @@ -311,9 +332,11 @@ void main() { final body = json.decode(await utf8.decodeStream(req)); final message = body['messages'][0]; final payload = message['payload']; + final private = message['private']; expect(payload, containsPair('myKey', 'myValue')); expect(message, containsPair('topic', 'myTopic')); + expect(private, true); await req.response.close(); break; diff --git a/packages/realtime_client/test/transformers_test.dart b/packages/realtime_client/test/transformers_test.dart index d6fbfe9d..ba3ed666 100644 --- a/packages/realtime_client/test/transformers_test.dart +++ b/packages/realtime_client/test/transformers_test.dart @@ -234,4 +234,19 @@ void main() { expect(enrichedPayload, expectedMap); }); }); + + group('httpEndpointURL', () { + test('Converts a hosted Supabase WS URL', () { + expect( + httpEndpointURL('wss://example.supabase.co/realtime/v1'), + equals('https://example.supabase.co/realtime/v1'), + ); + }); + test('Converts a custom domain WS URL', () { + expect( + httpEndpointURL('wss://custom-domain.com/realtime/v1'), + equals('https://custom-domain.com/realtime/v1'), + ); + }); + }); } From 7225ed133ccd0818641da9797add0e4ebdbf0af4 Mon Sep 17 00:00:00 2001 From: dshukertjr Date: Wed, 10 Jul 2024 14:21:34 +0900 Subject: [PATCH 2/7] remove unused variables --- .../realtime_client/lib/src/realtime_client.dart | 3 +-- packages/realtime_client/lib/src/types.dart | 14 -------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/packages/realtime_client/lib/src/realtime_client.dart b/packages/realtime_client/lib/src/realtime_client.dart index 295cd259..6873a2ca 100644 --- a/packages/realtime_client/lib/src/realtime_client.dart +++ b/packages/realtime_client/lib/src/realtime_client.dart @@ -51,7 +51,7 @@ class RealtimeClient { String? accessToken; List channels = []; final String endPoint; - final String httpEndpoint; + final Map headers; final Map params; final Duration timeout; @@ -117,7 +117,6 @@ class RealtimeClient { logLevel == null ? null : {'log_level': logLevel.name}, ) .toString(), - httpEndpoint = httpEndpointURL(endPoint), headers = { ...Constants.defaultHeaders, if (headers != null) ...headers, diff --git a/packages/realtime_client/lib/src/types.dart b/packages/realtime_client/lib/src/types.dart index eedc5e20..571dbdf9 100644 --- a/packages/realtime_client/lib/src/types.dart +++ b/packages/realtime_client/lib/src/types.dart @@ -408,17 +408,3 @@ class SinglePresenceState { @override String toString() => 'PresenceState(key: $key, presences: $presences)'; } - -class Channel { - final String name; - final String inserted_at; - final String updated_at; - final int id; - - Channel({ - required this.name, - required this.inserted_at, - required this.updated_at, - required this.id, - }); -} From 89fd9ef969538e6473115ae94e690103c347dafd Mon Sep 17 00:00:00 2001 From: dshukertjr Date: Wed, 10 Jul 2024 14:32:23 +0900 Subject: [PATCH 3/7] update test --- packages/realtime_client/test/socket_test.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/realtime_client/test/socket_test.dart b/packages/realtime_client/test/socket_test.dart index b795b7e0..a5463ab3 100644 --- a/packages/realtime_client/test/socket_test.dart +++ b/packages/realtime_client/test/socket_test.dart @@ -287,7 +287,8 @@ void main() { expect(channel.params, { 'config': { 'broadcast': {'ack': false, 'self': false}, - 'presence': {'key': ''} + 'presence': {'key': ''}, + 'private': false, } }); }); From e6526e03cf0b4dbf66f45989a4eda30c864ea40c Mon Sep 17 00:00:00 2001 From: dshukertjr Date: Thu, 11 Jul 2024 11:40:14 +0900 Subject: [PATCH 4/7] chore: add comment to _private in realtime_channel --- packages/realtime_client/lib/src/realtime_channel.dart | 1 + packages/realtime_client/lib/src/types.dart | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/realtime_client/lib/src/realtime_channel.dart b/packages/realtime_client/lib/src/realtime_channel.dart index e4a94f0e..fe7b4ca8 100644 --- a/packages/realtime_client/lib/src/realtime_channel.dart +++ b/packages/realtime_client/lib/src/realtime_channel.dart @@ -32,6 +32,7 @@ class RealtimeChannel { @internal final RealtimeClient socket; + /// Defines if the channel is private or not and if RLS policies will be used to check data late final bool _private; RealtimeChannel( diff --git a/packages/realtime_client/lib/src/types.dart b/packages/realtime_client/lib/src/types.dart index 571dbdf9..19fb68b9 100644 --- a/packages/realtime_client/lib/src/types.dart +++ b/packages/realtime_client/lib/src/types.dart @@ -148,7 +148,7 @@ class RealtimeChannelConfig { /// [key] option is used to track presence payload across clients final String key; - /// defines if the channel is private or not and if RLS policies will be used to check data + /// Defines if the channel is private or not and if RLS policies will be used to check data final bool private; const RealtimeChannelConfig({ From 435fd7c409d2952769aac33a73a3963bd130f0be Mon Sep 17 00:00:00 2001 From: Tyler Date: Fri, 12 Jul 2024 11:35:12 +0900 Subject: [PATCH 5/7] Update packages/realtime_client/lib/src/realtime_channel.dart Co-authored-by: Bruno D'Luka <45696119+bdlukaa@users.noreply.github.com> --- packages/realtime_client/lib/src/realtime_channel.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/realtime_client/lib/src/realtime_channel.dart b/packages/realtime_client/lib/src/realtime_channel.dart index fe7b4ca8..8ca989b4 100644 --- a/packages/realtime_client/lib/src/realtime_channel.dart +++ b/packages/realtime_client/lib/src/realtime_channel.dart @@ -491,7 +491,8 @@ class RealtimeChannel { if (!canPush && type == RealtimeListenTypes.broadcast) { final headers = { 'Content-Type': 'application/json', - 'apikey': socket.params['apikey'] ?? '', + if (socket.params['apikey'] != null) + 'apikey': socket.params['apikey']!, ...socket.headers, 'Authorization': socket.accessToken != null ? 'Bearer ${socket.accessToken}' : '', From e2e551c280f5cc9dc801fd3e90c7cc91138c8e53 Mon Sep 17 00:00:00 2001 From: Tyler Date: Fri, 12 Jul 2024 11:35:32 +0900 Subject: [PATCH 6/7] Update packages/realtime_client/lib/src/realtime_channel.dart Co-authored-by: Bruno D'Luka <45696119+bdlukaa@users.noreply.github.com> --- packages/realtime_client/lib/src/realtime_channel.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/realtime_client/lib/src/realtime_channel.dart b/packages/realtime_client/lib/src/realtime_channel.dart index 8ca989b4..bc953b2d 100644 --- a/packages/realtime_client/lib/src/realtime_channel.dart +++ b/packages/realtime_client/lib/src/realtime_channel.dart @@ -494,8 +494,8 @@ class RealtimeChannel { if (socket.params['apikey'] != null) 'apikey': socket.params['apikey']!, ...socket.headers, - 'Authorization': - socket.accessToken != null ? 'Bearer ${socket.accessToken}' : '', + if (socket.accessToken != null) + 'Authorization': 'Bearer ${socket.accessToken}', }; final body = { 'messages': [ From a66e9f6c39648f9fa176818cc4d0b26fc88e90a4 Mon Sep 17 00:00:00 2001 From: dshukertjr Date: Fri, 12 Jul 2024 11:39:13 +0900 Subject: [PATCH 7/7] format code --- packages/realtime_client/lib/src/realtime_channel.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/realtime_client/lib/src/realtime_channel.dart b/packages/realtime_client/lib/src/realtime_channel.dart index bc953b2d..c7cd8568 100644 --- a/packages/realtime_client/lib/src/realtime_channel.dart +++ b/packages/realtime_client/lib/src/realtime_channel.dart @@ -491,8 +491,7 @@ class RealtimeChannel { if (!canPush && type == RealtimeListenTypes.broadcast) { final headers = { 'Content-Type': 'application/json', - if (socket.params['apikey'] != null) - 'apikey': socket.params['apikey']!, + if (socket.params['apikey'] != null) 'apikey': socket.params['apikey']!, ...socket.headers, if (socket.accessToken != null) 'Authorization': 'Bearer ${socket.accessToken}',