Skip to content

Commit

Permalink
Merge pull request #1 from TesteurManiak/features/channels_endpoints
Browse files Browse the repository at this point in the history
finished modify channel information & get channel editors
  • Loading branch information
TesteurManiak authored Jun 24, 2021
2 parents 1dbcc1e + 6d5a7c3 commit b5af4d5
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 14 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Future<TwitchToken> _openConnectionPage({List<TwitchApiScope> scopes = const []}

Now you are ready to use the methods implemented in [Supported Features](#supported-features) section.

## Supported Features
## Supported Endpoints (16/56)

### Ads

Expand All @@ -76,7 +76,7 @@ Now you are ready to use the methods implemented in [Supported Features](#suppor
### Channels

* [x] Get Channel Information
* [ ] Modify Channel Information
* [x] Modify Channel Information
* [ ] Get Channel Editors

### Channel Points
Expand Down
30 changes: 22 additions & 8 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,8 @@ class _MyHomePageState extends State<MyHomePage> {

if (_twitchClient.accessToken == null) {
WidgetsBinding.instance.scheduleFrameCallback((_) {
_openConnectionPage(scopes: [
TwitchApiScope.channelEditCommercial,
TwitchApiScope.analyticsReadExtensions,
TwitchApiScope.analyticsReadGames,
TwitchApiScope.userReadEmail,
TwitchApiScope.channelReadSubscriptions,
TwitchApiScope.bitsRead,
]).then((value) => setState(() {}));
_openConnectionPage(scopes: TwitchApiScope.values)
.then((value) => setState(() {}));
});
}
}
Expand Down Expand Up @@ -237,6 +231,26 @@ class _MyHomePageState extends State<MyHomePage> {
),
child: const Text('Get Cheermotes'),
),
ElevatedButton(
onPressed: () => _twitchClient.modifyChannelinformation(
broadcasterId: _twitchClient.accessToken.userId,
title: 'Test',
),
child: const Text('Modify your channel title to: Test'),
),
ElevatedButton(
onPressed: () => _twitchClient
.getChannelEditors(
broadcasterId: _twitchClient.accessToken.userId,
)
.then(
(value) => _displayDataAlert(
'You have ${value.data.length} editors',
value.data.map<String>((e) => e.userName).join(', '),
),
),
child: const Text('Get your channel editors'),
)
],
),
);
Expand Down
114 changes: 111 additions & 3 deletions lib/src/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:dio/dio.dart';
import 'package:twitch_api/src/exceptions/twitch_api_exception.dart';
import 'package:twitch_api/src/models/twitch_bits_leaderboard.dart';
import 'package:twitch_api/src/models/twitch_broadcaster_subscription.dart';
import 'package:twitch_api/src/models/twitch_channel_editor.dart';
import 'package:twitch_api/src/models/twitch_channel_info.dart';
import 'package:twitch_api/src/models/twitch_cheermote.dart';
import 'package:twitch_api/src/models/twitch_extension_transaction.dart';
Expand Down Expand Up @@ -36,7 +37,7 @@ class TwitchClient {
TwitchToken? get accessToken => _accessToken;

Uri authorizeUri(List<TwitchApiScope> scopes) {
final scopesSet = <String?>{}
final scopesSet = <String>{}
..add('viewing_activity_read')
..addAll(scopes.map((e) => TwitchApiScopes.getScopeString(e)).toSet());
return oauth2Url.replace(
Expand Down Expand Up @@ -148,6 +149,39 @@ class TwitchClient {
}
}

/// Generic method for authenticated POST calls.
Future<dynamic> patchCall(
Iterable<String> pathSegments,
dynamic data, {
Map<String, dynamic> queryParameters = const {},
}) async {
try {
await validateToken();
if (_accessToken!.isValid) {
final options = Options(headers: {
'Client-Id': clientId,
'Authorization': 'Bearer ${accessToken!.token}',
'Content-Type': 'application/json',
});
final response = await _dio.patchUri(
baseUrl.replace(
pathSegments: <String>[basePath, ...pathSegments],
queryParameters: queryParameters,
),
options: options,
data: data,
);
return response.data;
} else {
throw const TwitchApiException('OAuth token is not valid');
}
} on DioError catch (dioError) {
throw dioError.response!.data['message'] as String;
} catch (e) {
rethrow;
}
}

void close() {}

void dispose() {
Expand Down Expand Up @@ -639,8 +673,82 @@ class TwitchClient {
if (id != null) queryParameters['id'] = id;
if (after != null) queryParameters['after'] = after;

final data = await getCall(['extensions', 'transactions'],
queryParameters: queryParameters);
final data = await getCall(
['extensions', 'transactions'],
queryParameters: queryParameters,
);
return TwitchResponse.extensionTransaction(data as Map<String, dynamic>);
}

/// Modifies channel information for users.
///
/// Required scope: [TwitchApiScope.channelManageBroadcast]
///
/// `broadcasterId`: ID of the channel to be updated
///
/// At least one of those parameter must be provided:
///
/// `gameId`: The current game ID being played on the channel. Use `"0"` or
/// `""` (an empty string) to unset the game.
///
/// `broadcasterLanguage`: The language of the channel. A language value must
/// be either the ISO 639-1 two-letter code for a supported stream language or
/// "other".
///
/// `title`: The title of the stream. Value must not be an empty string.
///
/// `delay`: Stream delay in seconds. Stream delay is a Twitch Partner
/// feature; trying to set this value for other account types will return a
/// 400 error.
Future<void> modifyChannelinformation({
required String broadcasterId,
String? gameId,
String? broadcasterLanguage,
String? title,
int? delay,
}) async {
assert(
gameId != null ||
broadcasterLanguage != null ||
title != null ||
delay != null,
'At least one optional parameter must be provided.',
);
if (broadcasterLanguage != null) {
assert(broadcasterLanguage == 'other' || broadcasterLanguage.length == 2);
}
if (title != null) {
assert(title.isNotEmpty, 'The title must not be an empty string.');
}
if (delay != null) assert(delay > 0);

final data = <String, dynamic>{};
if (gameId != null) data['game_id'] = gameId;
if (broadcasterLanguage != null) {
data['broadcaster_language'] = broadcasterLanguage;
}
if (title != null) data['title'] = title;
if (delay != null) data['delay'] = delay;

await patchCall(
['channels'],
data,
queryParameters: {'broadcaster_id': broadcasterId},
);
}

/// Gets a list of users who have editor permissions for a specific channel.
///
/// Required scope: [TwitchApiScope.channelReadEditors]
///
/// `broadcasterId`: Broadcaster’s user ID associated with the channel.
Future<TwitchResponse<TwitchChannelEditor>> getChannelEditors({
required String broadcasterId,
}) async {
final data = await getCall(
['channels', 'editors'],
queryParameters: {'broadcaster_id': broadcasterId},
);
return TwitchResponse.channelEditor(data as Map<String, dynamic>);
}
}
6 changes: 5 additions & 1 deletion lib/src/models/twitch_api_scopes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ enum TwitchApiScope {
analyticsReadGames,
bitsRead,
channelEditCommercial,
channelManageBroadcast,
channelReadEditors,
channelReadSubscriptions,
userReadEmail,
}
Expand All @@ -13,9 +15,11 @@ class TwitchApiScopes {
TwitchApiScope.analyticsReadGames: 'analytics:read:games',
TwitchApiScope.bitsRead: 'bits:read',
TwitchApiScope.channelEditCommercial: 'channel:edit:commercial',
TwitchApiScope.channelManageBroadcast: 'channel:manage:broadcast',
TwitchApiScope.channelReadEditors: 'channel:read:editors',
TwitchApiScope.channelReadSubscriptions: 'channel:read:subscriptions',
TwitchApiScope.userReadEmail: 'user:read:email',
};

static String? getScopeString(TwitchApiScope scope) => _scopes[scope];
static String getScopeString(TwitchApiScope scope) => _scopes[scope]!;
}
24 changes: 24 additions & 0 deletions lib/src/models/twitch_channel_editor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/// User who have editor permissions
class TwitchChannelEditor {
/// User ID of the editor.
final String userId;

/// Display name of the editor.
final String userName;

/// Date and time the editor was given editor permissions.
final DateTime createdAt;

TwitchChannelEditor({
required this.userId,
required this.userName,
required this.createdAt,
});

factory TwitchChannelEditor.fromJson(Map<String, dynamic> json) =>
TwitchChannelEditor(
userId: json['user_id'] as String,
userName: json['user_name'] as String,
createdAt: DateTime.parse(json['created_at'] as String),
);
}
9 changes: 9 additions & 0 deletions lib/src/models/twitch_response.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:twitch_api/src/models/twitch_bits_leaderboard.dart';
import 'package:twitch_api/src/models/twitch_broadcaster_subscription.dart';
import 'package:twitch_api/src/models/twitch_channel_editor.dart';
import 'package:twitch_api/src/models/twitch_channel_info.dart';
import 'package:twitch_api/src/models/twitch_date_range.dart';
import 'package:twitch_api/src/models/twitch_extension_analytic.dart';
Expand Down Expand Up @@ -181,4 +182,12 @@ class TwitchResponse<T> {
as T)
.toList(),
);

