Improve chat error handling

This commit is contained in:
tommyxchow 2022-01-15 15:46:46 -05:00
parent 268f8427d6
commit 679a7f45f0
9 changed files with 181 additions and 101 deletions

View file

@ -20,7 +20,7 @@ class BTTVApi {
return emotes.map((emote) => Emote.fromBTTV(emote, EmoteType.bttvGlobal)).toList();
} else {
throw Exception('Failed to get global BTTV emotes.');
return Future.error('Failed to get BTTV global emotes.');
}
}
@ -39,7 +39,7 @@ class BTTVApi {
return emoteToUrl;
} else {
throw Exception('Failed to get channel BTTV emotes.');
return Future.error('Failed to get BTTV channel emotes.');
}
}
@ -54,7 +54,7 @@ class BTTVApi {
return {for (final badge in badgeObjects) badge.providerId: Badge.fromBTTV(badge)};
} else {
throw Exception('Failed to get BTTV badges');
return Future.error('Failed to get BTTV badges');
}
}
}

View file

@ -27,7 +27,7 @@ class FFZApi {
return emotes.map((emote) => Emote.fromFFZ(emote, EmoteType.ffzGlobal)).toList();
} else {
throw Exception('Failed to get global FFZ emotes.');
return Future.error('Failed to get FFZ global emotes.');
}
}
@ -45,7 +45,7 @@ class FFZApi {
return Tuple2(roomInfo, emotes.map((emote) => Emote.fromFFZ(emote, EmoteType.ffzChannel)).toList());
} else {
throw Exception('Failed to get FFZ room info.}');
return Future.error('Failed to get FFZ room info.');
}
}
@ -73,7 +73,7 @@ class FFZApi {
return result;
} else {
throw Exception('Failed to get FFZ badges.');
return Future.error('Failed to get FFZ badges.');
}
}
}

View file

@ -21,7 +21,7 @@ class SevenTVAPI {
return emotes.map((emote) => Emote.from7TV(emote, EmoteType.sevenTvGlobal)).toList();
} else {
throw Exception('Failed to get global 7TV emotes.');
return Future.error('Failed to get 7TV global emotes');
}
}
@ -36,7 +36,7 @@ class SevenTVAPI {
return emotes.map((emote) => Emote.from7TV(emote, EmoteType.sevenTvChannel)).toList();
} else {
throw Exception('Failed to get channel 7TV emotes.');
return Future.error('Failed to get 7TV channel emotes');
}
}
@ -63,7 +63,7 @@ class SevenTVAPI {
return result;
} else {
throw Exception('Failed to get 7TV badges.');
return Future.error('Failed to get 7TV badges.');
}
}
}

View file

