diff --git a/CHANGELOG.md b/CHANGELOG.md index f351265..0cd4318 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Release Note +- Added `domain_blocks API methods`. ([#12](https://github.com/mastodon-dart/mastodon-api/issues/12)) + - `GET /api/v1/domain_blocks` + - `POST /api/v1/domain_blocks` + - `DELETE /api/v1/domain_blocks` + ## v0.4.0 - Added endpoints in `StatusesV1Service`. ([#23](https://github.com/mastodon-dart/mastodon-api/issues/23)) diff --git a/lib/src/core/client/client_context.dart b/lib/src/core/client/client_context.dart index 49934db..50a7a3f 100644 --- a/lib/src/core/client/client_context.dart +++ b/lib/src/core/client/client_context.dart @@ -53,8 +53,9 @@ abstract class ClientContext { Future delete( UserContext userContext, - Uri uri, - ); + Uri uri, { + dynamic body, + }); Future put( UserContext userContext, @@ -172,11 +173,16 @@ class _ClientContext implements ClientContext { @override Future delete( UserContext userContext, - Uri uri, - ) async => + Uri uri, { + dynamic body, + }) async => await _challengeWithRetryIfNecessary( _clientResolver.execute(userContext), - (client) async => await client.delete(uri, timeout: timeout), + (client) async => await client.delete( + uri, + body: body, + timeout: timeout, + ), ); @override diff --git a/lib/src/core/service_helper.dart b/lib/src/core/service_helper.dart index aaea6f8..b8cf0db 100644 --- a/lib/src/core/service_helper.dart +++ b/lib/src/core/service_helper.dart @@ -189,11 +189,13 @@ class ServiceHelper implements Service { Future delete( UserContext userContext, final String unencodedPath, { + dynamic body = const {}, http.Response Function(http.Response response)? validate, }) async { final response = await _context.delete( userContext, Uri.https(_authority, unencodedPath), + body: body, ); return validate != null ? validate(response) : response; diff --git a/lib/src/service/base_service.dart b/lib/src/service/base_service.dart index 0c4ce2e..1e1426b 100644 --- a/lib/src/service/base_service.dart +++ b/lib/src/service/base_service.dart @@ -159,11 +159,13 @@ abstract class BaseService implements _Service { Future delete( UserContext userContext, final String unencodedPath, { + dynamic body = const {}, bool checkUnprocessableEntity = false, }) async => await _helper.delete( userContext, unencodedPath, + body: body, validate: ((response) => checkResponse( response, checkUnprocessableEntity, diff --git a/lib/src/service/entities/notification.freezed.dart b/lib/src/service/entities/notification.freezed.dart index e2595a4..311a81a 100644 --- a/lib/src/service/entities/notification.freezed.dart +++ b/lib/src/service/entities/notification.freezed.dart @@ -32,8 +32,9 @@ mixin _$Notification { /// The account that performed the action that generated the notification. Account get account => throw _privateConstructorUsedError; - /// Status that was the object of the notification. Attached when type of the - /// notification is favourite, reblog, status, mention, poll, or update. + /// Status that was the object of the notification. Attached when type of + /// the notification is favourite, reblog, status, mention, poll, + /// or update. Status? get status => throw _privateConstructorUsedError; // Report that was the object of the notification. // Attached when type of the notification is admin.report. @@ -247,8 +248,9 @@ class _$_Notification implements _Notification { @override final Account account; - /// Status that was the object of the notification. Attached when type of the - /// notification is favourite, reblog, status, mention, poll, or update. + /// Status that was the object of the notification. Attached when type of + /// the notification is favourite, reblog, status, mention, poll, + /// or update. @override final Status? status; // Report that was the object of the notification. @@ -324,8 +326,9 @@ abstract class _Notification implements Notification { Account get account; @override - /// Status that was the object of the notification. Attached when type of the - /// notification is favourite, reblog, status, mention, poll, or update. + /// Status that was the object of the notification. Attached when type of + /// the notification is favourite, reblog, status, mention, poll, + /// or update. Status? get status; @override // Report that was the object of the notification. // Attached when type of the notification is admin.report. diff --git a/lib/src/service/v1/accounts/accounts_v1_service.dart b/lib/src/service/v1/accounts/accounts_v1_service.dart index 3d13810..718c14c 100644 --- a/lib/src/service/v1/accounts/accounts_v1_service.dart +++ b/lib/src/service/v1/accounts/accounts_v1_service.dart @@ -1197,6 +1197,92 @@ abstract class AccountsV1Service { Future>> lookupBookmarkedStatuses({ int? limit, }); + + /// View domains the user has blocked. + /// + /// ## Parameters + /// + /// - [limit]: Maximum number of results to return. Defaults to 100 domain + /// blocks. Max 200 domain blocks. + /// + /// ## Endpoint Url + /// + /// - GET /api/v1/domain_blocks HTTP/1.1 + /// + /// ## Authentication Methods + /// + /// - OAuth 2.0 + /// + /// ## Required Scopes + /// + /// - follow + /// - read:blocks + /// + /// ## Reference + /// + /// - https://docs.joinmastodon.org/methods/domain_blocks/#get + Future>> lookupBlockedDomains({ + int? limit, + }); + + /// Block a domain to: + /// + /// - hide all public posts from it + /// - hide all notifications from it + /// - remove all followers from it + /// - prevent following new users from it (but does not remove existing + /// follows) + /// + /// ## Parameters + /// + /// - [domainName]: Domain to block. + /// + /// ## Endpoint Url + /// + /// - POST /api/v1/domain_blocks HTTP/1.1 + /// + /// ## Authentication Methods + /// + /// - OAuth 2.0 + /// + /// ## Required Scopes + /// + /// - follow + /// - write:blocks + /// + /// ## Reference + /// + /// - https://docs.joinmastodon.org/methods/domain_blocks/#block + Future> createBlockedDomain({ + required String domainName, + }); + + /// Remove a domain block, if it exists in the user’s array of + /// blocked domains. + /// + /// ## Parameters + /// + /// - [domainName]: Domain to block. + /// + /// ## Endpoint Url + /// + /// - DELETE /api/v1/domain_blocks HTTP/1.1 + /// + /// ## Authentication Methods + /// + /// - OAuth 2.0 + /// + /// ## Required Scopes + /// + /// - follow + /// - write:blocks + /// + /// ## Reference + /// + /// - https://docs.joinmastodon.org/methods/domain_blocks/#unblock + Future> destroyBlockedDomain({ + required String domainName, + }); } class _AccountsV1Service extends BaseService implements AccountsV1Service { @@ -1854,4 +1940,43 @@ class _AccountsV1Service extends BaseService implements AccountsV1Service { ), dataBuilder: Status.fromJson, ); + + @override + Future>> lookupBlockedDomains({ + int? limit, + }) async => + super.transformMultiRawDataResponse( + await super.get( + UserContext.oauth2Only, + '/api/v1/domain_blocks', + queryParameters: { + 'limit': limit, + }, + ), + ); + + @override + Future> createBlockedDomain({ + required String domainName, + }) async => + super.evaluateResponse( + await super.post( + UserContext.oauth2Only, + '/api/v1/domain_blocks', + body: { + 'domain': domainName, + }, + ), + ); + + @override + Future> destroyBlockedDomain({ + required String domainName, + }) async => + super.evaluateResponse( + await super.delete( + UserContext.oauth2Only, + '/api/v1/domain_blocks', + ), + ); } diff --git a/pubspec.yaml b/pubspec.yaml index fb45b4c..64471c5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: mastodon_api description: The easiest and powerful Dart/Flutter library for Mastodon API. -version: 0.4.0 +version: 0.4.1 repository: https://github.com/mastodon-dart/mastodon-api issue_tracker: https://github.com/mastodon-dart/mastodon-api/issues diff --git a/test/mocks/client_context_stubs.dart b/test/mocks/client_context_stubs.dart index 2edfdb4..5c9802a 100644 --- a/test/mocks/client_context_stubs.dart +++ b/test/mocks/client_context_stubs.dart @@ -104,6 +104,7 @@ MockClientContext buildDeleteStub( when(mockClientContext.delete( UserContext.oauth2Only, Uri.https(instance, unencodedPath), + body: anyNamed('body'), )).thenAnswer( (_) async => Response( await File(resourcePath).readAsString(), diff --git a/test/mocks/mock.mocks.dart b/test/mocks/mock.mocks.dart index 61bd182..601341b 100644 --- a/test/mocks/mock.mocks.dart +++ b/test/mocks/mock.mocks.dart @@ -142,8 +142,9 @@ class MockClientContext extends _i1.Mock implements _i3.ClientContext { @override _i4.Future<_i2.Response> delete( _i5.UserContext? userContext, - Uri? uri, - ) => + Uri? uri, { + dynamic body, + }) => (super.noSuchMethod( Invocation.method( #delete, @@ -151,6 +152,7 @@ class MockClientContext extends _i1.Mock implements _i3.ClientContext { userContext, uri, ], + {#body: body}, ), returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_0( this, @@ -160,6 +162,7 @@ class MockClientContext extends _i1.Mock implements _i3.ClientContext { userContext, uri, ], + {#body: body}, ), )), ) as _i4.Future<_i2.Response>); diff --git a/test/src/service/v1/accounts/accounts_v1_service_test.dart b/test/src/service/v1/accounts/accounts_v1_service_test.dart index f53f06b..17db7ba 100644 --- a/test/src/service/v1/accounts/accounts_v1_service_test.dart +++ b/test/src/service/v1/accounts/accounts_v1_service_test.dart @@ -2633,4 +2633,232 @@ void main() { ); }); }); + + group('.lookupBlockedDomains', () { + test('normal case', () async { + final accountsService = AccountsV1Service( + instance: 'test', + context: context.buildGetStub( + 'test', + UserContext.oauth2Only, + '/api/v1/domain_blocks', + 'test/src/service/v1/accounts/data/lookup_blocked_domains.json', + { + 'limit': '40', + }, + ), + ); + + final response = await accountsService.lookupBlockedDomains( + limit: 40, + ); + + expect(response, isA()); + expect(response.rateLimit, isA()); + expect(response.data, isA>()); + expect(response.data, ['nsfw.social', 'artalley.social']); + }); + + test('when unauthorized', () async { + final accountsService = AccountsV1Service( + instance: 'test', + context: context.buildGetStub( + 'test', + UserContext.oauth2Only, + '/api/v1/domain_blocks', + 'test/src/service/v1/accounts/data/lookup_blocked_domains.json', + { + 'limit': '40', + }, + statusCode: 401, + ), + ); + + expectUnauthorizedException( + () async => await accountsService.lookupBlockedDomains( + limit: 40, + ), + ); + }); + + test('when rate limit exceeded', () async { + final accountsService = AccountsV1Service( + instance: 'test', + context: context.buildGetStub( + 'test', + UserContext.oauth2Only, + '/api/v1/domain_blocks', + 'test/src/service/v1/accounts/data/lookup_blocked_domains.json', + { + 'limit': '40', + }, + statusCode: 429, + ), + ); + + expectRateLimitExceededException( + () async => await accountsService.lookupBlockedDomains( + limit: 40, + ), + ); + }); + }); + + group('.createBlockedDomain', () { + test('normal case', () async { + final accountsService = AccountsV1Service( + instance: 'test', + context: context.buildPostStub( + 'test', + UserContext.oauth2Only, + '/api/v1/domain_blocks', + 'test/src/service/v1/accounts/data/create_blocked_domain.json', + ), + ); + + final response = await accountsService.createBlockedDomain( + domainName: 'test.com', + ); + + expect(response, isA()); + expect(response.rateLimit, isA()); + expect(response.data, isTrue); + }); + + test('when unauthorized', () async { + final accountsService = AccountsV1Service( + instance: 'test', + context: context.buildPostStub( + 'test', + UserContext.oauth2Only, + '/api/v1/domain_blocks', + 'test/src/service/v1/accounts/data/create_blocked_domain.json', + statusCode: 401, + ), + ); + + expectUnauthorizedException( + () async => await accountsService.createBlockedDomain( + domainName: 'test.com', + ), + ); + }); + + test('when rate limit exceeded', () async { + final accountsService = AccountsV1Service( + instance: 'test', + context: context.buildPostStub( + 'test', + UserContext.oauth2Only, + '/api/v1/domain_blocks', + 'test/src/service/v1/accounts/data/create_blocked_domain.json', + statusCode: 429, + ), + ); + + expectRateLimitExceededException( + () async => await accountsService.createBlockedDomain( + domainName: 'test.com', + ), + ); + }); + + test('when parameters are invalid', () async { + final accountsService = AccountsV1Service( + instance: 'test', + context: context.buildPostStub( + 'test', + UserContext.oauth2Only, + '/api/v1/domain_blocks', + 'test/src/service/v1/accounts/data/create_blocked_domain.json', + statusCode: 422, + ), + ); + + final response = await accountsService.createBlockedDomain( + domainName: 'test.com', + ); + + expect(response, isA()); + expect(response.rateLimit, isA()); + expect(response.data, isFalse); + }); + }); + + group('.destroyBlockedDomain', () { + test('normal case', () async { + final accountsService = AccountsV1Service( + instance: 'test', + context: context.buildDeleteStub( + 'test', + '/api/v1/domain_blocks', + 'test/src/service/v1/accounts/data/destroy_blocked_domain.json', + ), + ); + + final response = await accountsService.destroyBlockedDomain( + domainName: 'test.com', + ); + + expect(response, isA()); + expect(response.rateLimit, isA()); + expect(response.data, isTrue); + }); + + test('when unauthorized', () async { + final accountsService = AccountsV1Service( + instance: 'test', + context: context.buildDeleteStub( + 'test', + '/api/v1/domain_blocks', + 'test/src/service/v1/accounts/data/destroy_blocked_domain.json', + statusCode: 401, + ), + ); + + expectUnauthorizedException( + () async => await accountsService.destroyBlockedDomain( + domainName: 'test.com', + ), + ); + }); + + test('when rate limit exceeded', () async { + final accountsService = AccountsV1Service( + instance: 'test', + context: context.buildDeleteStub( + 'test', + '/api/v1/domain_blocks', + 'test/src/service/v1/accounts/data/destroy_blocked_domain.json', + statusCode: 429, + ), + ); + + expectRateLimitExceededException( + () async => await accountsService.destroyBlockedDomain( + domainName: 'test.com', + ), + ); + }); + + test('when parameters are invalid', () async { + final accountsService = AccountsV1Service( + instance: 'test', + context: context.buildDeleteStub( + 'test', + '/api/v1/domain_blocks', + 'test/src/service/v1/accounts/data/destroy_blocked_domain.json', + statusCode: 422, + ), + ); + + final response = await accountsService.destroyBlockedDomain( + domainName: 'test.com', + ); + + expect(response, isA()); + expect(response.rateLimit, isA()); + expect(response.data, isFalse); + }); + }); } diff --git a/test/src/service/v1/accounts/data/create_blocked_domain.json b/test/src/service/v1/accounts/data/create_blocked_domain.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/test/src/service/v1/accounts/data/create_blocked_domain.json @@ -0,0 +1 @@ +{} diff --git a/test/src/service/v1/accounts/data/destroy_blocked_domain.json b/test/src/service/v1/accounts/data/destroy_blocked_domain.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/test/src/service/v1/accounts/data/destroy_blocked_domain.json @@ -0,0 +1 @@ +{} diff --git a/test/src/service/v1/accounts/data/lookup_blocked_domains.json b/test/src/service/v1/accounts/data/lookup_blocked_domains.json new file mode 100644 index 0000000..769adb5 --- /dev/null +++ b/test/src/service/v1/accounts/data/lookup_blocked_domains.json @@ -0,0 +1 @@ +["nsfw.social", "artalley.social"]