factory TwitchResponse.channelEditor(Map<String, dynamic> json) =>
TwitchResponse(
data: (json['data'] as Iterable)
.map<T>((e) =>
TwitchChannelEditor.fromJson(e as Map<String, dynamic>) as T)
.toList(),
);
}
14 changes: 14 additions & 0 deletions test/parsing_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:io';

import 'package:test/test.dart';
import 'package:twitch_api/src/models/twitch_channel_editor.dart';
import 'package:twitch_api/src/models/twitch_extension_transaction.dart';
import 'package:twitch_api/src/models/twitch_response.dart';
import 'package:twitch_api/twitch_api.dart';
Expand Down Expand Up @@ -102,5 +103,18 @@ void main() {
expect(twitchStreamInfo.isMature, false);
});
});

group('Get Channel Editors', () {
test('1', () {
final json = jsonDecode(readFileString('get_channel_editors.json'))
as Map<String, dynamic>;
final obj = TwitchResponse<TwitchChannelEditor>.channelEditor(json);
expect(obj.data!.length, 2);

final channelEditor = obj.data!.first;
expect(channelEditor.userId, '182891647');
expect(channelEditor.userName, 'mauerbac');
});
});
});
}
14 changes: 14 additions & 0 deletions test/test_resources/get_channel_editors.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"data": [
{
"user_id": "182891647",
"user_name": "mauerbac",
"created_at": "2019-02-15T21:19:50.380833Z"
},
{
"user_id": "135093069",
"user_name": "BlueLava",
"created_at": "2018-03-07T16:28:29.872937Z"
}
]
}

0 comments on commit b5af4d5

Please sign in to comment.