@ -26,7 +26,7 @@ class TwitchApi {
return emotes.map((emote) => Emote.fromTwitch(emote, EmoteType.twitchGlobal)).toList();
} else {
throw Exception('Failed to get global Twitch emotes.');
return Future.error('Failed to get Twitch global emotes.');
}
}
@ -44,7 +44,7 @@ class TwitchApi {
return emotes.map((emote) => Emote.fromTwitch(emote, EmoteType.twitchChannel)).toList();
} else {
throw Exception('Failed to get channel Twitch emotes.');
return Future.error('Failed to get Twitch channel emotes.');
}
}
@ -72,7 +72,7 @@ class TwitchApi {
}
}).toList();
} else {
throw Exception('Failed to get Twitch emotes set.');
return Future.error('Failed to get Twitch emotes set.');
}
}
@ -89,7 +89,7 @@ class TwitchApi {
return result;
} else {
throw Exception('Failed to get global Twitch badges.');
return Future.error('Failed to get Twitch global badges.');
}
}
@ -106,7 +106,7 @@ class TwitchApi {
return result;
} else {
throw Exception('Failed to get Twitch channel badges.');
return Future.error('Failed to get Twitch channel badges.');
}
}
@ -120,7 +120,7 @@ class TwitchApi {
return UserTwitch.fromJson(userData.first);
} else {
throw Exception('Failed to get user info');
return Future.error('Failed to get Twitch user info');
}
}
@ -141,7 +141,7 @@ class TwitchApi {
if (response.statusCode == 200) {
return jsonDecode(response.body)['access_token'];
} else {
throw Exception('Failed to get default token.');
return Future.error('Failed to get default token.');
}
}
@ -227,10 +227,10 @@ class TwitchApi {
if (data.isNotEmpty) {
return StreamTwitch.fromJson(data.first);
} else {
throw Exception('$userLogin is offline');
return Future.error('$userLogin is offline');
}
} else {
throw Exception('Failed to get stream info.');
return Future.error('Failed to get stream info.');
}
}
@ -293,7 +293,7 @@ class TwitchApi {
return channelData.map((e) => ChannelQuery.fromJson(e)).toList();
} else {
throw Exception('Failed to get channels.');
return Future.error('Failed to get channels.');
}
}
@ -325,7 +325,7 @@ class TwitchApi {
return CategoriesTwitch.fromJson(decoded);
} else {
throw Exception('Failed to get categories.');
return Future.error('Failed to get categories.');
}
}
@ -339,7 +339,7 @@ class TwitchApi {
return decoded['total'] as int;
} else {
throw Exception('Failed to get sub count.');
return Future.error('Failed to get sub count.');
}
}
@ -352,7 +352,7 @@ class TwitchApi {
return ChatUsers.fromJson(decoded);
} else {
throw Exception('Failed to get chatters.');
return Future.error('Failed to get chatters.');
}
}
@ -381,10 +381,10 @@ class TwitchApi {
return result;
} else {
throw Exception('User does not have anyone blocked');
return Future.error('User does not have anyone blocked');
}
} else {
throw Exception('User does not exist');
return Future.error('User does not exist');
}
}

View file

