relatica/lib/friendica_client/friendica_client.dart

975 wiersze
35 KiB
Dart

import 'dart:convert';
import 'dart:typed_data';
import 'package:http/http.dart' as http;
import 'package:http_parser/http_parser.dart';
import 'package:logging/logging.dart';
import 'package:result_monad/result_monad.dart';
import '../friendica_client/paged_response.dart';
import '../globals.dart';
import '../models/TimelineIdentifiers.dart';
import '../models/auth/profile.dart';
import '../models/connection.dart';
import '../models/direct_message.dart';
import '../models/exec_error.dart';
import '../models/follow_request.dart';
import '../models/gallery_data.dart';
import '../models/group_data.dart';
import '../models/image_entry.dart';
import '../models/instance_info.dart';
import '../models/media_attachment_uploads/image_types_enum.dart';
import '../models/timeline_entry.dart';
import '../models/user_notification.dart';
import '../models/visibility.dart';
import '../serializers/friendica/direct_message_friendica_extensions.dart';
import '../serializers/friendica/gallery_data_friendica_extensions.dart';
import '../serializers/friendica/image_entry_friendica_extensions.dart';
import '../serializers/friendica/visibility_friendica_extensions.dart';
import '../serializers/mastodon/connection_mastodon_extensions.dart';
import '../serializers/mastodon/follow_request_mastodon_extensions.dart';
import '../serializers/mastodon/group_data_mastodon_extensions.dart';
import '../serializers/mastodon/instance_info_mastodon_extensions.dart';
import '../serializers/mastodon/notification_mastodon_extension.dart';
import '../serializers/mastodon/timeline_entry_mastodon_extensions.dart';
import '../serializers/mastodon/visibility_mastodon_extensions.dart';
import '../services/network_status_service.dart';
import 'paging_data.dart';
class DirectMessagingClient extends FriendicaClient {
static final _logger = Logger('$DirectMessagingClient');
DirectMessagingClient(super.credentials) : super();
FutureResult<List<DirectMessage>, ExecError> getDirectMessages(
PagingData page) async {
_networkStatusService.startDirectMessageUpdateStatus();
final baseUrl = 'https://$serverName/api/direct_messages/all';
final pagingQP = page.toQueryParameters(limitKeyword: 'count');
final url = '$baseUrl?$pagingQP';
final request = Uri.parse(url);
_logger.finest(() => 'Getting direct messages with paging data $page');
final result = (await _getApiListRequest(request).andThenSuccessAsync(
(response) async => response.data
.map((json) => DirectMessageFriendicaExtension.fromJson(json))
.toList()))
.execErrorCast();
_networkStatusService.finishDirectMessageUpdateStatus();
return result;
}
FutureResult<DirectMessage, ExecError> markDirectMessageRead(
DirectMessage message) async {
_networkStatusService.startDirectMessageUpdateStatus();
final id = message.id;
final url = Uri.parse(
'https://$serverName/api/friendica/direct_messages_setseen?id=$id');
final result =
await _postUrl(url, {}).andThenSuccessAsync((jsonString) async {
return message.copy(seen: true);
});
_networkStatusService.finishDirectMessageUpdateStatus();
return result.execErrorCast();
}
FutureResult<DirectMessage, ExecError> postDirectMessage(
String? messageIdRepliedTo,
String receivingUserId,
String text,
) async {
_networkStatusService.startDirectMessageUpdateStatus();
final url = Uri.parse('https://$serverName/api/direct_messages/new');
final body = {
'user_id': receivingUserId,
'text': text,
if (messageIdRepliedTo != null) 'replyto': messageIdRepliedTo,
};
final result = await _postUrl(url, body)
.andThenAsync<DirectMessage, ExecError>((jsonString) async {
final json = jsonDecode(jsonString) as Map<String, dynamic>;
if (json.containsKey('error')) {
return buildErrorResult(
type: ErrorType.serverError,
message: "Error from server: ${json['error']}");
}
return Result.ok(
DirectMessageFriendicaExtension.fromJson(jsonDecode(jsonString)));
});
_networkStatusService.finishDirectMessageUpdateStatus();
return result.execErrorCast();
}
}
class GalleryClient extends FriendicaClient {
static final _logger = Logger('$GalleryClient');
GalleryClient(super.credentials) : super();
// TODO Convert Albums to using paging for real
FutureResult<List<GalleryData>, ExecError> getGalleryData() async {
_networkStatusService.startGalleryLoading();
_logger.finest(() => 'Getting gallery data');
final url = 'https://$serverName/api/friendica/photoalbums';
final request = Uri.parse(url);
final result = (await _getApiListRequest(request).andThenSuccessAsync(
(albumsJson) async => albumsJson.data
.map((json) => GalleryDataFriendicaExtensions.fromJson(json))
.toList()))
.execErrorCast();
_networkStatusService.finishGalleryLoading();
return result;
}
// TODO Convert Gallery Images to using paging for real once server side available
FutureResult<List<ImageEntry>, ExecError> getGalleryImages(
String galleryName,
PagingData page,
) async {
_networkStatusService.startGalleryLoading();
_logger.finest(() => 'Getting gallery $galleryName data with page: $page');
final baseUrl = 'https://$serverName/api/friendica/photoalbum?';
final gallery = 'album=$galleryName&latest_first=true';
final pageParams = page.toQueryParameters();
final url = '$baseUrl$gallery&$pageParams';
final request = Uri.parse(url);
final result = (await _getApiListRequest(request).andThenSuccessAsync(
(imagesJson) async => imagesJson.data
.map((json) => ImageEntryFriendicaExtension.fromJson(json))
.toList()))
.execErrorCast();
_networkStatusService.finishGalleryLoading();
return result;
}
}
class GroupsClient extends FriendicaClient {
static final _logger = Logger('$GroupsClient');
GroupsClient(super.credentials) : super();
// TODO Convert Groups to using paging for real (if it is supported)
FutureResult<List<GroupData>, ExecError> getGroups() async {
_logger.finest(() => 'Getting group (Mastodon List) data');
final url = 'https://$serverName/api/v1/lists';
final request = Uri.parse(url);
return (await _getApiListRequest(request).andThenSuccessAsync(
(listsJson) async => listsJson.data
.map((json) => GroupDataMastodonExtensions.fromJson(json))
.toList()))
.mapError((error) => error is ExecError
? error
: ExecError(type: ErrorType.localError, message: error.toString()));
}
// TODO Convert groups for connection to using paging for real (if available)
FutureResult<List<GroupData>, ExecError> getMemberGroupsForConnection(
String connectionId) async {
_logger.finest(() =>
'Getting groups (Mastodon Lists) containing connection: $connectionId');
final url = 'https://$serverName/api/v1/accounts/$connectionId/lists';
final request = Uri.parse(url);
return (await _getApiListRequest(request).andThenSuccessAsync(
(listsJson) async => listsJson.data
.map((json) => GroupDataMastodonExtensions.fromJson(json))
.toList()))
.mapError((error) => error as ExecError);
}
FutureResult<bool, ExecError> addConnectionToGroup(
GroupData group,
Connection connection,
) async {
_logger.finest(() => 'Adding connection to group');
final url = 'https://$serverName/api/v1/lists/${group.id}/accounts';
final request = Uri.parse(url);
final requestData = {
'account_ids': [connection.id]
};
return (await _postUrl(request, requestData)).mapValue((_) => true);
}
FutureResult<bool, ExecError> removeConnectionFromGroup(
GroupData group,
Connection connection,
) async {
_logger.finest(() => 'Adding connection to group');
final url = 'https://$serverName/api/v1/lists/${group.id}/accounts';
final request = Uri.parse(url);
final requestData = {
'account_ids': [connection.id]
};
return (await _deleteUrl(request, requestData)).mapValue((_) => true);
}
}
class InteractionsClient extends FriendicaClient {
static final _logger = Logger('$InteractionsClient');
InteractionsClient(super.credentials) : super();
// TODO Convert getLikes to using paging for real
FutureResult<List<Connection>, ExecError> getLikes(String id) async {
_networkStatusService.startInteractionsLoading();
final result = (await runCatchingAsync(() async {
final url = 'https://$serverName/api/v1/statuses/$id/favourited_by';
final request = Uri.parse('$url');
_logger.finest(() => 'Getting favorites for status $id');
return (await _getApiListRequest(request)
.andThenSuccessAsync((jsonArray) async {
return jsonArray.data
.map((p) => ConnectionMastodonExtensions.fromJson(p))
.toList();
}));
}))
.execErrorCastAsync();
_networkStatusService.finishInteractionsLoading();
return result;
}
// TODO Convert getReshares to using paging for real
FutureResult<List<Connection>, ExecError> getReshares(String id) async {
_networkStatusService.startInteractionsLoading();
final result = (await runCatchingAsync(() async {
final url = 'https://$serverName/api/v1/statuses/$id/reblogged_by';
final request = Uri.parse('$url');
_logger.finest(() => 'Getting rebloggers for status $id');
return (await _getApiListRequest(request)
.andThenSuccessAsync((jsonArray) async {
return jsonArray.data
.map((p) => ConnectionMastodonExtensions.fromJson(p))
.toList();
}));
}))
.execErrorCastAsync();
_networkStatusService.finishInteractionsLoading();
return result;
}
FutureResult<TimelineEntry, ExecError> changeFavoriteStatus(
String id, bool status) async {
final action = status ? 'favourite' : 'unfavourite';
final url = Uri.parse('https://$serverName/api/v1/statuses/$id/$action');
final result = await _postUrl(url, {});
if (result.isFailure) {
return result.errorCast();
}
final responseText = result.value;
return runCatching<TimelineEntry>(() {
final json = jsonDecode(responseText);
return Result.ok(TimelineEntryMastodonExtensions.fromJson(json));
}).mapError((error) {
return ExecError(type: ErrorType.parsingError, message: error.toString());
});
}
}
class InstanceDataClient extends FriendicaClient {
static final _logger = Logger('$InteractionsClient');
InstanceDataClient(super.credentials);
FutureResult<InstanceInfo, ExecError> getInstanceData() async {
_logger.finest(() => 'Getting $serverName instance info');
final v2Result = await getInstanceDataV2();
if (v2Result.isSuccess) {
return v2Result;
}
return getInstanceDataV1();
}
FutureResult<InstanceInfo, ExecError> getInstanceDataV1() async {
_logger.finest(() => 'Getting $serverName instance info via V1 endpoint');
final url = Uri.parse('https://$serverName/api/v1/instance');
final result = await _getApiRequest(url);
return result.andThen((json) {
return fromInstanceV1Json(json);
}).execErrorCast();
}
FutureResult<InstanceInfo, ExecError> getInstanceDataV2() async {
_logger.finest(() => 'Getting $serverName instance info via V2 endpoint');
final url = Uri.parse('https://$serverName/api/v2/instance');
final result = await _getApiRequest(url);
return result.andThen((response) {
final instanceInfoResult = fromInstanceV2Json(response);
return instanceInfoResult;
}).execErrorCast();
}
}
class NotificationsClient extends FriendicaClient {
static final _logger = Logger('$NotificationsClient');
NotificationsClient(super.credentials) : super();
FutureResult<PagedResponse<List<UserNotification>>, ExecError>
getNotifications(PagingData page) async {
_networkStatusService.startNotificationUpdate();
final url = 'https://$serverName/api/v1/notifications?include_all=true';
final request = Uri.parse('$url&${page.toQueryParameters()}');
_logger.finest(() => 'Getting new notifications');
final result = await _getApiListRequest(request);
_networkStatusService.finishNotificationUpdate();
return result
.andThenSuccess((response) => response.map((jsonArray) => jsonArray
.map((json) => NotificationMastodonExtension.fromJson(json))
.toList()))
.execErrorCast();
}
FutureResult<bool, ExecError> clearNotifications() async {
final url = 'https://$serverName/api/v1/notifications/clear';
final request = Uri.parse(url);
_logger.finest(() => 'Clearing unread notification');
final response = await _postUrl(request, {});
return response.mapValue((value) => true);
}
FutureResult<bool, ExecError> clearNotification(
UserNotification notification) async {
final url =
'https://$serverName/api/v1/notifications/${notification.id}/dismiss';
final request = Uri.parse(url);
_logger.finest(() => 'Clearing unread notification for $notification');
final response = await _postUrl(request, {});
return response.mapValue((value) => true);
}
}
class RelationshipsClient extends FriendicaClient {
static final _logger = Logger('$RelationshipsClient');
RelationshipsClient(super.credentials) : super();
FutureResult<PagedResponse<List<Connection>>, ExecError> getMyFollowing(
PagingData page) async {
_logger.finest(() => 'Getting following with paging data $page');
_networkStatusService.startConnectionUpdateStatus();
final myId = profile.userId;
final baseUrl = 'https://$serverName/api/v1/accounts/$myId';
final result = await _getApiListRequest(
Uri.parse('$baseUrl/following?${page.toQueryParameters()}'),
);
_networkStatusService.finishConnectionUpdateStatus();
return result
.andThenSuccess((response) => response.map((jsonArray) => jsonArray
.map((json) => ConnectionMastodonExtensions.fromJson(json))
.toList()))
.execErrorCast();
}
FutureResult<PagedResponse<List<FollowRequest>>, ExecError> getFollowRequests(
PagingData page) async {
_logger.finest(() => 'Getting follow requests with paging data $page');
_networkStatusService.startConnectionUpdateStatus();
final baseUrl = 'https://$serverName/api/v1/follow_requests';
final result = await _getApiListRequest(
Uri.parse('$baseUrl?${page.toQueryParameters()}'),
);
_networkStatusService.finishConnectionUpdateStatus();
return result
.andThenSuccess((response) => response.map((jsonArray) => jsonArray
.map((json) => FollowRequestMastodonExtension.fromJson(json))
.toList()))
.execErrorCast();
}
FutureResult<PagedResponse<List<Connection>>, ExecError> getMyFollowers(
PagingData page) async {
_logger.finest(() => 'Getting followers data with page data $page');
_networkStatusService.startConnectionUpdateStatus();
final myId = profile.userId;
final baseUrl = 'https://$serverName/api/v1/accounts/$myId';
final result1 = await _getApiListRequest(
Uri.parse('$baseUrl/followers&${page.toQueryParameters()}'),
);
_networkStatusService.finishConnectionUpdateStatus();
return result1
.andThenSuccess((response) => response.map((jsonArray) => jsonArray
.map((json) => ConnectionMastodonExtensions.fromJson(json))
.toList()))
.execErrorCast();
}
FutureResult<Connection, ExecError> getConnectionWithStatus(
Connection connection) async {
_logger.finest(() => 'Getting group (Mastodon List) data');
_networkStatusService.startConnectionUpdateStatus();
final myId = profile.userId;
final id = int.parse(connection.id);
final paging = '?min_id=${id - 1}&max_id=${id + 1}';
final baseUrl = 'https://$serverName/api/v1/accounts/$myId';
final following =
await _getApiListRequest(Uri.parse('$baseUrl/following$paging')).fold(
onSuccess: (followings) => followings.data.isNotEmpty,
onError: (error) {
_logger.severe('Error getting following list: $error');
return false;
});
final follower =
await _getApiListRequest(Uri.parse('$baseUrl/followers$paging')).fold(
onSuccess: (followings) => followings.data.isNotEmpty,
onError: (error) {
_logger.severe('Error getting follower list: $error');
return false;
});
var status = ConnectionStatus.none;
if (following && follower) {
status = ConnectionStatus.mutual;
} else if (following) {
status = ConnectionStatus.youFollowThem;
} else if (follower) {
status = ConnectionStatus.theyFollowYou;
}
_networkStatusService.finishConnectionUpdateStatus();
return Result.ok(connection.copy(status: status));
}
FutureResult<PagedResponse<List<Connection>>, ExecError>
getConnectionRequests(PagingData page) async {
_logger.finest(() => 'Getting connection requests with page data $page');
_networkStatusService.startConnectionUpdateStatus();
final baseUrl = 'https://$serverName/api/v1/follow_requests';
final result1 = await _getApiListRequest(
Uri.parse('$baseUrl&${page.toQueryParameters()}'),
);
_networkStatusService.finishConnectionUpdateStatus();
return result1
.andThenSuccess((response) => response.map((jsonArray) => jsonArray
.map((json) => ConnectionMastodonExtensions.fromJson(json))
.toList()))
.execErrorCast();
}
FutureResult<Connection, ExecError> acceptFollow(
Connection connection) async {
final id = connection.id;
final url =
Uri.parse('https://$serverName/api/v1/follow_requests/$id/authorize');
final result =
await _postUrl(url, {}).andThenSuccessAsync((jsonString) async {
return _updateConnectionFromFollowRequestResult(connection, jsonString);
});
return result.mapError((error) => error is ExecError
? error
: ExecError(type: ErrorType.localError, message: error.toString()));
}
FutureResult<Connection, ExecError> rejectFollow(
Connection connection) async {
final id = connection.id;
final url =
Uri.parse('https://$serverName/api/v1/follow_requests/$id/reject');
final result =
await _postUrl(url, {}).andThenSuccessAsync((jsonString) async {
return _updateConnectionFromFollowRequestResult(connection, jsonString);
});
return result.mapError((error) => error is ExecError
? error
: ExecError(type: ErrorType.localError, message: error.toString()));
}
FutureResult<Connection, ExecError> ignoreFollow(
Connection connection) async {
final id = connection.id;
final url =
Uri.parse('https://$serverName/api/v1/follow_requests/$id/ignore');
final result =
await _postUrl(url, {}).andThenSuccessAsync((jsonString) async {
return _updateConnectionFromFollowRequestResult(connection, jsonString);
});
return result.mapError((error) => error is ExecError
? error
: ExecError(type: ErrorType.localError, message: error.toString()));
}
FutureResult<Connection, ExecError> followConnection(
Connection connection) async {
final id = connection.id;
final url = Uri.parse('https://$serverName/api/v1/accounts/$id/follow');
final result =
await _postUrl(url, {}).andThenSuccessAsync((jsonString) async {
return _updateConnectionFromFollowRequestResult(connection, jsonString);
});
return result.mapError((error) => error is ExecError
? error
: ExecError(type: ErrorType.localError, message: error.toString()));
}
FutureResult<Connection, ExecError> unFollowConnection(
Connection connection) async {
final id = connection.id;
final url = Uri.parse('https://$serverName/api/v1/accounts/$id/unfollow');
final result =
await _postUrl(url, {}).andThenSuccessAsync((jsonString) async {
return _updateConnectionFromFollowRequestResult(connection, jsonString);
});
return result.mapError((error) => error is ExecError
? error
: ExecError(type: ErrorType.localError, message: error.toString()));
}
Connection _updateConnectionFromFollowRequestResult(
Connection connection, String jsonString) {
final json = jsonDecode(jsonString) as Map<String, dynamic>;
final theyFollowYou = json['followed_by'] ?? 'false';
final youFollowThem = json['following'] ?? 'false';
late final ConnectionStatus newStatus;
if (theyFollowYou && youFollowThem) {
newStatus = ConnectionStatus.mutual;
} else if (theyFollowYou) {
newStatus = ConnectionStatus.theyFollowYou;
} else if (youFollowThem) {
newStatus = ConnectionStatus.youFollowThem;
} else {
newStatus = ConnectionStatus.none;
}
return connection.copy(status: newStatus);
}
}
class RemoteFileClient extends FriendicaClient {
static final _logger = Logger('$RemoteFileClient');
RemoteFileClient(super.credentials) : super();
FutureResult<Uint8List, ExecError> getFileBytes(Uri url) async {
_logger.finest('GET: $url');
try {
final response = await http.get(
url,
headers: {
'Authorization': _profile.credentials.authHeaderValue,
},
);
if (response.statusCode != 200) {
return Result.error(ExecError(
type: ErrorType.authentication,
message: '${response.statusCode}: ${response.reasonPhrase}'));
}
return Result.ok(response.bodyBytes);
} catch (e) {
return Result.error(
ExecError(type: ErrorType.localError, message: e.toString()));
}
}
FutureResult<ImageEntry, ExecError> uploadFileAsAttachment({
required List<int> bytes,
String description = '',
String album = '',
String fileName = '',
required Visibility visibility,
}) async {
final postUri = Uri.parse('https://$serverName/api/friendica/photo/create');
final request = http.MultipartRequest('POST', postUri);
request.headers['Authorization'] = _profile.credentials.authHeaderValue;
if (usePhpDebugging) {
request.headers['Cookie'] = 'XDEBUG_SESSION=PHPSTORM;path=/';
}
request.fields['desc'] = description;
request.fields['album'] = album;
request.fields.addAll(visibility.toMapEntries());
request.files.add(http.MultipartFile.fromBytes(
'media',
filename: fileName,
contentType:
MediaType.parse('image/${ImageTypes.fromExtension(fileName).name}'),
bytes));
final response = await request.send();
final body = utf8.decode(await response.stream.toBytes());
if (response.statusCode != 200) {
return Result.error(
ExecError(
type: ErrorType.missingEndpoint,
message: body,
),
);
}
final imageDataJson = jsonDecode(body);
final newImageData = ImageEntryFriendicaExtension.fromJson(imageDataJson);
return Result.ok(newImageData);
}
}
class ProfileClient extends FriendicaClient {
static final _logger = Logger('$ProfileClient');
ProfileClient(super.credentials) : super();
FutureResult<Connection, ExecError> getMyProfile() async {
_logger.finest(() => 'Getting logged in user profile');
final request =
Uri.parse('https://$serverName/api/v1/accounts/verify_credentials');
return (await _getApiRequest(request))
.mapValue((json) => ConnectionMastodonExtensions.fromJson(
json,
defaultServerName: serverName,
).copy(
status: ConnectionStatus.you,
network: 'friendica',
));
}
}
class StatusesClient extends FriendicaClient {
static final _logger = Logger('$StatusesClient');
StatusesClient(super.credentials) : super();
// TODO Convert getPostOrComment to using paging for real
FutureResult<List<TimelineEntry>, ExecError> getPostOrComment(String id,
{bool fullContext = false}) async {
_networkStatusService.startTimelineLoading();
final result = (await runCatchingAsync(() async {
final baseUrl = 'https://$serverName/api/v1/statuses/$id';
final url = fullContext ? '$baseUrl/context' : baseUrl;
final request = Uri.parse('$url?limit=1000');
_logger.finest(() =>
'Getting entry for status $id, full context? $fullContext : $url');
return (await _getApiRequest(request).andThenSuccessAsync((json) async {
if (fullContext) {
final ancestors = json['ancestors'] as List<dynamic>;
final descendants = json['descendants'] as List<dynamic>;
final items = [
...ancestors
.map((a) => TimelineEntryMastodonExtensions.fromJson(a)),
...descendants
.map((d) => TimelineEntryMastodonExtensions.fromJson(d))
];
return items;
} else {
return [TimelineEntryMastodonExtensions.fromJson(json)];
}
}));
}));
_networkStatusService.finishTimelineLoading();
return result.execErrorCast();
}
FutureResult<TimelineEntry, ExecError> createNewStatus({
required String text,
String spoilerText = '',
String inReplyToId = '',
List<String> mediaIds = const [],
required Visibility visibility,
}) async {
_logger.finest(() =>
'Creating status ${inReplyToId.isNotEmpty ? "In Reply to: " : ""} $inReplyToId, with media: $mediaIds');
final url = Uri.parse('https://$serverName/api/v1/statuses');
final body = {
'status': text,
if (spoilerText.isNotEmpty) 'spoiler_text': spoilerText,
if (inReplyToId.isNotEmpty) 'in_reply_to_id': inReplyToId,
if (mediaIds.isNotEmpty) 'media_ids': mediaIds,
'visibility': visibility.toCreateStatusValue(),
'friendica': {
'title': '',
},
};
final result = await _postUrl(url, body);
if (result.isFailure) {
return result.errorCast();
}
final responseText = result.value;
return runCatching<TimelineEntry>(() {
final json = jsonDecode(responseText);
return Result.ok(TimelineEntryMastodonExtensions.fromJson(json));
}).mapError((error) {
return ExecError(type: ErrorType.parsingError, message: error.toString());
});
}
FutureResult<TimelineEntry, ExecError> editStatus({
required String id,
required String text,
String spoilerText = '',
List<String> mediaIds = const [],
}) async {
_logger.finest(() => 'Updating status $id');
final url = Uri.parse('https://$serverName/api/v1/statuses/$id');
final body = {
'status': text,
if (spoilerText.isNotEmpty) 'spoiler_text': spoilerText,
if (mediaIds.isNotEmpty) 'media_ids': mediaIds,
'friendica': {
'title': '',
},
};
final result = await _putUrl(url, body);
if (result.isFailure) {
return result.errorCast();
}
final responseText = result.value;
return runCatching<TimelineEntry>(() {
final json = jsonDecode(responseText);
return Result.ok(TimelineEntryMastodonExtensions.fromJson(json));
}).mapError((error) {
return ExecError(type: ErrorType.parsingError, message: error.toString());
});
}
FutureResult<TimelineEntry, ExecError> resharePost(String id) async {
_logger.finest(() => 'Reshare post $id');
final url = Uri.parse('https://$serverName/api/v1/statuses/$id/reblog');
final result = await _postUrl(url, {});
if (result.isFailure) {
return result.errorCast();
}
final responseText = result.value;
return runCatching<TimelineEntry>(() {
final json = jsonDecode(responseText);
return Result.ok(TimelineEntryMastodonExtensions.fromJson(json));
}).mapError((error) {
return ExecError(type: ErrorType.parsingError, message: error.toString());
});
}
FutureResult<TimelineEntry, ExecError> unResharePost(String id) async {
_logger.finest(() => 'Reshare post $id');
final url = Uri.parse('https://$serverName/api/v1/statuses/$id/unreblog');
final result = await _postUrl(url, {});
if (result.isFailure) {
return result.errorCast();
}
final responseText = result.value;
return runCatching<TimelineEntry>(() {
final json = jsonDecode(responseText);
return Result.ok(TimelineEntryMastodonExtensions.fromJson(json));
}).mapError((error) {
return ExecError(type: ErrorType.parsingError, message: error.toString());
});
}
FutureResult<bool, ExecError> deleteEntryById(String id) async {
_logger.finest(() => 'Deleting post/comment $id');
final url = 'https://$serverName/api/v1/statuses/$id';
final request = Uri.parse(url);
return (await _deleteUrl(request, {})).mapValue((_) => true);
}
}
class TimelineClient extends FriendicaClient {
static final _logger = Logger('$TimelineClient');
TimelineClient(super.credentials) : super();
// TODO Convert User Timeline to using paging for real
FutureResult<List<TimelineEntry>, ExecError> getUserTimeline(
{String userId = '', int page = 1, int count = 10}) async {
_logger.finest(() => 'Getting user timeline for $userId');
_networkStatusService.startTimelineLoading();
final baseUrl = 'https://$serverName/api/statuses/user_timeline?';
final pagingData = 'count=$count&page=$page';
final url = userId.isEmpty
? '$baseUrl$pagingData'
: '${baseUrl}screen_name=$userId$pagingData';
final request = Uri.parse(url);
final result = (await _getApiListRequest(request).andThenSuccessAsync(
(postsJson) async => postsJson.data
.map((json) => TimelineEntryMastodonExtensions.fromJson(json))
.toList()))
.execErrorCast();
_networkStatusService.finishTimelineLoading();
return result;
}
FutureResult<List<TimelineEntry>, ExecError> getTimeline(
{required TimelineIdentifiers type, required PagingData page}) async {
_networkStatusService.startTimelineLoading();
final String timelinePath = _typeToTimelinePath(type);
final String timelineQPs = _typeToTimelineQueryParameters(type);
final baseUrl = 'https://$serverName/api/v1/$timelinePath';
final url =
'$baseUrl?exclude_replies=true&${page.toQueryParameters()}&$timelineQPs';
final request = Uri.parse(url);
_logger.finest(() => 'Getting ${type.toHumanKey()} with paging data $page');
final result = (await _getApiListRequest(request).andThenSuccessAsync(
(postsJson) async => postsJson.data
.map((json) => TimelineEntryMastodonExtensions.fromJson(json))
.toList()))
.execErrorCast();
_networkStatusService.finishTimelineLoading();
return result;
}
String _typeToTimelinePath(TimelineIdentifiers type) {
switch (type.timeline) {
case TimelineType.home:
return 'timelines/home';
case TimelineType.global:
return 'timelines/public';
case TimelineType.local:
return 'timelines/public';
case TimelineType.group:
return 'timelines/list/${type.auxData}';
case TimelineType.profile:
return '/accounts/${type.auxData}/statuses';
case TimelineType.self:
final myId = profile.userId;
return '/accounts/$myId/statuses';
}
}
String _typeToTimelineQueryParameters(TimelineIdentifiers type) {
switch (type.timeline) {
case TimelineType.home:
case TimelineType.global:
case TimelineType.profile:
case TimelineType.group:
case TimelineType.self:
return '';
case TimelineType.local:
return 'local=true';
}
}
}
abstract class FriendicaClient {
static final _logger = Logger('$FriendicaClient');
final Profile _profile;
late final NetworkStatusService _networkStatusService;
String get serverName => _profile.serverName;
Profile get profile => _profile;
FriendicaClient(Profile credentials) : _profile = credentials {
_networkStatusService = getIt<NetworkStatusService>();
}
FutureResult<PagedResponse<String>, ExecError> _getUrl(Uri url) async {
_logger.finer('GET: $url');
try {
final response = await http.get(
url,
headers: _header,
);
if (response.statusCode != 200) {
return Result.error(ExecError(
type: ErrorType.authentication,
message: '${response.statusCode}: ${response.reasonPhrase}'));
}
return PagedResponse.fromLinkHeader(
response.headers['link'],
utf8.decode(response.bodyBytes),
);
} catch (e) {
return Result.error(
ExecError(type: ErrorType.localError, message: e.toString()));
}
}
FutureResult<String, ExecError> _postUrl(
Uri url, Map<String, dynamic> body) async {
_logger.finer('POST: $url \n Body: $body');
try {
final response = await http.post(
url,
headers: _header,
body: jsonEncode(body),
);
if (response.statusCode != 200) {
return Result.error(ExecError(
type: ErrorType.authentication,
message: '${response.statusCode}: ${response.reasonPhrase}'));
}
return Result.ok(utf8.decode(response.bodyBytes));
} catch (e) {
return Result.error(
ExecError(type: ErrorType.localError, message: e.toString()));
}
}
FutureResult<String, ExecError> _putUrl(
Uri url, Map<String, dynamic> body) async {
_logger.finer('PUT: $url \n Body: $body');
try {
final response = await http.put(
url,
headers: _header,
body: jsonEncode(body),
);
if (response.statusCode != 200) {
return Result.error(ExecError(
type: ErrorType.authentication,
message: '${response.statusCode}: ${response.reasonPhrase}'));
}
return Result.ok(utf8.decode(response.bodyBytes));
} catch (e) {
return Result.error(
ExecError(type: ErrorType.localError, message: e.toString()));
}
}
FutureResult<String, ExecError> _deleteUrl(
Uri url, Map<String, dynamic> body) async {
_logger.finer('DELETE: $url');
try {
final response = await http.delete(
url,
headers: _header,
body: jsonEncode(body),
);
if (response.statusCode != 200) {
return Result.error(ExecError(
type: ErrorType.authentication,
message: '${response.statusCode}: ${response.reasonPhrase}'));
}
return Result.ok(utf8.decode(response.bodyBytes));
} catch (e) {
return Result.error(
ExecError(type: ErrorType.localError, message: e.toString()));
}
}
FutureResult<PagedResponse<List<dynamic>>, ExecError> _getApiListRequest(
Uri url) async {
return (await _getUrl(url).andThenSuccessAsync(
(response) async =>
response.map((data) => jsonDecode(data) as List<dynamic>),
))
.mapError((error) => error as ExecError);
}
FutureResult<dynamic, ExecError> _getApiRequest(Uri url) async {
return (await _getUrl(url).andThenSuccessAsync(
(response) async => jsonDecode(response.data),
))
.execErrorCastAsync();
}
Map<String, String> get _header => {
'Authorization': _profile.credentials.authHeaderValue,
'Content-Type': 'application/json; charset=UTF-8',
if (usePhpDebugging) 'Cookie': 'XDEBUG_SESSION=PHPSTORM;path=/',
};
}