diff --git a/CHANGELOG.md b/CHANGELOG.md index b3a5676b..ba0b82f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Release Note +## v4.8.1 + +- Supported `GET users/profile_banner`. ([#590](https://github.com/twitter-dart/twitter-api-v2/issues/590)) + ## v4.8.0 - Supported `POST account/update_profile_image`. ([#617](https://github.com/twitter-dart/twitter-api-v2/issues/617)) diff --git a/README.md b/README.md index ea08e2c0..4af014b3 100644 --- a/README.md +++ b/README.md @@ -483,12 +483,14 @@ Future main() async { > **Note**
> Twitter API v1.1 endpoint is used because Twitter Official does not yet release the endpoints to manage user profile for Twitter API v2.0. Therefore, this service may be changed in the future. -| Endpoint | Method Name | -| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | -| [POST /1.1/account/update_profile.json](https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile) | [updateProfile](https://pub.dev/documentation/twitter_api_v2/latest/twitter_api_v2/UsersService/updateProfile.html) | -| [POST /1.1/account/update_profile_image.json](https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile_image) | [updateProfileImage](https://pub.dev/documentation/twitter_api_v2/latest/twitter_api_v2/UsersService/updateProfileImage.html) | -| [POST /1.1/account/update_profile_banner.json](https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile_banner) | [updateProfileBanner](https://pub.dev/documentation/twitter_api_v2/latest/twitter_api_v2/UsersService/updateProfileBanner.html) | -| [POST /1.1/account/remove_profile_banner.json](https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/api-reference/post-account-remove_profile_banner) | [destroyProfileBanner](https://pub.dev/documentation/twitter_api_v2/latest/twitter_api_v2/UsersService/destroyProfileBanner.html) | +| Endpoint | Method Name | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | +| [POST /1.1/account/update_profile.json](https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile) | [updateProfile](https://pub.dev/documentation/twitter_api_v2/latest/twitter_api_v2/UsersService/updateProfile.html) | +| [POST /1.1/account/update_profile_image.json](https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile_image) | [updateProfileImage](https://pub.dev/documentation/twitter_api_v2/latest/twitter_api_v2/UsersService/updateProfileImage.html) | +| [POST /1.1/account/update_profile_banner.json](https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile_banner) | [updateProfileBanner](https://pub.dev/documentation/twitter_api_v2/latest/twitter_api_v2/UsersService/updateProfileBanner.html) | +| [POST /1.1/account/remove_profile_banner.json](https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/api-reference/post-account-remove_profile_banner) | [destroyProfileBanner](https://pub.dev/documentation/twitter_api_v2/latest/twitter_api_v2/UsersService/destroyProfileBanner.html) | +| [GET users/profile_banner.json](https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/api-reference/get-users-profile_banner) | [lookupProfileBannerById](https://pub.dev/documentation/twitter_api_v2/latest/twitter_api_v2/UsersService/lookupProfileBannerById.html) | +| [GET users/profile_banner.json](https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/api-reference/get-users-profile_banner) | [lookupProfileBannerByName](https://pub.dev/documentation/twitter_api_v2/latest/twitter_api_v2/UsersService/lookupProfileBannerByName.html) | #### 1.3.2.6. Report Spam diff --git a/lib/src/core/adaptor/profile_banner_object_adaptor.dart b/lib/src/core/adaptor/profile_banner_object_adaptor.dart new file mode 100644 index 00000000..6d665c74 --- /dev/null +++ b/lib/src/core/adaptor/profile_banner_object_adaptor.dart @@ -0,0 +1,16 @@ +// Copyright 2023 Kato Shinya. All rights reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided the conditions. + +// 🌎 Project imports: +import 'object_adaptor.dart'; + +class ProfileBannerObjectAdaptor implements ObjectAdaptor { + /// Returns the new instance of [ProfileBannerObjectAdaptor]. + const ProfileBannerObjectAdaptor(); + + @override + Map execute(final Map source) => { + 'data': source['sizes'], + }; +} diff --git a/lib/src/service/base_service.dart b/lib/src/service/base_service.dart index 1ed1b3ed..1b773e1a 100644 --- a/lib/src/service/base_service.dart +++ b/lib/src/service/base_service.dart @@ -494,6 +494,13 @@ abstract class BaseService implements _Service { ); } + if (response.statusCode == 404) { + throw DataNotFoundException( + 'No data exists in response.', + response, + ); + } + if (response.statusCode == 429) { throw RateLimitExceededException( 'Rate limit exceeded.', diff --git a/lib/src/service/users/profile_banner_variant.dart b/lib/src/service/users/profile_banner_variant.dart new file mode 100644 index 00000000..2803a0cd --- /dev/null +++ b/lib/src/service/users/profile_banner_variant.dart @@ -0,0 +1,28 @@ +// Copyright 2023 Kato Shinya. All rights reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided the conditions. + +// ignore_for_file: invalid_annotation_target + +// 📦 Package imports: +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'profile_banner_variant.freezed.dart'; +part 'profile_banner_variant.g.dart'; + +@freezed +class ProfileBannerVariant with _$ProfileBannerVariant { + const factory ProfileBannerVariant({ + /// The height of this image. + @JsonKey(name: 'h') required int height, + + /// The height of this image. + @JsonKey(name: 'w') required int width, + + /// The url of this image. + required String url, + }) = _ProfileBannerVariant; + + factory ProfileBannerVariant.fromJson(Map json) => + _$ProfileBannerVariantFromJson(json); +} diff --git a/lib/src/service/users/profile_banner_variant.freezed.dart b/lib/src/service/users/profile_banner_variant.freezed.dart new file mode 100644 index 00000000..80ed11fa --- /dev/null +++ b/lib/src/service/users/profile_banner_variant.freezed.dart @@ -0,0 +1,219 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'profile_banner_variant.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +ProfileBannerVariant _$ProfileBannerVariantFromJson(Map json) { + return _ProfileBannerVariant.fromJson(json); +} + +/// @nodoc +mixin _$ProfileBannerVariant { + /// The height of this image. + @JsonKey(name: 'h') + int get height => throw _privateConstructorUsedError; + + /// The height of this image. + @JsonKey(name: 'w') + int get width => throw _privateConstructorUsedError; + + /// The url of this image. + String get url => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $ProfileBannerVariantCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ProfileBannerVariantCopyWith<$Res> { + factory $ProfileBannerVariantCopyWith(ProfileBannerVariant value, + $Res Function(ProfileBannerVariant) then) = + _$ProfileBannerVariantCopyWithImpl<$Res, ProfileBannerVariant>; + @useResult + $Res call( + {@JsonKey(name: 'h') int height, + @JsonKey(name: 'w') int width, + String url}); +} + +/// @nodoc +class _$ProfileBannerVariantCopyWithImpl<$Res, + $Val extends ProfileBannerVariant> + implements $ProfileBannerVariantCopyWith<$Res> { + _$ProfileBannerVariantCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? height = null, + Object? width = null, + Object? url = null, + }) { + return _then(_value.copyWith( + height: null == height + ? _value.height + : height // ignore: cast_nullable_to_non_nullable + as int, + width: null == width + ? _value.width + : width // ignore: cast_nullable_to_non_nullable + as int, + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$_ProfileBannerVariantCopyWith<$Res> + implements $ProfileBannerVariantCopyWith<$Res> { + factory _$$_ProfileBannerVariantCopyWith(_$_ProfileBannerVariant value, + $Res Function(_$_ProfileBannerVariant) then) = + __$$_ProfileBannerVariantCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {@JsonKey(name: 'h') int height, + @JsonKey(name: 'w') int width, + String url}); +} + +/// @nodoc +class __$$_ProfileBannerVariantCopyWithImpl<$Res> + extends _$ProfileBannerVariantCopyWithImpl<$Res, _$_ProfileBannerVariant> + implements _$$_ProfileBannerVariantCopyWith<$Res> { + __$$_ProfileBannerVariantCopyWithImpl(_$_ProfileBannerVariant _value, + $Res Function(_$_ProfileBannerVariant) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? height = null, + Object? width = null, + Object? url = null, + }) { + return _then(_$_ProfileBannerVariant( + height: null == height + ? _value.height + : height // ignore: cast_nullable_to_non_nullable + as int, + width: null == width + ? _value.width + : width // ignore: cast_nullable_to_non_nullable + as int, + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_ProfileBannerVariant implements _ProfileBannerVariant { + const _$_ProfileBannerVariant( + {@JsonKey(name: 'h') required this.height, + @JsonKey(name: 'w') required this.width, + required this.url}); + + factory _$_ProfileBannerVariant.fromJson(Map json) => + _$$_ProfileBannerVariantFromJson(json); + + /// The height of this image. + @override + @JsonKey(name: 'h') + final int height; + + /// The height of this image. + @override + @JsonKey(name: 'w') + final int width; + + /// The url of this image. + @override + final String url; + + @override + String toString() { + return 'ProfileBannerVariant(height: $height, width: $width, url: $url)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_ProfileBannerVariant && + (identical(other.height, height) || other.height == height) && + (identical(other.width, width) || other.width == width) && + (identical(other.url, url) || other.url == url)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, height, width, url); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_ProfileBannerVariantCopyWith<_$_ProfileBannerVariant> get copyWith => + __$$_ProfileBannerVariantCopyWithImpl<_$_ProfileBannerVariant>( + this, _$identity); + + @override + Map toJson() { + return _$$_ProfileBannerVariantToJson( + this, + ); + } +} + +abstract class _ProfileBannerVariant implements ProfileBannerVariant { + const factory _ProfileBannerVariant( + {@JsonKey(name: 'h') required final int height, + @JsonKey(name: 'w') required final int width, + required final String url}) = _$_ProfileBannerVariant; + + factory _ProfileBannerVariant.fromJson(Map json) = + _$_ProfileBannerVariant.fromJson; + + @override + + /// The height of this image. + @JsonKey(name: 'h') + int get height; + @override + + /// The height of this image. + @JsonKey(name: 'w') + int get width; + @override + + /// The url of this image. + String get url; + @override + @JsonKey(ignore: true) + _$$_ProfileBannerVariantCopyWith<_$_ProfileBannerVariant> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/src/service/users/profile_banner_variant.g.dart b/lib/src/service/users/profile_banner_variant.g.dart new file mode 100644 index 00000000..274c47d4 --- /dev/null +++ b/lib/src/service/users/profile_banner_variant.g.dart @@ -0,0 +1,32 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: non_constant_identifier_names + +part of 'profile_banner_variant.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$_ProfileBannerVariant _$$_ProfileBannerVariantFromJson(Map json) => + $checkedCreate( + r'_$_ProfileBannerVariant', + json, + ($checkedConvert) { + final val = _$_ProfileBannerVariant( + height: $checkedConvert('h', (v) => v as int), + width: $checkedConvert('w', (v) => v as int), + url: $checkedConvert('url', (v) => v as String), + ); + return val; + }, + fieldKeyMap: const {'height': 'h', 'width': 'w'}, + ); + +Map _$$_ProfileBannerVariantToJson( + _$_ProfileBannerVariant instance) => + { + 'h': instance.height, + 'w': instance.width, + 'url': instance.url, + }; diff --git a/lib/src/service/users/profile_banner_variants_data.dart b/lib/src/service/users/profile_banner_variants_data.dart new file mode 100644 index 00000000..51f0a9b5 --- /dev/null +++ b/lib/src/service/users/profile_banner_variants_data.dart @@ -0,0 +1,41 @@ +// Copyright 2023 Kato Shinya. All rights reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided the conditions. + +// 📦 Package imports: +import 'package:freezed_annotation/freezed_annotation.dart'; + +// 🌎 Project imports: +import '../common/data.dart'; +import 'profile_banner_variant.dart'; + +part 'profile_banner_variants_data.freezed.dart'; +part 'profile_banner_variants_data.g.dart'; + +@freezed +class ProfileBannerVariantsData + with _$ProfileBannerVariantsData + implements Data { + const factory ProfileBannerVariantsData({ + /// The banner image for ipad. + required ProfileBannerVariant ipad, + + /// The banner image for ipad retina. + required ProfileBannerVariant ipadRetina, + + /// The banner image for web. + required ProfileBannerVariant web, + + /// The banner image for web retina. + required ProfileBannerVariant webRetina, + + /// The banner image for mobile. + required ProfileBannerVariant mobile, + + /// The banner image for mobile retina. + required ProfileBannerVariant mobileRetina, + }) = _ProfileBannerVariantsData; + + factory ProfileBannerVariantsData.fromJson(Map json) => + _$ProfileBannerVariantsDataFromJson(json); +} diff --git a/lib/src/service/users/profile_banner_variants_data.freezed.dart b/lib/src/service/users/profile_banner_variants_data.freezed.dart new file mode 100644 index 00000000..a9bdd5f5 --- /dev/null +++ b/lib/src/service/users/profile_banner_variants_data.freezed.dart @@ -0,0 +1,368 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'profile_banner_variants_data.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +ProfileBannerVariantsData _$ProfileBannerVariantsDataFromJson( + Map json) { + return _ProfileBannerVariantsData.fromJson(json); +} + +/// @nodoc +mixin _$ProfileBannerVariantsData { + /// The banner image for ipad. + ProfileBannerVariant get ipad => throw _privateConstructorUsedError; + + /// The banner image for ipad retina. + ProfileBannerVariant get ipadRetina => throw _privateConstructorUsedError; + + /// The banner image for web. + ProfileBannerVariant get web => throw _privateConstructorUsedError; + + /// The banner image for web retina. + ProfileBannerVariant get webRetina => throw _privateConstructorUsedError; + + /// The banner image for mobile. + ProfileBannerVariant get mobile => throw _privateConstructorUsedError; + + /// The banner image for mobile retina. + ProfileBannerVariant get mobileRetina => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $ProfileBannerVariantsDataCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ProfileBannerVariantsDataCopyWith<$Res> { + factory $ProfileBannerVariantsDataCopyWith(ProfileBannerVariantsData value, + $Res Function(ProfileBannerVariantsData) then) = + _$ProfileBannerVariantsDataCopyWithImpl<$Res, ProfileBannerVariantsData>; + @useResult + $Res call( + {ProfileBannerVariant ipad, + ProfileBannerVariant ipadRetina, + ProfileBannerVariant web, + ProfileBannerVariant webRetina, + ProfileBannerVariant mobile, + ProfileBannerVariant mobileRetina}); + + $ProfileBannerVariantCopyWith<$Res> get ipad; + $ProfileBannerVariantCopyWith<$Res> get ipadRetina; + $ProfileBannerVariantCopyWith<$Res> get web; + $ProfileBannerVariantCopyWith<$Res> get webRetina; + $ProfileBannerVariantCopyWith<$Res> get mobile; + $ProfileBannerVariantCopyWith<$Res> get mobileRetina; +} + +/// @nodoc +class _$ProfileBannerVariantsDataCopyWithImpl<$Res, + $Val extends ProfileBannerVariantsData> + implements $ProfileBannerVariantsDataCopyWith<$Res> { + _$ProfileBannerVariantsDataCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? ipad = null, + Object? ipadRetina = null, + Object? web = null, + Object? webRetina = null, + Object? mobile = null, + Object? mobileRetina = null, + }) { + return _then(_value.copyWith( + ipad: null == ipad + ? _value.ipad + : ipad // ignore: cast_nullable_to_non_nullable + as ProfileBannerVariant, + ipadRetina: null == ipadRetina + ? _value.ipadRetina + : ipadRetina // ignore: cast_nullable_to_non_nullable + as ProfileBannerVariant, + web: null == web + ? _value.web + : web // ignore: cast_nullable_to_non_nullable + as ProfileBannerVariant, + webRetina: null == webRetina + ? _value.webRetina + : webRetina // ignore: cast_nullable_to_non_nullable + as ProfileBannerVariant, + mobile: null == mobile + ? _value.mobile + : mobile // ignore: cast_nullable_to_non_nullable + as ProfileBannerVariant, + mobileRetina: null == mobileRetina + ? _value.mobileRetina + : mobileRetina // ignore: cast_nullable_to_non_nullable + as ProfileBannerVariant, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $ProfileBannerVariantCopyWith<$Res> get ipad { + return $ProfileBannerVariantCopyWith<$Res>(_value.ipad, (value) { + return _then(_value.copyWith(ipad: value) as $Val); + }); + } + + @override + @pragma('vm:prefer-inline') + $ProfileBannerVariantCopyWith<$Res> get ipadRetina { + return $ProfileBannerVariantCopyWith<$Res>(_value.ipadRetina, (value) { + return _then(_value.copyWith(ipadRetina: value) as $Val); + }); + } + + @override + @pragma('vm:prefer-inline') + $ProfileBannerVariantCopyWith<$Res> get web { + return $ProfileBannerVariantCopyWith<$Res>(_value.web, (value) { + return _then(_value.copyWith(web: value) as $Val); + }); + } + + @override + @pragma('vm:prefer-inline') + $ProfileBannerVariantCopyWith<$Res> get webRetina { + return $ProfileBannerVariantCopyWith<$Res>(_value.webRetina, (value) { + return _then(_value.copyWith(webRetina: value) as $Val); + }); + } + + @override + @pragma('vm:prefer-inline') + $ProfileBannerVariantCopyWith<$Res> get mobile { + return $ProfileBannerVariantCopyWith<$Res>(_value.mobile, (value) { + return _then(_value.copyWith(mobile: value) as $Val); + }); + } + + @override + @pragma('vm:prefer-inline') + $ProfileBannerVariantCopyWith<$Res> get mobileRetina { + return $ProfileBannerVariantCopyWith<$Res>(_value.mobileRetina, (value) { + return _then(_value.copyWith(mobileRetina: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$_ProfileBannerVariantsDataCopyWith<$Res> + implements $ProfileBannerVariantsDataCopyWith<$Res> { + factory _$$_ProfileBannerVariantsDataCopyWith( + _$_ProfileBannerVariantsData value, + $Res Function(_$_ProfileBannerVariantsData) then) = + __$$_ProfileBannerVariantsDataCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {ProfileBannerVariant ipad, + ProfileBannerVariant ipadRetina, + ProfileBannerVariant web, + ProfileBannerVariant webRetina, + ProfileBannerVariant mobile, + ProfileBannerVariant mobileRetina}); + + @override + $ProfileBannerVariantCopyWith<$Res> get ipad; + @override + $ProfileBannerVariantCopyWith<$Res> get ipadRetina; + @override + $ProfileBannerVariantCopyWith<$Res> get web; + @override + $ProfileBannerVariantCopyWith<$Res> get webRetina; + @override + $ProfileBannerVariantCopyWith<$Res> get mobile; + @override + $ProfileBannerVariantCopyWith<$Res> get mobileRetina; +} + +/// @nodoc +class __$$_ProfileBannerVariantsDataCopyWithImpl<$Res> + extends _$ProfileBannerVariantsDataCopyWithImpl<$Res, + _$_ProfileBannerVariantsData> + implements _$$_ProfileBannerVariantsDataCopyWith<$Res> { + __$$_ProfileBannerVariantsDataCopyWithImpl( + _$_ProfileBannerVariantsData _value, + $Res Function(_$_ProfileBannerVariantsData) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? ipad = null, + Object? ipadRetina = null, + Object? web = null, + Object? webRetina = null, + Object? mobile = null, + Object? mobileRetina = null, + }) { + return _then(_$_ProfileBannerVariantsData( + ipad: null == ipad + ? _value.ipad + : ipad // ignore: cast_nullable_to_non_nullable + as ProfileBannerVariant, + ipadRetina: null == ipadRetina + ? _value.ipadRetina + : ipadRetina // ignore: cast_nullable_to_non_nullable + as ProfileBannerVariant, + web: null == web + ? _value.web + : web // ignore: cast_nullable_to_non_nullable + as ProfileBannerVariant, + webRetina: null == webRetina + ? _value.webRetina + : webRetina // ignore: cast_nullable_to_non_nullable + as ProfileBannerVariant, + mobile: null == mobile + ? _value.mobile + : mobile // ignore: cast_nullable_to_non_nullable + as ProfileBannerVariant, + mobileRetina: null == mobileRetina + ? _value.mobileRetina + : mobileRetina // ignore: cast_nullable_to_non_nullable + as ProfileBannerVariant, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_ProfileBannerVariantsData implements _ProfileBannerVariantsData { + const _$_ProfileBannerVariantsData( + {required this.ipad, + required this.ipadRetina, + required this.web, + required this.webRetina, + required this.mobile, + required this.mobileRetina}); + + factory _$_ProfileBannerVariantsData.fromJson(Map json) => + _$$_ProfileBannerVariantsDataFromJson(json); + + /// The banner image for ipad. + @override + final ProfileBannerVariant ipad; + + /// The banner image for ipad retina. + @override + final ProfileBannerVariant ipadRetina; + + /// The banner image for web. + @override + final ProfileBannerVariant web; + + /// The banner image for web retina. + @override + final ProfileBannerVariant webRetina; + + /// The banner image for mobile. + @override + final ProfileBannerVariant mobile; + + /// The banner image for mobile retina. + @override + final ProfileBannerVariant mobileRetina; + + @override + String toString() { + return 'ProfileBannerVariantsData(ipad: $ipad, ipadRetina: $ipadRetina, web: $web, webRetina: $webRetina, mobile: $mobile, mobileRetina: $mobileRetina)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_ProfileBannerVariantsData && + (identical(other.ipad, ipad) || other.ipad == ipad) && + (identical(other.ipadRetina, ipadRetina) || + other.ipadRetina == ipadRetina) && + (identical(other.web, web) || other.web == web) && + (identical(other.webRetina, webRetina) || + other.webRetina == webRetina) && + (identical(other.mobile, mobile) || other.mobile == mobile) && + (identical(other.mobileRetina, mobileRetina) || + other.mobileRetina == mobileRetina)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, ipad, ipadRetina, web, webRetina, mobile, mobileRetina); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_ProfileBannerVariantsDataCopyWith<_$_ProfileBannerVariantsData> + get copyWith => __$$_ProfileBannerVariantsDataCopyWithImpl< + _$_ProfileBannerVariantsData>(this, _$identity); + + @override + Map toJson() { + return _$$_ProfileBannerVariantsDataToJson( + this, + ); + } +} + +abstract class _ProfileBannerVariantsData implements ProfileBannerVariantsData { + const factory _ProfileBannerVariantsData( + {required final ProfileBannerVariant ipad, + required final ProfileBannerVariant ipadRetina, + required final ProfileBannerVariant web, + required final ProfileBannerVariant webRetina, + required final ProfileBannerVariant mobile, + required final ProfileBannerVariant mobileRetina}) = + _$_ProfileBannerVariantsData; + + factory _ProfileBannerVariantsData.fromJson(Map json) = + _$_ProfileBannerVariantsData.fromJson; + + @override + + /// The banner image for ipad. + ProfileBannerVariant get ipad; + @override + + /// The banner image for ipad retina. + ProfileBannerVariant get ipadRetina; + @override + + /// The banner image for web. + ProfileBannerVariant get web; + @override + + /// The banner image for web retina. + ProfileBannerVariant get webRetina; + @override + + /// The banner image for mobile. + ProfileBannerVariant get mobile; + @override + + /// The banner image for mobile retina. + ProfileBannerVariant get mobileRetina; + @override + @JsonKey(ignore: true) + _$$_ProfileBannerVariantsDataCopyWith<_$_ProfileBannerVariantsData> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/src/service/users/profile_banner_variants_data.g.dart b/lib/src/service/users/profile_banner_variants_data.g.dart new file mode 100644 index 00000000..f300b375 --- /dev/null +++ b/lib/src/service/users/profile_banner_variants_data.g.dart @@ -0,0 +1,60 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: non_constant_identifier_names + +part of 'profile_banner_variants_data.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$_ProfileBannerVariantsData _$$_ProfileBannerVariantsDataFromJson(Map json) => + $checkedCreate( + r'_$_ProfileBannerVariantsData', + json, + ($checkedConvert) { + final val = _$_ProfileBannerVariantsData( + ipad: $checkedConvert( + 'ipad', + (v) => ProfileBannerVariant.fromJson( + Map.from(v as Map))), + ipadRetina: $checkedConvert( + 'ipad_retina', + (v) => ProfileBannerVariant.fromJson( + Map.from(v as Map))), + web: $checkedConvert( + 'web', + (v) => ProfileBannerVariant.fromJson( + Map.from(v as Map))), + webRetina: $checkedConvert( + 'web_retina', + (v) => ProfileBannerVariant.fromJson( + Map.from(v as Map))), + mobile: $checkedConvert( + 'mobile', + (v) => ProfileBannerVariant.fromJson( + Map.from(v as Map))), + mobileRetina: $checkedConvert( + 'mobile_retina', + (v) => ProfileBannerVariant.fromJson( + Map.from(v as Map))), + ); + return val; + }, + fieldKeyMap: const { + 'ipadRetina': 'ipad_retina', + 'webRetina': 'web_retina', + 'mobileRetina': 'mobile_retina' + }, + ); + +Map _$$_ProfileBannerVariantsDataToJson( + _$_ProfileBannerVariantsData instance) => + { + 'ipad': instance.ipad.toJson(), + 'ipad_retina': instance.ipadRetina.toJson(), + 'web': instance.web.toJson(), + 'web_retina': instance.webRetina.toJson(), + 'mobile': instance.mobile.toJson(), + 'mobile_retina': instance.mobileRetina.toJson(), + }; diff --git a/lib/src/service/users/user_data.freezed.dart b/lib/src/service/users/user_data.freezed.dart index 67099e1b..ebdd4c8a 100644 --- a/lib/src/service/users/user_data.freezed.dart +++ b/lib/src/service/users/user_data.freezed.dart @@ -35,7 +35,7 @@ mixin _$UserData { /// The Twitter screen name, handle, or alias that this user identifies /// themselves with. Usernames are unique but subject to change. Typically - /// a maximum of 15 characters long, but some historical accounts may exist + /// a maximum of 15 characters long, but some historical accounts may exist /// with longer names. String get username => throw _privateConstructorUsedError; @@ -434,7 +434,7 @@ class _$_UserData implements _UserData { /// The Twitter screen name, handle, or alias that this user identifies /// themselves with. Usernames are unique but subject to change. Typically - /// a maximum of 15 characters long, but some historical accounts may exist + /// a maximum of 15 characters long, but some historical accounts may exist /// with longer names. @override final String username; @@ -638,7 +638,7 @@ abstract class _UserData implements UserData { /// The Twitter screen name, handle, or alias that this user identifies /// themselves with. Usernames are unique but subject to change. Typically - /// a maximum of 15 characters long, but some historical accounts may exist + /// a maximum of 15 characters long, but some historical accounts may exist /// with longer names. String get username; @override diff --git a/lib/src/service/users/users_service.dart b/lib/src/service/users/users_service.dart index 38f36c78..11b824e1 100644 --- a/lib/src/service/users/users_service.dart +++ b/lib/src/service/users/users_service.dart @@ -9,6 +9,7 @@ import 'dart:io'; import 'package:http/http.dart'; // 🌎 Project imports: +import '../../core/adaptor/profile_banner_object_adaptor.dart'; import '../../core/adaptor/user_object_adaptor.dart'; import '../../core/client/client_context.dart'; import '../../core/client/user_context.dart'; @@ -16,6 +17,7 @@ import '../base_service.dart'; import '../pagination/bidirectional_pagination.dart'; import '../response/twitter_response.dart'; import '../tweets/tweet_field.dart'; +import 'profile_banner_variants_data.dart'; import 'user_data.dart'; import 'user_expansion.dart'; import 'user_field.dart'; @@ -1136,6 +1138,58 @@ abstract class UsersService { String? url, String? location, }); + + /// Returns a map of the available size variations of the specified user's + /// profile banner. + /// + /// The profile banner data available at each size variant's URL is in PNG + /// format. + /// + /// ## Parameters + /// + /// - [userId]: The ID of the user for whom to return results. + /// + /// ## Endpoint Url + /// + /// - https://api.twitter.com/1.1/users/profile_banner.json?screen_name=twitterapi + /// + /// ## Authentication Methods + /// + /// - OAuth 1.0a + /// + /// ## Reference + /// + /// - https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/api-reference/get-users-profile_banner + Future> + lookupProfileBannerById({ + required String userId, + }); + + /// Returns a map of the available size variations of the specified user's + /// profile banner. + /// + /// The profile banner data available at each size variant's URL is in PNG + /// format. + /// + /// ## Parameters + /// + /// - [username]: The screen name of the user for whom to return results. + /// + /// ## Endpoint Url + /// + /// - https://api.twitter.com/1.1/users/profile_banner.json?screen_name=twitterapi + /// + /// ## Authentication Methods + /// + /// - OAuth 1.0a + /// + /// ## Reference + /// + /// - https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/api-reference/get-users-profile_banner + Future> + lookupProfileBannerByName({ + required String username, + }); } class _UsersService extends BaseService implements UsersService { @@ -1517,6 +1571,20 @@ class _UsersService extends BaseService implements UsersService { adaptor: _userObjectAdaptor, ); + @override + Future> + lookupProfileBannerById({ + required String userId, + }) async => + _lookupProfileBanner(userId: userId); + + @override + Future> + lookupProfileBannerByName({ + required String username, + }) async => + _lookupProfileBanner(username: username); + Future> _createReport({ String? userId, String? username, @@ -1535,4 +1603,22 @@ class _UsersService extends BaseService implements UsersService { dataBuilder: UserData.fromJson, adaptor: _userObjectAdaptor, ); + + Future> + _lookupProfileBanner({ + String? userId, + String? username, + }) async => + super.transformSingleDataResponse( + await super.get( + UserContext.oauth1Only, + '/1.1/users/profile_banner.json', + queryParameters: { + 'user_id': userId, + 'screen_name': username, + }, + ), + dataBuilder: ProfileBannerVariantsData.fromJson, + adaptor: const ProfileBannerObjectAdaptor(), + ); } diff --git a/lib/twitter_api_v2.dart b/lib/twitter_api_v2.dart index 3182bffe..a79e85b7 100644 --- a/lib/twitter_api_v2.dart +++ b/lib/twitter_api_v2.dart @@ -126,6 +126,8 @@ export 'package:twitter_api_v2/src/service/tweets/tweet_type.dart'; export 'package:twitter_api_v2/src/service/tweets/tweet_withheld.dart'; export 'package:twitter_api_v2/src/service/tweets/tweets_service.dart'; export 'package:twitter_api_v2/src/service/tweets/tweets_service_extension.dart'; +export 'package:twitter_api_v2/src/service/users/profile_banner_variant.dart'; +export 'package:twitter_api_v2/src/service/users/profile_banner_variants_data.dart'; export 'package:twitter_api_v2/src/service/users/user_data.dart'; export 'package:twitter_api_v2/src/service/users/user_description_entity.dart'; export 'package:twitter_api_v2/src/service/users/user_entities.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index ae866361..6be8131a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: twitter_api_v2 description: The most famous and powerful Dart/Flutter library for Twitter API v2.0. -version: 4.8.0 +version: 4.8.1 repository: https://github.com/twitter-dart/twitter-api-v2 issue_tracker: https://github.com/twitter-dart/twitter-api-v2/issues homepage: https://github.com/twitter-dart diff --git a/test/src/service/users/data/lookup_profile_banner_by_id.json b/test/src/service/users/data/lookup_profile_banner_by_id.json new file mode 100644 index 00000000..599d59f2 --- /dev/null +++ b/test/src/service/users/data/lookup_profile_banner_by_id.json @@ -0,0 +1,54 @@ +{ + "sizes": { + "ipad": { + "h": 313, + "w": 626, + "url": "https:\/\/pbs.twimg.com\/profile_banners\/1593998704381689857\/1672906382\/ipad" + }, + "ipad_retina": { + "h": 626, + "w": 1252, + "url": "https:\/\/pbs.twimg.com\/profile_banners\/1593998704381689857\/1672906382\/ipad_retina" + }, + "web": { + "h": 260, + "w": 520, + "url": "https:\/\/pbs.twimg.com\/profile_banners\/1593998704381689857\/1672906382\/web" + }, + "web_retina": { + "h": 520, + "w": 1040, + "url": "https:\/\/pbs.twimg.com\/profile_banners\/1593998704381689857\/1672906382\/web_retina" + }, + "mobile": { + "h": 160, + "w": 320, + "url": "https:\/\/pbs.twimg.com\/profile_banners\/1593998704381689857\/1672906382\/mobile" + }, + "mobile_retina": { + "h": 320, + "w": 640, + "url": "https:\/\/pbs.twimg.com\/profile_banners\/1593998704381689857\/1672906382\/mobile_retina" + }, + "300x100": { + "h": 100, + "w": 300, + "url": "https:\/\/pbs.twimg.com\/profile_banners\/1593998704381689857\/1672906382\/300x100" + }, + "600x200": { + "h": 200, + "w": 600, + "url": "https:\/\/pbs.twimg.com\/profile_banners\/1593998704381689857\/1672906382\/600x200" + }, + "1500x500": { + "h": 500, + "w": 1500, + "url": "https:\/\/pbs.twimg.com\/profile_banners\/1593998704381689857\/1672906382\/1500x500" + }, + "1080x360": { + "h": 360, + "w": 1080, + "url": "https:\/\/pbs.twimg.com\/profile_banners\/1593998704381689857\/1672906382\/1080x360" + } + } +} \ No newline at end of file diff --git a/test/src/service/users/data/lookup_profile_banner_by_name.json b/test/src/service/users/data/lookup_profile_banner_by_name.json new file mode 100644 index 00000000..599d59f2 --- /dev/null +++ b/test/src/service/users/data/lookup_profile_banner_by_name.json @@ -0,0 +1,54 @@ +{ + "sizes": { + "ipad": { + "h": 313, + "w": 626, + "url": "https:\/\/pbs.twimg.com\/profile_banners\/1593998704381689857\/1672906382\/ipad" + }, + "ipad_retina": { + "h": 626, + "w": 1252, + "url": "https:\/\/pbs.twimg.com\/profile_banners\/1593998704381689857\/1672906382\/ipad_retina" + }, + "web": { + "h": 260, + "w": 520, + "url": "https:\/\/pbs.twimg.com\/profile_banners\/1593998704381689857\/1672906382\/web" + }, + "web_retina": { + "h": 520, + "w": 1040, + "url": "https:\/\/pbs.twimg.com\/profile_banners\/1593998704381689857\/1672906382\/web_retina" + }, + "mobile": { + "h": 160, + "w": 320, + "url": "https:\/\/pbs.twimg.com\/profile_banners\/1593998704381689857\/1672906382\/mobile" + }, + "mobile_retina": { + "h": 320, + "w": 640, + "url": "https:\/\/pbs.twimg.com\/profile_banners\/1593998704381689857\/1672906382\/mobile_retina" + }, + "300x100": { + "h": 100, + "w": 300, + "url": "https:\/\/pbs.twimg.com\/profile_banners\/1593998704381689857\/1672906382\/300x100" + }, + "600x200": { + "h": 200, + "w": 600, + "url": "https:\/\/pbs.twimg.com\/profile_banners\/1593998704381689857\/1672906382\/600x200" + }, + "1500x500": { + "h": 500, + "w": 1500, + "url": "https:\/\/pbs.twimg.com\/profile_banners\/1593998704381689857\/1672906382\/1500x500" + }, + "1080x360": { + "h": 360, + "w": 1080, + "url": "https:\/\/pbs.twimg.com\/profile_banners\/1593998704381689857\/1672906382\/1080x360" + } + } +} \ No newline at end of file diff --git a/test/src/service/users/users_service_test.dart b/test/src/service/users/users_service_test.dart index a814364f..bcc972a8 100644 --- a/test/src/service/users/users_service_test.dart +++ b/test/src/service/users/users_service_test.dart @@ -15,6 +15,7 @@ import 'package:twitter_api_v2/src/core/client/user_context.dart'; import 'package:twitter_api_v2/src/service/pagination/pagination_control.dart'; import 'package:twitter_api_v2/src/service/response/pagination_response.dart'; import 'package:twitter_api_v2/src/service/response/twitter_response.dart'; +import 'package:twitter_api_v2/src/service/users/profile_banner_variants_data.dart'; import 'package:twitter_api_v2/src/service/users/user_data.dart'; import 'package:twitter_api_v2/src/service/users/user_meta.dart'; import 'package:twitter_api_v2/src/service/users/users_service.dart'; @@ -2238,4 +2239,208 @@ void main() { expect(response.data, isA()); }); }); + + group('.lookupProfileBannerById', () { + test('normal case', () async { + final usersService = UsersService( + context: context.buildGetStub( + UserContext.oauth1Only, + '/1.1/users/profile_banner.json', + 'test/src/service/users/data/lookup_profile_banner_by_id.json', + { + 'user_id': '1111', + }, + ), + ); + + final response = await usersService.lookupProfileBannerById( + userId: '1111', + ); + + expect(response, isA()); + expect(response.data, isA()); + }); + + test('with invalid access token', () async { + final usersService = UsersService( + context: ClientContext( + bearerToken: '', + oauthTokens: OAuthTokens( + consumerKey: '1234', + consumerSecret: '1234', + accessToken: '1234', + accessTokenSecret: '1234', + ), + timeout: Duration(seconds: 10), + ), + ); + + expectUnauthorizedException( + () async => await usersService.lookupProfileBannerById( + userId: '1111', + ), + ); + }); + + test('with rate limit exceeded error', () async { + final usersService = UsersService( + context: context.buildGetStub( + UserContext.oauth1Only, + '/1.1/users/profile_banner.json', + 'test/src/service/users/data/lookup_profile_banner_by_id.json', + { + 'user_id': '1111', + }, + statusCode: 429, + ), + ); + + expectRateLimitExceededException( + () async => await usersService.lookupProfileBannerById( + userId: '1111', + ), + ); + }); + + test('with errors', () async { + final usersService = UsersService( + context: context.buildGetStub( + UserContext.oauth1Only, + '/1.1/users/profile_banner.json', + 'test/src/service/users/data/no_data.json', + { + 'user_id': '1111', + }, + statusCode: 404, + ), + ); + + expectDataNotFoundExceptionDueToNoData( + () async => await usersService.lookupProfileBannerById( + userId: '1111', + ), + ); + }); + + test('with no json', () async { + final usersService = UsersService( + context: context.buildGetStub( + UserContext.oauth1Only, + '/1.1/users/profile_banner.json', + 'test/src/service/users/data/no_json.json', + { + 'user_id': '1111', + }, + ), + ); + + expectDataNotFoundExceptionDueToNoJson( + () async => await usersService.lookupProfileBannerById( + userId: '1111', + ), + ); + }); + }); + + group('.lookupProfileBannerByName', () { + test('normal case', () async { + final usersService = UsersService( + context: context.buildGetStub( + UserContext.oauth1Only, + '/1.1/users/profile_banner.json', + 'test/src/service/users/data/lookup_profile_banner_by_name.json', + { + 'screen_name': '1111', + }, + ), + ); + + final response = await usersService.lookupProfileBannerByName( + username: '1111', + ); + + expect(response, isA()); + expect(response.data, isA()); + }); + + test('with invalid access token', () async { + final usersService = UsersService( + context: ClientContext( + bearerToken: '', + oauthTokens: OAuthTokens( + consumerKey: '1234', + consumerSecret: '1234', + accessToken: '1234', + accessTokenSecret: '1234', + ), + timeout: Duration(seconds: 10), + ), + ); + + expectUnauthorizedException( + () async => await usersService.lookupProfileBannerByName( + username: '1111', + ), + ); + }); + + test('with rate limit exceeded error', () async { + final usersService = UsersService( + context: context.buildGetStub( + UserContext.oauth1Only, + '/1.1/users/profile_banner.json', + 'test/src/service/users/data/lookup_profile_banner_by_name.json', + { + 'screen_name': '1111', + }, + statusCode: 429, + ), + ); + + expectRateLimitExceededException( + () async => await usersService.lookupProfileBannerByName( + username: '1111', + ), + ); + }); + + test('with errors', () async { + final usersService = UsersService( + context: context.buildGetStub( + UserContext.oauth1Only, + '/1.1/users/profile_banner.json', + 'test/src/service/users/data/no_data.json', + { + 'screen_name': '1111', + }, + statusCode: 404, + ), + ); + + expectDataNotFoundExceptionDueToNoData( + () async => await usersService.lookupProfileBannerByName( + username: '1111', + ), + ); + }); + + test('with no json', () async { + final usersService = UsersService( + context: context.buildGetStub( + UserContext.oauth1Only, + '/1.1/users/profile_banner.json', + 'test/src/service/users/data/no_json.json', + { + 'screen_name': '1111', + }, + ), + ); + + expectDataNotFoundExceptionDueToNoJson( + () async => await usersService.lookupProfileBannerByName( + username: '1111', + ), + ); + }); + }); }