@ -4,7 +4,6 @@ import 'package:frosty/api/seventv_api.dart';
import 'package:frosty/api/twitch_api.dart';
import 'package:frosty/models/badges.dart';
import 'package:frosty/models/emotes.dart';
import 'package:frosty/models/user.dart';
import 'package:mobx/mobx.dart';
part 'chat_assets_store.g.dart';
@ -81,56 +80,76 @@ abstract class _ChatAssetsStoreBase with Store {
/// Fetches global and channel assets (badges and emotes) and stores them in [_emoteToUrl]
@action
Future<void> getAssets({required String channelName, required Map<String, String> headers}) async {
// Fetch the desired channel/user's information.
final channelInfo = await twitchApi.getUser(userLogin: channelName, headers: headers);
if (channelInfo != null) {
Future<void> assetsFuture({
required String channelName,
required String channelId,
required Map<String, String> headers,
required Function onEmoteError,
required Function onBadgeError,
}) =>
// Fetch the global and channel's assets (emotes & badges).
// Async awaits are placed in a list so they are performed in parallel.
await Future.wait([
getEmotes(channelInfo: channelInfo, headers: headers),
getBadges(channelInfo: channelInfo, headers: headers),
Future.wait([
emotesFuture(
channelName: channelName,
channelId: channelId,
headers: headers,
onError: onEmoteError,
),
badgesFuture(
channelId: channelId,
headers: headers,
onError: onBadgeError,
),
]);
}
}
@action
Future<void> getEmotes({required UserTwitch channelInfo, required Map<String, String> headers}) async {
final assets = await Future.wait([
ffzApi.getEmotesGlobal(),
bttvApi.getEmotesGlobal(),
bttvApi.getEmotesChannel(id: channelInfo.id),
twitchApi.getEmotesGlobal(headers: headers),
twitchApi.getEmotesChannel(id: channelInfo.id, headers: headers),
sevenTvApi.getEmotesGlobal(),
sevenTvApi.getEmotesChannel(user: channelInfo.login),
ffzApi.getRoomInfo(name: channelInfo.login).then((ffzRoom) {
ffzRoomInfo = ffzRoom?.item1;
return ffzRoom?.item2 ?? <Emote>[];
}),
]);
final emotes = assets.expand((list) => list);
_emoteToObject = {for (final emote in emotes) emote.name: emote};
}
Future<void> emotesFuture({
required String channelId,
required String channelName,
required Map<String, String> headers,
required Function onError,
}) =>
// Parallel futures look ugly, but are worth the performance improvement.
// Each future has it's own catchError so that the entire future is not ended on an error.
// This will ensure that other futures complete even if one fails.
Future.wait([
ffzApi.getEmotesGlobal().catchError(onError),
bttvApi.getEmotesGlobal().catchError(onError),
bttvApi.getEmotesChannel(id: channelId).catchError(onError),
twitchApi.getEmotesGlobal(headers: headers).catchError(onError),
twitchApi.getEmotesChannel(id: channelId, headers: headers).catchError(onError),
sevenTvApi.getEmotesGlobal().catchError(onError),
sevenTvApi.getEmotesChannel(user: channelName).catchError(onError),
ffzApi.getRoomInfo(name: channelName).then((ffzRoom) {
ffzRoomInfo = ffzRoom.item1;
return ffzRoom.item2;
}).catchError(onError),
]).then((assets) => assets.expand((list) => list)).then((emotes) => _emoteToObject = {for (final emote in emotes) emote.name: emote});
@action
Future<void> getBadges({required UserTwitch channelInfo, required Map<String, String> headers}) async => await Future.wait([
Future<void> badgesFuture({
required String channelId,
required Map<String, String> headers,
required Function onError,
}) =>
Future.wait([
twitchApi
.getBadgesGlobal()
.then((badges) => twitchBadgesToObject.addAll(badges))
.then((_) => twitchApi.getBadgesChannel(id: channelInfo.id).then((badges) => twitchBadgesToObject.addAll(badges))),
ffzApi.getBadges().then((badges) => _userToFFZBadges = badges ?? {}),
sevenTvApi.getBadges().then((badges) => _userTo7TVBadges = badges ?? {}),
bttvApi.getBadges().then((badges) => _userToBTTVBadges = badges ?? {}),
.then((_) => twitchApi.getBadgesChannel(id: channelId).then((badges) => twitchBadgesToObject.addAll(badges)).catchError(onError)),
ffzApi.getBadges().then((badges) => _userToFFZBadges = badges).catchError(onError),
sevenTvApi.getBadges().then((badges) => _userTo7TVBadges = badges).catchError(onError),
bttvApi.getBadges().then((badges) => _userToBTTVBadges = badges).catchError(onError),
]);
@action
Future<void> getUserEmotes({required List<String> emoteSets, required Map<String, String> headers}) async =>
await Future.wait(emoteSets.map((setId) => twitchApi.getEmotesSets(setId: setId, headers: headers)))
Future<void> userEmotesFuture({
required List<String> emoteSets,
required Map<String, String> headers,
required Function onError,
}) =>
Future.wait(emoteSets.map((setId) => twitchApi.getEmotesSets(setId: setId, headers: headers).catchError(onError)))
.then((emotes) => emotes.expand((list) => list).toList())
.then((userEmotes) => _userEmoteToObject = {for (final emote in userEmotes) emote.name: emote}.asObservable());
}

View file

@ -152,41 +152,77 @@ mixin _$ChatAssetsStore on _ChatAssetsStoreBase, Store {
});
}
final _$getAssetsAsyncAction = AsyncAction('_ChatAssetsStoreBase.getAssets');
final _$_ChatAssetsStoreBaseActionController =
ActionController(name: '_ChatAssetsStoreBase');
@override
Future<void> getAssets(
{required String channelName, required Map<String, String> headers}) {
return _$getAssetsAsyncAction
.run(() => super.getAssets(channelName: channelName, headers: headers));
Future<void> assetsFuture(
{required String channelName,
required String channelId,
required Map<String, String> headers,
required Function onEmoteError,
required Function onBadgeError}) {
final _$actionInfo = _$_ChatAssetsStoreBaseActionController.startAction(
name: '_ChatAssetsStoreBase.assetsFuture');
try {
return super.assetsFuture(
channelName: channelName,
channelId: channelId,
headers: headers,
onEmoteError: onEmoteError,
onBadgeError: onBadgeError);
} finally {
_$_ChatAssetsStoreBaseActionController.endAction(_$actionInfo);
}
}
final _$getEmotesAsyncAction = AsyncAction('_ChatAssetsStoreBase.getEmotes');
@override
Future<void> getEmotes(
{required UserTwitch channelInfo, required Map<String, String> headers}) {
return _$getEmotesAsyncAction
.run(() => super.getEmotes(channelInfo: channelInfo, headers: headers));
Future<void> emotesFuture(
{required String channelId,
required String channelName,
required Map<String, String> headers,
required Function onError}) {
final _$actionInfo = _$_ChatAssetsStoreBaseActionController.startAction(
name: '_ChatAssetsStoreBase.emotesFuture');
try {
return super.emotesFuture(
channelId: channelId,
channelName: channelName,
headers: headers,
onError: onError);
} finally {
_$_ChatAssetsStoreBaseActionController.endAction(_$actionInfo);
}
}
final _$getBadgesAsyncAction = AsyncAction('_ChatAssetsStoreBase.getBadges');
@override
Future<void> getBadges(
{required UserTwitch channelInfo, required Map<String, String> headers}) {
return _$getBadgesAsyncAction
.run(() => super.getBadges(channelInfo: channelInfo, headers: headers));
Future<void> badgesFuture(
{required String channelId,
required Map<String, String> headers,
required Function onError}) {
final _$actionInfo = _$_ChatAssetsStoreBaseActionController.startAction(
name: '_ChatAssetsStoreBase.badgesFuture');
try {
return super.badgesFuture(
channelId: channelId, headers: headers, onError: onError);
} finally {
_$_ChatAssetsStoreBaseActionController.endAction(_$actionInfo);
}
}
final _$getUserEmotesAsyncAction =
AsyncAction('_ChatAssetsStoreBase.getUserEmotes');
@override
Future<void> getUserEmotes(
{required List<String> emoteSets, required Map<String, String> headers}) {
return _$getUserEmotesAsyncAction
.run(() => super.getUserEmotes(emoteSets: emoteSets, headers: headers));
Future<void> userEmotesFuture(
{required List<String> emoteSets,
required Map<String, String> headers,
required Function onError}) {
final _$actionInfo = _$_ChatAssetsStoreBaseActionController.startAction(
name: '_ChatAssetsStoreBase.userEmotesFuture');
try {
return super.userEmotesFuture(
emoteSets: emoteSets, headers: headers, onError: onError);
} finally {
_$_ChatAssetsStoreBaseActionController.endAction(_$actionInfo);
}
}
@override

View file

@ -3,6 +3,8 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:frosty/core/auth/auth_store.dart';
import 'package:frosty/models/badges.dart';
import 'package:frosty/models/emotes.dart';
import 'package:frosty/models/irc_message.dart';
import 'package:frosty/screens/channel/stores/chat_assets_store.dart';
import 'package:frosty/screens/channel/stores/chat_details_store.dart';
@ -30,6 +32,12 @@ abstract class _ChatStoreBase with Store {
/// The name of the channel to connect to.
final String channelName;
/// The channel's ID for API requests.
final String channelId;
/// The channel's display name to show on widgets.
final String displayName;
/// The Twitch IRC WebSocket channel.
WebSocketChannel? _channel;
@ -74,6 +82,8 @@ abstract class _ChatStoreBase with Store {
required this.assetsStore,
required this.settings,
required this.channelName,
required this.channelId,
required this.displayName,
}) {
// Create a reaction where anytime the emote menu is shown or hidden,
// scroll to the bottom of the list. This will prevent the emote menu
@ -145,11 +155,14 @@ abstract class _ChatStoreBase with Store {
case Command.globalUserState:
final setIds = parsedIRCMessage.tags['emote-sets']?.split(',');
if (setIds != null) {
_messages.add(IRCMessage.createNotice(message: 'Fetching user emotes...'));
assetsStore
.getUserEmotes(emoteSets: setIds, headers: auth.headersTwitch)
.then((_) => _messages.add(IRCMessage.createNotice(message: 'User emotes fetched!')))
.onError((error, stackTrace) => _messages.add(IRCMessage.createNotice(message: 'Failed to fetch user emotes: ${error.toString()}')));
assetsStore.userEmotesFuture(
emoteSets: setIds,
headers: auth.headersTwitch,
onError: (error) {
debugPrint(error.toString());
return <Emote>[];
},
);
}
continue;
case Command.none:
@ -170,15 +183,22 @@ abstract class _ChatStoreBase with Store {
_channel?.sink.add('PONG :tmi.twitch.tv');
return;
} else if (message.contains('Welcome, GLHF!')) {
_messages.add(IRCMessage.createNotice(message: "Connected to $channelName's chat"));
_messages.add(IRCMessage.createNotice(message: "Connected to $displayName's chat"));
// Fetch the assets used in chat including badges and emotes.
_messages.add(IRCMessage.createNotice(message: 'Fetching badges and emotes...'));
assetsStore
.getAssets(channelName: channelName, headers: auth.headersTwitch)
.then((_) => _messages.add(IRCMessage.createNotice(message: 'Badges and emotes fetched!')))
.onError((error, stackTrace) => _messages.add(IRCMessage.createNotice(message: 'Failed to fetch assets: ${error.toString()}')));
assetsStore.assetsFuture(
channelName: channelName,
channelId: channelId,
headers: auth.headersTwitch,
onEmoteError: (error) {
debugPrint(error.toString());
return <Emote>[];
},
onBadgeError: (error) {
debugPrint(error.toString());
return <Badge>[];
},
);
// Reset exponential backoff if successfully connected.
_retries = 0;
_backoffTime = 0;
@ -211,7 +231,6 @@ abstract class _ChatStoreBase with Store {
(data) => _handleIRCData(data.toString()),
onError: (error) {
debugPrint('Chat error: ${error.toString()}');
_messages.add(IRCMessage.createNotice(message: 'Chat error - ${error.toString()}'));
},
onDone: () async {
if (_channel == null) return;

View file

@ -35,6 +35,9 @@ class SearchResultsChannels extends StatelessWidget {
builder: (context) => VideoChat(
displayName: channelInfo.broadcasterName,
chatStore: ChatStore(
channelName: channelInfo.broadcasterLogin,
channelId: channelInfo.broadcasterId,
displayName: channelInfo.broadcasterName,
chatDetailsStore: ChatDetailsStore(
twitchApi: context.read<TwitchApi>(),
),
@ -44,7 +47,6 @@ class SearchResultsChannels extends StatelessWidget {
bttvApi: context.read<BTTVApi>(),
sevenTvApi: context.read<SevenTVAPI>(),
),
channelName: channelInfo.broadcasterLogin,
auth: searchStore.authStore,
settings: context.read<SettingsStore>(),
),
@ -110,6 +112,9 @@ class SearchResultsChannels extends StatelessWidget {
builder: (context) => VideoChat(
displayName: channel.displayName,
chatStore: ChatStore(
channelName: channel.broadcasterLogin,
channelId: channel.id,
displayName: channel.displayName,
chatDetailsStore: ChatDetailsStore(
twitchApi: context.read<TwitchApi>(),
),
@ -119,7 +124,6 @@ class SearchResultsChannels extends StatelessWidget {
bttvApi: context.read<BTTVApi>(),
sevenTvApi: context.read<SevenTVAPI>(),
),
channelName: channel.broadcasterLogin,
auth: searchStore.authStore,
settings: context.read<SettingsStore>(),
),

View file

@ -44,6 +44,8 @@ class StreamCard extends StatelessWidget {
displayName: streamInfo.userName,
chatStore: ChatStore(
channelName: streamInfo.userLogin,
channelId: streamInfo.userId,
displayName: streamInfo.userName,
auth: context.read<AuthStore>(),
settings: context.read<SettingsStore>(),
chatDetailsStore: ChatDetailsStore(