diff --git a/lib/components/chat_history/twitch/raid_event.dart b/lib/components/chat_history/twitch/raid_event.dart index c057b4a56..465e941a5 100644 --- a/lib/components/chat_history/twitch/raid_event.dart +++ b/lib/components/chat_history/twitch/raid_event.dart @@ -3,10 +3,10 @@ import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:rtchat/components/chat_history/decorated_event.dart'; import 'package:rtchat/components/image/resilient_network_image.dart'; -import 'package:rtchat/models/adapters/actions.dart'; import 'package:rtchat/models/channels.dart'; import 'package:rtchat/models/messages/twitch/event.dart'; import 'package:rtchat/models/messages/twitch/eventsub_configuration.dart'; +import 'package:rtchat/models/user.dart'; class TwitchRaidEventWidget extends StatelessWidget { final TwitchRaidEventModel model; @@ -49,8 +49,9 @@ class TwitchRaidEventWidget extends StatelessWidget { color: Theme.of(context).buttonTheme.colorScheme?.primary))), onTap: () { - ActionsAdapter.instance - .send(channel, "/shoutout ${model.from.login}"); + final userModel = + Provider.of(context, listen: false); + userModel.send(channel, "/shoutout ${model.from.login}"); }); }), ]), diff --git a/lib/components/drawer/sidebar.dart b/lib/components/drawer/sidebar.dart index 1969958d3..549796b5b 100644 --- a/lib/components/drawer/sidebar.dart +++ b/lib/components/drawer/sidebar.dart @@ -5,7 +5,6 @@ import 'package:provider/provider.dart'; import 'package:rtchat/components/channel_search_bottom_sheet.dart'; import 'package:rtchat/components/drawer/quicklinks_listview.dart'; import 'package:rtchat/components/image/cross_fade_image.dart'; -import 'package:rtchat/models/adapters/actions.dart'; import 'package:rtchat/models/audio.dart'; import 'package:rtchat/models/channels.dart'; import 'package:rtchat/models/layout.dart'; @@ -132,10 +131,12 @@ class _DrawerHeader extends StatelessWidget { userChannel == model.activeChannel && userChannel != null ? (channel) { - ActionsAdapter.instance.send( - userChannel, - "/raid ${channel.displayName}", - ); + final userModel = + Provider.of( + context, + listen: false); + userModel.send(userChannel, + "/raid ${channel.displayName}"); } : null, controller: controller, diff --git a/lib/components/message_input.dart b/lib/components/message_input.dart index 1db9077fd..1a8c5956c 100644 --- a/lib/components/message_input.dart +++ b/lib/components/message_input.dart @@ -8,9 +8,9 @@ import 'package:provider/provider.dart'; import 'package:rtchat/components/autocomplete.dart'; import 'package:rtchat/components/emote_picker.dart'; import 'package:rtchat/components/image/resilient_network_image.dart'; -import 'package:rtchat/models/adapters/actions.dart'; import 'package:rtchat/models/channels.dart'; import 'package:rtchat/models/commands.dart'; +import 'package:rtchat/models/user.dart'; class MessageInputWidget extends StatefulWidget { final Channel channel; @@ -83,13 +83,8 @@ class _MessageInputWidgetState extends State { await Future.wait([ () async { try { - final error = - await ActionsAdapter.instance.send(widget.channel, value); - if (error != null) { - messenger.showSnackBar(SnackBar( - content: Text(error), - )); - } + final userModel = Provider.of(context, listen: false); + await userModel.send(widget.channel, value); } catch (e) { messenger.showSnackBar(SnackBar( content: Text(e.toString()), diff --git a/lib/models/adapters/actions.dart b/lib/models/adapters/actions.dart index 89fb8095e..bee57d232 100644 --- a/lib/models/adapters/actions.dart +++ b/lib/models/adapters/actions.dart @@ -1,6 +1,5 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_functions/cloud_functions.dart'; -import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:rtchat/models/channels.dart'; class ActionsAdapter { @@ -14,25 +13,6 @@ class ActionsAdapter { functions: FirebaseFunctions.instance); static ActionsAdapter? _instance; - Future send(Channel channel, String message) async { - final call = functions.httpsCallable('send'); - final key = firestore.collection('actions').doc().id; - for (var i = 0; i < 3; i++) { - try { - final result = await call({ - "id": key, - "provider": channel.provider, - "channelId": channel.channelId, - "message": message, - }); - return result.data; - } catch (e) { - FirebaseCrashlytics.instance.recordError(e, StackTrace.current); - } - } - throw Exception("Failed to send message"); - } - Future ban(Channel channel, String username) async { final call = functions.httpsCallable('ban'); await call({ diff --git a/lib/models/adapters/channels.dart b/lib/models/adapters/channels.dart index 304d0a4d0..75db02080 100644 --- a/lib/models/adapters/channels.dart +++ b/lib/models/adapters/channels.dart @@ -51,4 +51,18 @@ class ChannelsAdapter { } }); } + + /// Returns the Twitch login for a given Twitch user ID. This is useful for + /// IRC which uses the login instead of the display name. + Future getLogin(Channel channel) async { + final doc = await db.collection("channels").doc(channel.toString()).get(); + if (!doc.exists) { + return null; + } + final data = doc.data(); + if (data == null) { + return null; + } + return data["login"]; + } } diff --git a/lib/models/adapters/tokens.dart b/lib/models/adapters/tokens.dart new file mode 100644 index 000000000..3838dd02a --- /dev/null +++ b/lib/models/adapters/tokens.dart @@ -0,0 +1,26 @@ +import 'dart:convert'; + +import 'package:cloud_firestore/cloud_firestore.dart'; + +class TokensAdapter { + final FirebaseFirestore db; + + TokensAdapter._({required this.db}); + + static TokensAdapter get instance => + _instance ??= TokensAdapter._(db: FirebaseFirestore.instance); + static TokensAdapter? _instance; + + Future getAccessToken( + {required String userId, required String provider}) async { + final doc = await db.collection("tokens").doc(userId).get(); + if (!doc.exists) { + return null; + } + final data = doc.data(); + if (data == null || !data.containsKey(provider)) { + return null; + } + return jsonDecode(data[provider])['access_token']; + } +} diff --git a/lib/models/user.dart b/lib/models/user.dart index 5e8d44960..1d657ada0 100644 --- a/lib/models/user.dart +++ b/lib/models/user.dart @@ -4,8 +4,11 @@ import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/foundation.dart'; +import 'package:rtchat/models/adapters/channels.dart'; import 'package:rtchat/models/adapters/profiles.dart'; +import 'package:rtchat/models/adapters/tokens.dart'; import 'package:rtchat/models/channels.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; class UserModel extends ChangeNotifier { bool _isLoading = true; @@ -87,4 +90,27 @@ class UserModel extends ChangeNotifier { Future signIn(String token) => FirebaseAuth.instance.signInWithCustomToken(token); + + Future send(Channel channel, String message) async { + final uid = _user?.uid; + final userChannel = _userChannel; + if (uid == null || userChannel == null) { + return; + } + + final ws = WebSocketChannel.connect( + Uri.parse('wss://irc-ws.chat.twitch.tv:443'), + ); + + final token = await TokensAdapter.instance + .getAccessToken(userId: uid, provider: channel.provider); + final userLogin = await ChannelsAdapter.instance.getLogin(userChannel); + final channelLogin = await ChannelsAdapter.instance.getLogin(channel); + + ws.sink.add('CAP REQ :twitch.tv/commands'); + ws.sink.add('PASS oauth:$token'); + ws.sink.add('NICK $userLogin'); + ws.sink.add('PRIVMSG #$channelLogin :$message'); + ws.sink.close(); + } } diff --git a/pubspec.lock b/pubspec.lock index 7ee509409..ba0b20079 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -173,10 +173,10 @@ packages: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.17.2" crypto: dependency: "direct main" description: @@ -553,10 +553,10 @@ packages: dependency: "direct main" description: name: intl - sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.18.0" + version: "0.18.1" js: dependency: transitive description: @@ -625,18 +625,18 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.5.0" meta: dependency: transitive description: @@ -854,10 +854,10 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" stack_trace: dependency: transitive description: @@ -926,10 +926,10 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.0" typed_data: dependency: transitive description: @@ -1066,6 +1066,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.1" + web: + dependency: transitive + description: + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.dev" + source: hosted + version: "0.1.4-beta" + web_socket_channel: + dependency: "direct main" + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" webview_flutter: dependency: "direct main" description: @@ -1123,5 +1139,5 @@ packages: source: hosted version: "1.0.0" sdks: - dart: ">=3.0.0 <4.0.0" + dart: ">=3.1.0-185.0.dev <4.0.0" flutter: ">=3.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index a2d5dfbdc..0efbd75bd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,6 +50,7 @@ dependencies: flutter_custom_tabs: ^1.0.4 styled_text: ^7.0.0 barcode_scan2: ^4.2.4 + web_socket_channel: ^2.4.0 dev_dependencies: flutter_lints: ^2.0.2