From e768a05c378c0e89a9693b7458dd4c6799f2e197 Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Fri, 31 Dec 2021 20:34:12 -0500 Subject: [PATCH] Migrate from BTTV's FFZ API to FFZ's official API --- lib/api/ffz_api.dart | 87 ++++++++++++++++++---------------------- lib/models/emotes.dart | 68 ++++++++++++++++++++----------- lib/models/emotes.g.dart | 34 +++++++++++----- test/emote_test.dart | 52 +++++++++++++++--------- 4 files changed, 139 insertions(+), 102 deletions(-) diff --git a/lib/api/ffz_api.dart b/lib/api/ffz_api.dart index 7787e231..3384647d 100644 --- a/lib/api/ffz_api.dart +++ b/lib/api/ffz_api.dart @@ -3,16 +3,23 @@ import 'package:flutter/foundation.dart'; import 'package:frosty/models/badges.dart'; import 'package:frosty/models/emotes.dart'; import 'package:http/http.dart' as http; +import 'package:tuple/tuple.dart'; class FFZ { /// Returns a map of global FFZ emotes to their URL. static Future> getEmotesGlobal() async { - final url = Uri.parse('https://api.betterttv.net/3/cached/frankerfacez/emotes/global'); + final url = Uri.parse('https://api.frankerfacez.com/v1/set/global'); final response = await http.get(url); if (response.statusCode == 200) { - final decoded = jsonDecode(response.body) as List; - final emotes = decoded.map((emote) => EmoteFFZ.fromJson(emote)); + final decoded = jsonDecode(response.body); + final defaultSets = decoded['default_sets'] as List; + + final emotes = []; + for (final setId in defaultSets) { + final emoticons = decoded['sets'][setId.toString()]['emoticons'] as List; + emotes.addAll(emoticons.map((emote) => EmoteFFZ.fromJson(emote))); + } return emotes.map((emote) => Emote.fromFFZ(emote, EmoteType.ffzGlobal)).toList(); } else { @@ -21,60 +28,44 @@ class FFZ { } } - /// Returns a map of a channel's FFZ emotes to their URL. - static Future> getEmotesChannel({required String id}) async { - final url = Uri.parse('https://api.betterttv.net/3/cached/frankerfacez/users/twitch/$id'); - final response = await http.get(url); - - if (response.statusCode == 200) { - final decoded = jsonDecode(response.body) as List; - final emotes = decoded.map((emote) => EmoteFFZ.fromJson(emote)); - - return emotes.map((emote) => Emote.fromFFZ(emote, EmoteType.ffzChannel)).toList(); - } else { - debugPrint('Failed to get FFZ emotes for id: $id. Error code: ${response.statusCode}'); - return []; - } - } - - static Future>?> getBadges() async { - final url = Uri.parse('https://api.frankerfacez.com/v1/badges'); + /// Returns a channel's FFZ room info including custom badges and emote used. + static Future>?> getRoomInfo({required String name}) async { + final url = Uri.parse('https://api.frankerfacez.com/v1/room/$name'); final response = await http.get(url); if (response.statusCode == 200) { final decoded = jsonDecode(response.body); - final badges = decoded['badges'] as List; - final users = decoded['users']; + final roomInfo = RoomFFZ.fromJson(decoded['room']); + final emoticons = decoded['sets'][roomInfo.set.toString()]['emoticons'] as List; + final emotes = emoticons.map((emote) => EmoteFFZ.fromJson(emote)); + + return Tuple2(roomInfo, emotes.map((emote) => Emote.fromFFZ(emote, EmoteType.ffzChannel)).toList()); + } else { + debugPrint('Failed to get FFZ emotes for id: $name. Error code: ${response.statusCode}'); + } + } + + static Future>?> getBadges() async { + final url = Uri.parse('https://api.frankerfacez.com/v1/badges/ids'); + final response = await http.get(url); + + if (response.statusCode == 200) { + final decoded = jsonDecode(response.body); + + final badges = decoded['badges'] as List; final badgeObjects = badges.map((badge) => BadgeInfoFFZ.fromJson(badge)).toList(); final result = >{}; - final developers = users['1'] as List; - final bots = users['2'] as List; - final supporters = users['3'] as List; - for (var username in developers) { - final entry = result[username]; - if (entry != null) { - entry.add(badgeObjects[0]); - } else { - result[username] = [badgeObjects[0]]; - } - } - for (var username in bots) { - final entry = result[username]; - if (entry != null) { - entry.add(badgeObjects[1]); - } else { - result[username] = [badgeObjects[1]]; - } - } - for (var username in supporters) { - final entry = result[username]; - if (entry != null) { - entry.add(badgeObjects[2]); - } else { - result[username] = [badgeObjects[2]]; + for (final badge in badgeObjects.reversed) { + for (final userId in decoded['users'][badge.id.toString()]) { + final entry = result[userId.toString()]; + if (entry == null) { + result[userId.toString()] = [badge]; + } else { + entry.add(badge); + } } } diff --git a/lib/models/emotes.dart b/lib/models/emotes.dart index 3fd51dd3..bbe13821 100644 --- a/lib/models/emotes.dart +++ b/lib/models/emotes.dart @@ -106,28 +106,32 @@ class EmoteBTTVChannel { } // * FFZ Emotes * -@JsonSerializable(createToJson: false) -class UserFFZ { - final int id; - final String name; - final String displayName; +@JsonSerializable(createToJson: false, fieldRename: FieldRename.snake) +class RoomFFZ { + final int set; + final String? moderatorBadge; + final ImagesFFZ? vipBadge; + final ImagesFFZ? modUrls; + // final Map> userBadges; + // final Map> userBadgesIds; - const UserFFZ( - this.id, - this.name, - this.displayName, + const RoomFFZ( + this.set, + this.moderatorBadge, + this.vipBadge, + this.modUrls, ); - factory UserFFZ.fromJson(Map json) => _$UserFFZFromJson(json); + factory RoomFFZ.fromJson(Map json) => _$RoomFFZFromJson(json); } @JsonSerializable(createToJson: false) class ImagesFFZ { - @JsonKey(name: '1x') + @JsonKey(name: '1') final String url1x; - @JsonKey(name: '2x') + @JsonKey(name: '2') final String? url2x; - @JsonKey(name: '4x') + @JsonKey(name: '4') final String? url4x; const ImagesFFZ( @@ -142,22 +146,40 @@ class ImagesFFZ { @JsonSerializable(createToJson: false) class EmoteFFZ { final int id; - final UserFFZ user; - final String code; - final ImagesFFZ images; - final String imageType; + final String name; + final int height; + final int width; + final OwnerFFZ owner; + final ImagesFFZ urls; const EmoteFFZ( this.id, - this.user, - this.code, - this.images, - this.imageType, + this.name, + this.height, + this.width, + this.owner, + this.urls, ); factory EmoteFFZ.fromJson(Map json) => _$EmoteFFZFromJson(json); } +@JsonSerializable(createToJson: false, fieldRename: FieldRename.snake) +class OwnerFFZ { + @JsonKey(name: '_id') + final int id; + final String name; + final String displayName; + + const OwnerFFZ( + this.id, + this.name, + this.displayName, + ); + + factory OwnerFFZ.fromJson(Map json) => _$OwnerFFZFromJson(json); +} + // * 7TV Emotes * @JsonSerializable(createToJson: false) @@ -272,9 +294,9 @@ class Emote { factory Emote.fromFFZ(EmoteFFZ emote, EmoteType type) => Emote( id: emote.id.toString(), - name: emote.code, + name: emote.name, zeroWidth: false, - url: emote.images.url4x ?? emote.images.url2x ?? emote.images.url1x, + url: 'https:' + (emote.urls.url4x ?? emote.urls.url2x ?? emote.urls.url1x), type: type, ); diff --git a/lib/models/emotes.g.dart b/lib/models/emotes.g.dart index a73a6395..cc774312 100644 --- a/lib/models/emotes.g.dart +++ b/lib/models/emotes.g.dart @@ -54,24 +54,36 @@ EmoteBTTVChannel _$EmoteBTTVChannelFromJson(Map json) => .toList(), ); -UserFFZ _$UserFFZFromJson(Map json) => UserFFZ( - json['id'] as int, - json['name'] as String, - json['displayName'] as String, +RoomFFZ _$RoomFFZFromJson(Map json) => RoomFFZ( + json['set'] as int, + json['moderator_badge'] as String?, + json['vip_badge'] == null + ? null + : ImagesFFZ.fromJson(json['vip_badge'] as Map), + json['mod_urls'] == null + ? null + : ImagesFFZ.fromJson(json['mod_urls'] as Map), ); ImagesFFZ _$ImagesFFZFromJson(Map json) => ImagesFFZ( - json['1x'] as String, - json['2x'] as String?, - json['4x'] as String?, + json['1'] as String, + json['2'] as String?, + json['4'] as String?, ); EmoteFFZ _$EmoteFFZFromJson(Map json) => EmoteFFZ( json['id'] as int, - UserFFZ.fromJson(json['user'] as Map), - json['code'] as String, - ImagesFFZ.fromJson(json['images'] as Map), - json['imageType'] as String, + json['name'] as String, + json['height'] as int, + json['width'] as int, + OwnerFFZ.fromJson(json['owner'] as Map), + ImagesFFZ.fromJson(json['urls'] as Map), + ); + +OwnerFFZ _$OwnerFFZFromJson(Map json) => OwnerFFZ( + json['_id'] as int, + json['name'] as String, + json['display_name'] as String, ); Role7TV _$Role7TVFromJson(Map json) => Role7TV( diff --git a/test/emote_test.dart b/test/emote_test.dart index 28f6f61b..214c83a9 100644 --- a/test/emote_test.dart +++ b/test/emote_test.dart @@ -182,34 +182,46 @@ void main() { test("emote should parse correctly", () { const sampleJson = """ { - "id": 317897, - "user": { - "id": 84534, - "name": "vulpeshd", - "displayName": "VulpesHD" + "id": 27081, + "name": "ZreknarF", + "height": 30, + "width": 40, + "public": false, + "hidden": false, + "modifier": false, + "offset": null, + "margins": null, + "css": null, + "owner": { + "_id": 1, + "name": "sirstendec", + "display_name": "SirStendec" }, - "code": "peepoPog", - "images": { - "1x": "https://cdn.betterttv.net/frankerfacez_emote/317897/1", - "2x": "https://cdn.betterttv.net/frankerfacez_emote/317897/2", - "4x": null + "urls": { + "1": "//cdn.frankerfacez.com/emote/27081/1", + "2": "//cdn.frankerfacez.com/emote/27081/2", + "4": "//cdn.frankerfacez.com/emote/27081/4" }, - "imageType": "png" + "status": 1, + "usage_count": 1, + "created_at": "2015-06-01T05:52:57.770Z", + "last_updated": "2015-06-01T05:53:03.744Z" } """; final json = jsonDecode(sampleJson); final emote = EmoteFFZ.fromJson(json); - expect(emote.id, 317897); - expect(emote.user.id, 84534); - expect(emote.user.name, "vulpeshd"); - expect(emote.user.displayName, "VulpesHD"); - expect(emote.code, "peepoPog"); - expect(emote.images.url1x, "https://cdn.betterttv.net/frankerfacez_emote/317897/1"); - expect(emote.images.url2x, "https://cdn.betterttv.net/frankerfacez_emote/317897/2"); - expect(emote.images.url4x, null); - expect(emote.imageType, "png"); + expect(emote.id, 27081); + expect(emote.height, 30); + expect(emote.width, 40); + expect(emote.name, 'ZreknarF'); + expect(emote.owner.id, 1); + expect(emote.owner.name, 'sirstendec'); + expect(emote.owner.displayName, 'SirStendec'); + expect(emote.urls.url1x, '//cdn.frankerfacez.com/emote/27081/1'); + expect(emote.urls.url2x, '//cdn.frankerfacez.com/emote/27081/2'); + expect(emote.urls.url4x, '//cdn.frankerfacez.com/emote/27081/4'); }); });