2022-11-09 02:26:03 +00:00
|
|
|
import 'dart:convert';
|
2022-12-27 17:21:56 +00:00
|
|
|
import 'dart:typed_data';
|
2022-11-09 02:26:03 +00:00
|
|
|
|
2022-11-17 16:04:14 +00:00
|
|
|
import 'package:http/http.dart' as http;
|
2022-12-26 20:26:30 +00:00
|
|
|
import 'package:http_parser/http_parser.dart';
|
2022-11-17 16:04:14 +00:00
|
|
|
import 'package:logging/logging.dart';
|
2022-11-10 02:02:26 +00:00
|
|
|
import 'package:result_monad/result_monad.dart';
|
|
|
|
|
2023-01-24 03:37:09 +00:00
|
|
|
import '../friendica_client/paged_response.dart';
|
2023-01-24 00:56:04 +00:00
|
|
|
import '../globals.dart';
|
|
|
|
import '../models/TimelineIdentifiers.dart';
|
2023-02-27 03:12:40 +00:00
|
|
|
import '../models/auth/profile.dart';
|
2023-01-24 00:56:04 +00:00
|
|
|
import '../models/connection.dart';
|
2023-01-25 01:53:55 +00:00
|
|
|
import '../models/direct_message.dart';
|
2023-01-24 00:56:04 +00:00
|
|
|
import '../models/exec_error.dart';
|
2023-03-21 18:27:38 +00:00
|
|
|
import '../models/follow_request.dart';
|
2023-01-24 00:56:04 +00:00
|
|
|
import '../models/gallery_data.dart';
|
|
|
|
import '../models/group_data.dart';
|
|
|
|
import '../models/image_entry.dart';
|
2023-03-17 20:08:25 +00:00
|
|
|
import '../models/instance_info.dart';
|
2023-01-24 00:56:04 +00:00
|
|
|
import '../models/media_attachment_uploads/image_types_enum.dart';
|
2023-03-22 04:16:23 +00:00
|
|
|
import '../models/search_results.dart';
|
|
|
|
import '../models/search_types.dart';
|
2023-01-24 00:56:04 +00:00
|
|
|
import '../models/timeline_entry.dart';
|
|
|
|
import '../models/user_notification.dart';
|
2023-03-21 01:55:47 +00:00
|
|
|
import '../models/visibility.dart';
|
2023-01-25 01:53:55 +00:00
|
|
|
import '../serializers/friendica/direct_message_friendica_extensions.dart';
|
2023-01-24 00:56:04 +00:00
|
|
|
import '../serializers/friendica/gallery_data_friendica_extensions.dart';
|
|
|
|
import '../serializers/friendica/image_entry_friendica_extensions.dart';
|
2023-03-21 01:55:47 +00:00
|
|
|
import '../serializers/friendica/visibility_friendica_extensions.dart';
|
2023-01-24 00:56:04 +00:00
|
|
|
import '../serializers/mastodon/connection_mastodon_extensions.dart';
|
2023-03-21 18:27:38 +00:00
|
|
|
import '../serializers/mastodon/follow_request_mastodon_extensions.dart';
|
2023-01-24 00:56:04 +00:00
|
|
|
import '../serializers/mastodon/group_data_mastodon_extensions.dart';
|
2023-03-17 20:08:25 +00:00
|
|
|
import '../serializers/mastodon/instance_info_mastodon_extensions.dart';
|
2023-01-24 00:56:04 +00:00
|
|
|
import '../serializers/mastodon/notification_mastodon_extension.dart';
|
2023-03-22 04:16:23 +00:00
|
|
|
import '../serializers/mastodon/search_result_mastodon_extensions.dart';
|
2023-01-24 00:56:04 +00:00
|
|
|
import '../serializers/mastodon/timeline_entry_mastodon_extensions.dart';
|
2023-03-21 01:55:47 +00:00
|
|
|
import '../serializers/mastodon/visibility_mastodon_extensions.dart';
|
2023-03-22 13:13:19 +00:00
|
|
|
import '../services/fediverse_server_validator.dart';
|
2023-01-29 12:17:49 +00:00
|
|
|
import '../services/network_status_service.dart';
|
2023-03-22 01:14:58 +00:00
|
|
|
import '../utils/network_utils.dart';
|
2023-01-24 03:37:09 +00:00
|
|
|
import 'paging_data.dart';
|
2022-11-09 02:26:03 +00:00
|
|
|
|
2023-02-24 22:53:48 +00:00
|
|
|
class DirectMessagingClient extends FriendicaClient {
|
|
|
|
static final _logger = Logger('$DirectMessagingClient');
|
2022-11-09 02:26:03 +00:00
|
|
|
|
2023-02-24 22:53:48 +00:00
|
|
|
DirectMessagingClient(super.credentials) : super();
|
2022-11-09 02:26:03 +00:00
|
|
|
|
2023-02-24 22:53:48 +00:00
|
|
|
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(
|
2023-02-25 22:39:33 +00:00
|
|
|
(response) async => response.data
|
2023-02-24 22:53:48 +00:00
|
|
|
.map((json) => DirectMessageFriendicaExtension.fromJson(json))
|
|
|
|
.toList()))
|
|
|
|
.execErrorCast();
|
2022-11-10 02:02:26 +00:00
|
|
|
|
2023-02-24 22:53:48 +00:00
|
|
|
_networkStatusService.finishDirectMessageUpdateStatus();
|
|
|
|
return result;
|
2022-11-09 02:26:03 +00:00
|
|
|
}
|
|
|
|
|
2023-02-24 22:53:48 +00:00
|
|
|
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');
|
2023-03-22 01:14:58 +00:00
|
|
|
final result = await postUrl(url, {}, headers: _headers)
|
|
|
|
.andThenSuccessAsync((jsonString) async {
|
2023-02-24 22:53:48 +00:00
|
|
|
return message.copy(seen: true);
|
|
|
|
});
|
|
|
|
_networkStatusService.finishDirectMessageUpdateStatus();
|
|
|
|
return result.execErrorCast();
|
2022-11-19 05:00:17 +00:00
|
|
|
}
|
|
|
|
|
2023-02-24 22:53:48 +00:00
|
|
|
FutureResult<DirectMessage, ExecError> postDirectMessage(
|
2023-02-25 22:39:33 +00:00
|
|
|
String? messageIdRepliedTo,
|
|
|
|
String receivingUserId,
|
|
|
|
String text,
|
|
|
|
) async {
|
2023-02-24 22:53:48 +00:00
|
|
|
_networkStatusService.startDirectMessageUpdateStatus();
|
|
|
|
final url = Uri.parse('https://$serverName/api/direct_messages/new');
|
|
|
|
final body = {
|
|
|
|
'user_id': receivingUserId,
|
|
|
|
'text': text,
|
|
|
|
if (messageIdRepliedTo != null) 'replyto': messageIdRepliedTo,
|
|
|
|
};
|
2023-03-22 01:14:58 +00:00
|
|
|
final result = await postUrl(url, body, headers: _headers)
|
2023-02-24 22:53:48 +00:00
|
|
|
.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)));
|
|
|
|
});
|
2022-11-22 04:46:34 +00:00
|
|
|
|
2023-02-24 22:53:48 +00:00
|
|
|
_networkStatusService.finishDirectMessageUpdateStatus();
|
|
|
|
return result.execErrorCast();
|
2022-11-19 19:16:46 +00:00
|
|
|
}
|
2023-02-24 22:53:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
class GalleryClient extends FriendicaClient {
|
|
|
|
static final _logger = Logger('$GalleryClient');
|
|
|
|
|
|
|
|
GalleryClient(super.credentials) : super();
|
2022-11-19 19:16:46 +00:00
|
|
|
|
2023-01-24 03:37:09 +00:00
|
|
|
// TODO Convert Albums to using paging for real
|
2022-12-14 02:06:10 +00:00
|
|
|
FutureResult<List<GalleryData>, ExecError> getGalleryData() async {
|
2023-01-29 21:46:22 +00:00
|
|
|
_networkStatusService.startGalleryLoading();
|
2022-12-14 02:06:10 +00:00
|
|
|
_logger.finest(() => 'Getting gallery data');
|
|
|
|
final url = 'https://$serverName/api/friendica/photoalbums';
|
|
|
|
final request = Uri.parse(url);
|
2023-01-29 21:46:22 +00:00
|
|
|
final result = (await _getApiListRequest(request).andThenSuccessAsync(
|
2023-02-25 22:39:33 +00:00
|
|
|
(albumsJson) async => albumsJson.data
|
2022-12-14 02:06:10 +00:00
|
|
|
.map((json) => GalleryDataFriendicaExtensions.fromJson(json))
|
|
|
|
.toList()))
|
2023-01-29 21:46:22 +00:00
|
|
|
.execErrorCast();
|
|
|
|
_networkStatusService.finishGalleryLoading();
|
|
|
|
return result;
|
2022-12-14 02:06:10 +00:00
|
|
|
}
|
|
|
|
|
2023-01-29 21:46:22 +00:00
|
|
|
// TODO Convert Gallery Images to using paging for real once server side available
|
2023-02-25 22:39:33 +00:00
|
|
|
FutureResult<List<ImageEntry>, ExecError> getGalleryImages(
|
|
|
|
String galleryName,
|
|
|
|
PagingData page,
|
|
|
|
) async {
|
2023-01-29 21:46:22 +00:00
|
|
|
_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';
|
2022-12-14 02:06:10 +00:00
|
|
|
final request = Uri.parse(url);
|
2023-01-29 21:46:22 +00:00
|
|
|
final result = (await _getApiListRequest(request).andThenSuccessAsync(
|
2023-02-25 22:39:33 +00:00
|
|
|
(imagesJson) async => imagesJson.data
|
2022-12-14 02:06:10 +00:00
|
|
|
.map((json) => ImageEntryFriendicaExtension.fromJson(json))
|
|
|
|
.toList()))
|
2023-01-29 21:46:22 +00:00
|
|
|
.execErrorCast();
|
|
|
|
_networkStatusService.finishGalleryLoading();
|
|
|
|
return result;
|
2022-12-14 02:06:10 +00:00
|
|
|
}
|
2023-05-02 23:27:36 +00:00
|
|
|
|
|
|
|
FutureResult<bool, ExecError> renameGallery(
|
|
|
|
String oldGalleryName, String newGalleryName) async {
|
|
|
|
_networkStatusService.startGalleryLoading();
|
|
|
|
_logger.finest(() => 'Getting gallery data');
|
|
|
|
final url =
|
|
|
|
Uri.parse('https://$serverName/api/friendica/photoalbum/update');
|
|
|
|
final body = {
|
|
|
|
'album': oldGalleryName,
|
|
|
|
'album_new': newGalleryName,
|
|
|
|
};
|
|
|
|
final result = await postUrl(
|
|
|
|
url,
|
|
|
|
body,
|
|
|
|
headers: _headers,
|
|
|
|
).transform((_) => true);
|
|
|
|
_networkStatusService.finishGalleryLoading();
|
|
|
|
return result.execErrorCast();
|
|
|
|
}
|
2023-02-24 22:53:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
class GroupsClient extends FriendicaClient {
|
|
|
|
static final _logger = Logger('$GroupsClient');
|
|
|
|
|
|
|
|
GroupsClient(super.credentials) : super();
|
2022-12-14 02:06:10 +00:00
|
|
|
|
2022-12-08 18:37:30 +00:00
|
|
|
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(
|
2023-02-25 22:39:33 +00:00
|
|
|
(listsJson) async => listsJson.data
|
2023-02-24 22:53:48 +00:00
|
|
|
.map((json) => GroupDataMastodonExtensions.fromJson(json))
|
|
|
|
.toList()))
|
2023-02-25 22:39:33 +00:00
|
|
|
.mapError((error) => error is ExecError
|
|
|
|
? error
|
|
|
|
: ExecError(type: ErrorType.localError, message: error.toString()));
|
2023-02-24 22:53:48 +00:00
|
|
|
}
|
|
|
|
|
2023-04-19 01:49:45 +00:00
|
|
|
FutureResult<PagedResponse<List<Connection>>, ExecError> getGroupMembers(
|
|
|
|
GroupData groupData,
|
|
|
|
PagingData page,
|
|
|
|
) async {
|
|
|
|
_networkStatusService.startConnectionUpdateStatus();
|
|
|
|
_logger.finest(() =>
|
|
|
|
'Getting members for group (Mastodon List) of name ${groupData.name} with paging: $page');
|
|
|
|
final baseUrl = 'https://$serverName/api/v1/lists/${groupData.id}/accounts';
|
|
|
|
final url = Uri.parse('$baseUrl?${page.toQueryParameters()}');
|
|
|
|
final result = await _getApiPagedRequest(url);
|
|
|
|
_networkStatusService.finishConnectionUpdateStatus();
|
|
|
|
return result
|
|
|
|
.andThenSuccess((response) => response.map((jsonArray) =>
|
|
|
|
(jsonArray as List<dynamic>)
|
|
|
|
.map((json) => ConnectionMastodonExtensions.fromJson(json))
|
|
|
|
.toList()))
|
|
|
|
.execErrorCast();
|
|
|
|
}
|
|
|
|
|
2023-04-18 23:39:52 +00:00
|
|
|
FutureResult<GroupData, ExecError> createGroup(String title) async {
|
|
|
|
_logger.finest(() => 'Creating group (Mastodon List) of name $title');
|
|
|
|
final url = 'https://$serverName/api/v1/lists';
|
|
|
|
final body = {
|
|
|
|
'title': title,
|
|
|
|
};
|
|
|
|
final result = await postUrl(
|
|
|
|
Uri.parse(url),
|
|
|
|
body,
|
|
|
|
headers: _headers,
|
|
|
|
).andThenSuccessAsync(
|
|
|
|
(data) async => GroupDataMastodonExtensions.fromJson(jsonDecode(data)));
|
|
|
|
return result.execErrorCast();
|
|
|
|
}
|
|
|
|
|
|
|
|
FutureResult<GroupData, ExecError> renameGroup(
|
|
|
|
String id, String title) async {
|
|
|
|
_logger.finest(() => 'Reanming group (Mastodon List) to name $title');
|
|
|
|
final url = 'https://$serverName/api/v1/lists/$id';
|
|
|
|
final body = {
|
|
|
|
'title': title,
|
|
|
|
};
|
|
|
|
final result = await putUrl(
|
|
|
|
Uri.parse(url),
|
|
|
|
body,
|
|
|
|
headers: _headers,
|
|
|
|
).andThenSuccessAsync((data) async {
|
|
|
|
final json = jsonDecode(data);
|
|
|
|
return GroupDataMastodonExtensions.fromJson(json);
|
|
|
|
});
|
|
|
|
return result.execErrorCast();
|
|
|
|
}
|
|
|
|
|
|
|
|
FutureResult<bool, ExecError> deleteGroup(GroupData groupData) async {
|
|
|
|
_logger.finest(
|
|
|
|
() => 'Reanming group (Mastodon List) to name ${groupData.name}');
|
|
|
|
final url = 'https://$serverName/api/v1/lists/${groupData.id}';
|
|
|
|
final result = await deleteUrl(Uri.parse(url), {}, headers: _headers);
|
|
|
|
return result.mapValue((_) => true).execErrorCast();
|
|
|
|
}
|
|
|
|
|
2023-02-24 22:53:48 +00:00
|
|
|
FutureResult<List<GroupData>, ExecError> getMemberGroupsForConnection(
|
|
|
|
String connectionId) async {
|
|
|
|
_logger.finest(() =>
|
2023-02-25 22:39:33 +00:00
|
|
|
'Getting groups (Mastodon Lists) containing connection: $connectionId');
|
2023-02-24 22:53:48 +00:00
|
|
|
final url = 'https://$serverName/api/v1/accounts/$connectionId/lists';
|
|
|
|
final request = Uri.parse(url);
|
|
|
|
return (await _getApiListRequest(request).andThenSuccessAsync(
|
2023-02-25 22:39:33 +00:00
|
|
|
(listsJson) async => listsJson.data
|
2022-12-08 18:37:30 +00:00
|
|
|
.map((json) => GroupDataMastodonExtensions.fromJson(json))
|
|
|
|
.toList()))
|
2023-02-24 22:53:48 +00:00
|
|
|
.mapError((error) => error as ExecError);
|
2022-12-08 18:37:30 +00:00
|
|
|
}
|
|
|
|
|
2023-02-25 22:39:33 +00:00
|
|
|
FutureResult<bool, ExecError> addConnectionToGroup(
|
|
|
|
GroupData group,
|
|
|
|
Connection connection,
|
|
|
|
) async {
|
2022-12-14 21:53:46 +00:00
|
|
|
_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]
|
|
|
|
};
|
2023-03-22 01:14:58 +00:00
|
|
|
return (await postUrl(request, requestData, headers: _headers))
|
|
|
|
.mapValue((_) => true);
|
2022-12-14 21:53:46 +00:00
|
|
|
}
|
|
|
|
|
2023-02-25 22:39:33 +00:00
|
|
|
FutureResult<bool, ExecError> removeConnectionFromGroup(
|
|
|
|
GroupData group,
|
|
|
|
Connection connection,
|
|
|
|
) async {
|
2022-12-14 21:53:46 +00:00
|
|
|
_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]
|
|
|
|
};
|
2023-03-22 01:14:58 +00:00
|
|
|
return (await deleteUrl(request, requestData, headers: _headers))
|
|
|
|
.mapValue((_) => true);
|
2022-12-14 21:53:46 +00:00
|
|
|
}
|
2023-02-24 22:53:48 +00:00
|
|
|
}
|
|
|
|
|
2023-05-02 23:27:36 +00:00
|
|
|
class ImageClient extends FriendicaClient {
|
|
|
|
ImageClient(super.credentials) : super();
|
|
|
|
|
|
|
|
FutureResult<ImageEntry, ExecError> editImageData(ImageEntry image) async {
|
|
|
|
_networkStatusService.startGalleryLoading();
|
|
|
|
final uri = Uri.parse('https://$serverName/api/friendica/photo/update');
|
|
|
|
final body = {
|
|
|
|
'album': image.album,
|
|
|
|
'desc': image.description,
|
|
|
|
'photo_id': image.id,
|
|
|
|
};
|
|
|
|
|
|
|
|
final result = await postUrl(uri, body, headers: _headers)
|
|
|
|
.andThen((_) => Result.ok(image));
|
|
|
|
_networkStatusService.finishGalleryLoading();
|
|
|
|
return result.execErrorCast();
|
|
|
|
}
|
|
|
|
|
|
|
|
FutureResult<ImageEntry, ExecError> deleteImage(ImageEntry image) async {
|
|
|
|
final uri = Uri.parse(
|
|
|
|
'https://$serverName/api/friendica/photo/delete?photo_id=${image.id}',
|
|
|
|
);
|
|
|
|
|
|
|
|
final result = await postUrl(uri, {}, headers: _headers)
|
|
|
|
.andThen((_) => Result.ok(image));
|
|
|
|
return result.execErrorCast();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-24 22:53:48 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2023-02-25 22:39:33 +00:00
|
|
|
FutureResult<TimelineEntry, ExecError> changeFavoriteStatus(
|
|
|
|
String id, bool status) async {
|
2023-02-24 22:53:48 +00:00
|
|
|
final action = status ? 'favourite' : 'unfavourite';
|
|
|
|
final url = Uri.parse('https://$serverName/api/v1/statuses/$id/$action');
|
2023-03-22 01:14:58 +00:00
|
|
|
final result = await postUrl(url, {}, headers: _headers);
|
2023-02-24 22:53:48 +00:00
|
|
|
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());
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-17 20:08:25 +00:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-24 22:53:48 +00:00
|
|
|
class NotificationsClient extends FriendicaClient {
|
|
|
|
static final _logger = Logger('$NotificationsClient');
|
|
|
|
|
|
|
|
NotificationsClient(super.credentials) : super();
|
|
|
|
|
|
|
|
FutureResult<PagedResponse<List<UserNotification>>, ExecError>
|
2023-02-25 22:39:33 +00:00
|
|
|
getNotifications(PagingData page) async {
|
2023-02-24 22:53:48 +00:00
|
|
|
_networkStatusService.startNotificationUpdate();
|
|
|
|
final url = 'https://$serverName/api/v1/notifications?include_all=true';
|
|
|
|
final request = Uri.parse('$url&${page.toQueryParameters()}');
|
|
|
|
_logger.finest(() => 'Getting new notifications');
|
2023-04-29 01:07:05 +00:00
|
|
|
final result =
|
|
|
|
await _getApiListRequest(request).transformAsync((response) async {
|
|
|
|
final notifications = <UserNotification>[];
|
|
|
|
|
|
|
|
final st = Stopwatch()..start();
|
|
|
|
for (final json in response.data) {
|
2023-05-08 18:24:45 +00:00
|
|
|
if (st.elapsedMilliseconds > maxProcessingMillis) {
|
|
|
|
await Future.delayed(processingSleep, () => st.reset());
|
2023-04-29 01:07:05 +00:00
|
|
|
}
|
|
|
|
notifications.add(NotificationMastodonExtension.fromJson(json));
|
|
|
|
}
|
|
|
|
return PagedResponse(notifications,
|
|
|
|
id: response.id, previous: response.previous, next: response.next);
|
|
|
|
});
|
2023-02-24 22:53:48 +00:00
|
|
|
_networkStatusService.finishNotificationUpdate();
|
2023-04-29 01:07:05 +00:00
|
|
|
return result.execErrorCast();
|
2023-02-24 22:53:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
FutureResult<bool, ExecError> clearNotifications() async {
|
|
|
|
final url = 'https://$serverName/api/v1/notifications/clear';
|
|
|
|
final request = Uri.parse(url);
|
|
|
|
_logger.finest(() => 'Clearing unread notification');
|
2023-03-22 01:14:58 +00:00
|
|
|
final response = await postUrl(request, {}, headers: _headers);
|
2023-02-24 22:53:48 +00:00
|
|
|
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);
|
2023-04-28 01:48:01 +00:00
|
|
|
_logger.fine(() => 'Clearing unread notification for $notification');
|
2023-03-22 01:14:58 +00:00
|
|
|
final response = await postUrl(request, {}, headers: _headers);
|
2023-02-24 22:53:48 +00:00
|
|
|
return response.mapValue((value) => true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class RelationshipsClient extends FriendicaClient {
|
|
|
|
static final _logger = Logger('$RelationshipsClient');
|
|
|
|
|
|
|
|
RelationshipsClient(super.credentials) : super();
|
2022-12-14 21:53:46 +00:00
|
|
|
|
2023-05-03 19:49:40 +00:00
|
|
|
FutureResult<PagedResponse<List<Connection>>, ExecError> getBlocks(
|
|
|
|
PagingData page) async {
|
|
|
|
_networkStatusService.startNotificationUpdate();
|
|
|
|
final url = 'https://$serverName/api/v1/blocks';
|
|
|
|
final request = Uri.parse('$url&${page.toQueryParameters()}');
|
|
|
|
_logger.finest(() => 'Getting blocks for $page');
|
|
|
|
final result =
|
|
|
|
await _getApiListRequest(request).transformAsync((response) async {
|
|
|
|
final blocks = <Connection>[];
|
|
|
|
|
|
|
|
final st = Stopwatch()..start();
|
|
|
|
for (final json in response.data) {
|
2023-05-08 18:24:45 +00:00
|
|
|
if (st.elapsedMilliseconds > maxProcessingMillis) {
|
|
|
|
await Future.delayed(processingSleep, () => st.reset());
|
2023-05-03 19:49:40 +00:00
|
|
|
}
|
|
|
|
blocks.add(
|
|
|
|
ConnectionMastodonExtensions.fromJson(json)
|
|
|
|
.copy(status: ConnectionStatus.blocked),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return PagedResponse(blocks,
|
|
|
|
id: response.id, previous: response.previous, next: response.next);
|
|
|
|
});
|
|
|
|
_networkStatusService.finishNotificationUpdate();
|
|
|
|
return result.execErrorCast();
|
|
|
|
}
|
|
|
|
|
2023-01-24 03:37:09 +00:00
|
|
|
FutureResult<PagedResponse<List<Connection>>, ExecError> getMyFollowing(
|
|
|
|
PagingData page) async {
|
2023-04-28 01:48:01 +00:00
|
|
|
_logger.fine(() => 'Getting following with paging data $page');
|
2023-01-31 20:27:26 +00:00
|
|
|
_networkStatusService.startConnectionUpdateStatus();
|
2023-02-27 03:12:40 +00:00
|
|
|
final myId = profile.userId;
|
2022-12-15 03:27:30 +00:00
|
|
|
final baseUrl = 'https://$serverName/api/v1/accounts/$myId';
|
2023-01-24 03:37:09 +00:00
|
|
|
final result = await _getApiListRequest(
|
|
|
|
Uri.parse('$baseUrl/following?${page.toQueryParameters()}'),
|
|
|
|
);
|
2023-01-31 20:27:26 +00:00
|
|
|
_networkStatusService.finishConnectionUpdateStatus();
|
2023-01-24 03:37:09 +00:00
|
|
|
return result
|
2023-02-25 22:39:33 +00:00
|
|
|
.andThenSuccess((response) => response.map((jsonArray) => jsonArray
|
|
|
|
.map((json) => ConnectionMastodonExtensions.fromJson(json))
|
|
|
|
.toList()))
|
2022-12-15 03:27:30 +00:00
|
|
|
.execErrorCast();
|
|
|
|
}
|
|
|
|
|
2023-03-21 18:27:38 +00:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2023-01-24 03:37:09 +00:00
|
|
|
FutureResult<PagedResponse<List<Connection>>, ExecError> getMyFollowers(
|
|
|
|
PagingData page) async {
|
|
|
|
_logger.finest(() => 'Getting followers data with page data $page');
|
2023-01-31 20:27:26 +00:00
|
|
|
_networkStatusService.startConnectionUpdateStatus();
|
2023-02-27 03:12:40 +00:00
|
|
|
final myId = profile.userId;
|
2022-12-15 03:27:30 +00:00
|
|
|
final baseUrl = 'https://$serverName/api/v1/accounts/$myId';
|
2023-01-24 03:37:09 +00:00
|
|
|
final result1 = await _getApiListRequest(
|
|
|
|
Uri.parse('$baseUrl/followers&${page.toQueryParameters()}'),
|
|
|
|
);
|
|
|
|
|
2023-01-31 20:27:26 +00:00
|
|
|
_networkStatusService.finishConnectionUpdateStatus();
|
2023-01-24 03:37:09 +00:00
|
|
|
return result1
|
2023-02-25 22:39:33 +00:00
|
|
|
.andThenSuccess((response) => response.map((jsonArray) => jsonArray
|
|
|
|
.map((json) => ConnectionMastodonExtensions.fromJson(json))
|
|
|
|
.toList()))
|
2022-12-15 03:27:30 +00:00
|
|
|
.execErrorCast();
|
|
|
|
}
|
|
|
|
|
2022-12-14 15:50:17 +00:00
|
|
|
FutureResult<Connection, ExecError> getConnectionWithStatus(
|
|
|
|
Connection connection) async {
|
|
|
|
_logger.finest(() => 'Getting group (Mastodon List) data');
|
2023-01-31 20:27:26 +00:00
|
|
|
_networkStatusService.startConnectionUpdateStatus();
|
2023-02-27 03:12:40 +00:00
|
|
|
final myId = profile.userId;
|
2022-12-14 15:50:17 +00:00
|
|
|
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 =
|
2023-02-25 22:39:33 +00:00
|
|
|
await _getApiListRequest(Uri.parse('$baseUrl/following$paging')).fold(
|
|
|
|
onSuccess: (followings) => followings.data.isNotEmpty,
|
|
|
|
onError: (error) {
|
|
|
|
_logger.severe('Error getting following list: $error');
|
|
|
|
return false;
|
|
|
|
});
|
2022-12-14 15:50:17 +00:00
|
|
|
final follower =
|
2023-02-25 22:39:33 +00:00
|
|
|
await _getApiListRequest(Uri.parse('$baseUrl/followers$paging')).fold(
|
|
|
|
onSuccess: (followings) => followings.data.isNotEmpty,
|
|
|
|
onError: (error) {
|
|
|
|
_logger.severe('Error getting follower list: $error');
|
|
|
|
return false;
|
|
|
|
});
|
2022-12-14 15:50:17 +00:00
|
|
|
|
|
|
|
var status = ConnectionStatus.none;
|
|
|
|
if (following && follower) {
|
|
|
|
status = ConnectionStatus.mutual;
|
|
|
|
} else if (following) {
|
|
|
|
status = ConnectionStatus.youFollowThem;
|
|
|
|
} else if (follower) {
|
|
|
|
status = ConnectionStatus.theyFollowYou;
|
|
|
|
}
|
2023-01-31 20:27:26 +00:00
|
|
|
|
|
|
|
_networkStatusService.finishConnectionUpdateStatus();
|
2022-12-14 15:50:17 +00:00
|
|
|
return Result.ok(connection.copy(status: status));
|
|
|
|
}
|
|
|
|
|
2023-01-31 21:41:13 +00:00
|
|
|
FutureResult<PagedResponse<List<Connection>>, ExecError>
|
2023-02-25 22:39:33 +00:00
|
|
|
getConnectionRequests(PagingData page) async {
|
2023-01-31 21:41:13 +00:00
|
|
|
_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
|
2023-02-25 22:39:33 +00:00
|
|
|
.andThenSuccess((response) => response.map((jsonArray) => jsonArray
|
|
|
|
.map((json) => ConnectionMastodonExtensions.fromJson(json))
|
|
|
|
.toList()))
|
2023-02-24 22:53:48 +00:00
|
|
|
.execErrorCast();
|
2022-12-08 18:37:30 +00:00
|
|
|
}
|
|
|
|
|
2023-02-24 22:53:48 +00:00
|
|
|
FutureResult<Connection, ExecError> acceptFollow(
|
|
|
|
Connection connection) async {
|
|
|
|
final id = connection.id;
|
|
|
|
final url =
|
2023-02-25 22:39:33 +00:00
|
|
|
Uri.parse('https://$serverName/api/v1/follow_requests/$id/authorize');
|
2023-03-22 01:14:58 +00:00
|
|
|
final result = await postUrl(url, {}, headers: _headers)
|
|
|
|
.andThenSuccessAsync((jsonString) async {
|
2023-02-24 22:53:48 +00:00
|
|
|
return _updateConnectionFromFollowRequestResult(connection, jsonString);
|
|
|
|
});
|
2023-02-25 22:39:33 +00:00
|
|
|
return result.mapError((error) => error is ExecError
|
2023-02-24 22:53:48 +00:00
|
|
|
? error
|
|
|
|
: ExecError(type: ErrorType.localError, message: error.toString()));
|
2022-11-09 02:26:03 +00:00
|
|
|
}
|
|
|
|
|
2023-02-24 22:53:48 +00:00
|
|
|
FutureResult<Connection, ExecError> rejectFollow(
|
|
|
|
Connection connection) async {
|
|
|
|
final id = connection.id;
|
2023-01-24 03:37:09 +00:00
|
|
|
final url =
|
2023-02-25 22:39:33 +00:00
|
|
|
Uri.parse('https://$serverName/api/v1/follow_requests/$id/reject');
|
2023-03-22 01:14:58 +00:00
|
|
|
final result = await postUrl(url, {}, headers: _headers)
|
|
|
|
.andThenSuccessAsync((jsonString) async {
|
2023-02-24 22:53:48 +00:00
|
|
|
return _updateConnectionFromFollowRequestResult(connection, jsonString);
|
|
|
|
});
|
2023-02-25 22:39:33 +00:00
|
|
|
return result.mapError((error) => error is ExecError
|
2023-02-24 22:53:48 +00:00
|
|
|
? error
|
|
|
|
: ExecError(type: ErrorType.localError, message: error.toString()));
|
2022-11-09 02:26:03 +00:00
|
|
|
}
|
|
|
|
|
2023-02-24 22:53:48 +00:00
|
|
|
FutureResult<Connection, ExecError> ignoreFollow(
|
|
|
|
Connection connection) async {
|
|
|
|
final id = connection.id;
|
|
|
|
final url =
|
2023-02-25 22:39:33 +00:00
|
|
|
Uri.parse('https://$serverName/api/v1/follow_requests/$id/ignore');
|
2023-03-22 01:14:58 +00:00
|
|
|
final result = await postUrl(url, {}, headers: _headers)
|
|
|
|
.andThenSuccessAsync((jsonString) async {
|
2023-02-24 22:53:48 +00:00
|
|
|
return _updateConnectionFromFollowRequestResult(connection, jsonString);
|
|
|
|
});
|
2023-02-25 22:39:33 +00:00
|
|
|
return result.mapError((error) => error is ExecError
|
2023-02-24 22:53:48 +00:00
|
|
|
? error
|
|
|
|
: ExecError(type: ErrorType.localError, message: error.toString()));
|
|
|
|
}
|
|
|
|
|
2023-05-03 19:49:40 +00:00
|
|
|
FutureResult<Connection, ExecError> blockConnection(
|
|
|
|
Connection connection) async {
|
|
|
|
final id = connection.id;
|
|
|
|
final url = Uri.parse('https://$serverName/api/v1/accounts/$id/block');
|
|
|
|
final result = await postUrl(url, {}, headers: _headers).transform(
|
|
|
|
(_) => connection.copy(status: ConnectionStatus.blocked),
|
|
|
|
);
|
|
|
|
return result.execErrorCast();
|
|
|
|
}
|
|
|
|
|
|
|
|
FutureResult<Connection, ExecError> unblockConnection(
|
|
|
|
Connection connection) async {
|
|
|
|
final id = connection.id;
|
|
|
|
final url = Uri.parse('https://$serverName/api/v1/accounts/$id/unblock');
|
|
|
|
final result = await postUrl(url, {}, headers: _headers)
|
|
|
|
.transformAsync((jsonString) async {
|
|
|
|
return _updateConnectionFromFollowRequestResult(connection, jsonString);
|
|
|
|
});
|
|
|
|
return result.execErrorCast();
|
|
|
|
}
|
|
|
|
|
2023-02-24 22:53:48 +00:00
|
|
|
FutureResult<Connection, ExecError> followConnection(
|
|
|
|
Connection connection) async {
|
|
|
|
final id = connection.id;
|
|
|
|
final url = Uri.parse('https://$serverName/api/v1/accounts/$id/follow');
|
2023-05-03 19:49:40 +00:00
|
|
|
final result =
|
|
|
|
await postUrl(url, {}, headers: _headers).transform((jsonString) {
|
2023-02-24 22:53:48 +00:00
|
|
|
return _updateConnectionFromFollowRequestResult(connection, jsonString);
|
|
|
|
});
|
2023-05-03 19:49:40 +00:00
|
|
|
return result.execErrorCast();
|
2023-02-24 22:53:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
FutureResult<Connection, ExecError> unFollowConnection(
|
|
|
|
Connection connection) async {
|
|
|
|
final id = connection.id;
|
|
|
|
final url = Uri.parse('https://$serverName/api/v1/accounts/$id/unfollow');
|
2023-05-03 19:49:40 +00:00
|
|
|
final result =
|
|
|
|
await postUrl(url, {}, headers: _headers).transform((jsonString) {
|
2023-02-24 22:53:48 +00:00
|
|
|
return _updateConnectionFromFollowRequestResult(connection, jsonString);
|
|
|
|
});
|
2023-05-03 19:49:40 +00:00
|
|
|
return result.execErrorCast();
|
2023-02-24 22:53:48 +00:00
|
|
|
}
|
|
|
|
|
2023-02-25 22:39:33 +00:00
|
|
|
Connection _updateConnectionFromFollowRequestResult(
|
|
|
|
Connection connection, String jsonString) {
|
2023-02-24 22:53:48 +00:00
|
|
|
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');
|
2022-12-27 17:21:56 +00:00
|
|
|
try {
|
|
|
|
final response = await http.get(
|
|
|
|
url,
|
|
|
|
headers: {
|
2023-02-27 03:12:40 +00:00
|
|
|
'Authorization': _profile.credentials.authHeaderValue,
|
2022-12-27 17:21:56 +00:00
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
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()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-24 22:53:48 +00:00
|
|
|
FutureResult<ImageEntry, ExecError> uploadFileAsAttachment({
|
|
|
|
required List<int> bytes,
|
|
|
|
String description = '',
|
|
|
|
String album = '',
|
|
|
|
String fileName = '',
|
2023-03-21 01:55:47 +00:00
|
|
|
required Visibility visibility,
|
2023-02-24 22:53:48 +00:00
|
|
|
}) async {
|
|
|
|
final postUri = Uri.parse('https://$serverName/api/friendica/photo/create');
|
|
|
|
final request = http.MultipartRequest('POST', postUri);
|
2023-02-27 03:12:40 +00:00
|
|
|
request.headers['Authorization'] = _profile.credentials.authHeaderValue;
|
2023-03-20 13:58:02 +00:00
|
|
|
if (usePhpDebugging) {
|
|
|
|
request.headers['Cookie'] = 'XDEBUG_SESSION=PHPSTORM;path=/';
|
|
|
|
}
|
2023-02-24 22:53:48 +00:00
|
|
|
request.fields['desc'] = description;
|
|
|
|
request.fields['album'] = album;
|
2023-03-21 01:55:47 +00:00
|
|
|
request.fields.addAll(visibility.toMapEntries());
|
|
|
|
request.files.add(http.MultipartFile.fromBytes(
|
2023-02-24 22:53:48 +00:00
|
|
|
'media',
|
|
|
|
filename: fileName,
|
|
|
|
contentType:
|
2023-02-25 22:39:33 +00:00
|
|
|
MediaType.parse('image/${ImageTypes.fromExtension(fileName).name}'),
|
2023-02-24 22:53:48 +00:00
|
|
|
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');
|
2023-03-17 13:20:24 +00:00
|
|
|
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',
|
|
|
|
));
|
2023-02-24 22:53:48 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class StatusesClient extends FriendicaClient {
|
|
|
|
static final _logger = Logger('$StatusesClient');
|
|
|
|
|
|
|
|
StatusesClient(super.credentials) : super();
|
|
|
|
|
2023-01-24 03:37:09 +00:00
|
|
|
// TODO Convert getPostOrComment to using paging for real
|
2022-11-17 16:04:14 +00:00
|
|
|
FutureResult<List<TimelineEntry>, ExecError> getPostOrComment(String id,
|
|
|
|
{bool fullContext = false}) async {
|
2023-03-19 21:42:25 +00:00
|
|
|
_networkStatusService.startTimelineLoading();
|
|
|
|
final result = (await runCatchingAsync(() async {
|
2022-11-17 16:04:14 +00:00
|
|
|
final baseUrl = 'https://$serverName/api/v1/statuses/$id';
|
|
|
|
final url = fullContext ? '$baseUrl/context' : baseUrl;
|
2022-12-19 14:43:06 +00:00
|
|
|
final request = Uri.parse('$url?limit=1000');
|
2022-11-21 21:21:45 +00:00
|
|
|
_logger.finest(() =>
|
2023-02-25 22:39:33 +00:00
|
|
|
'Getting entry for status $id, full context? $fullContext : $url');
|
2022-11-17 16:04:14 +00:00
|
|
|
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)];
|
|
|
|
}
|
|
|
|
}));
|
2023-03-19 21:42:25 +00:00
|
|
|
}));
|
|
|
|
_networkStatusService.finishTimelineLoading();
|
|
|
|
|
|
|
|
return result.execErrorCast();
|
2022-11-17 16:04:14 +00:00
|
|
|
}
|
|
|
|
|
2022-12-26 20:26:30 +00:00
|
|
|
FutureResult<TimelineEntry, ExecError> createNewStatus({
|
|
|
|
required String text,
|
|
|
|
String spoilerText = '',
|
|
|
|
String inReplyToId = '',
|
|
|
|
List<String> mediaIds = const [],
|
2023-03-21 01:55:47 +00:00
|
|
|
required Visibility visibility,
|
2022-12-26 20:26:30 +00:00
|
|
|
}) async {
|
2022-11-23 02:59:08 +00:00
|
|
|
_logger.finest(() =>
|
2023-02-25 22:39:33 +00:00
|
|
|
'Creating status ${inReplyToId.isNotEmpty ? "In Reply to: " : ""} $inReplyToId, with media: $mediaIds');
|
2022-11-17 16:04:14 +00:00
|
|
|
final url = Uri.parse('https://$serverName/api/v1/statuses');
|
2022-11-22 18:55:50 +00:00
|
|
|
final body = {
|
|
|
|
'status': text,
|
|
|
|
if (spoilerText.isNotEmpty) 'spoiler_text': spoilerText,
|
2022-11-23 02:59:08 +00:00
|
|
|
if (inReplyToId.isNotEmpty) 'in_reply_to_id': inReplyToId,
|
2022-12-26 20:26:30 +00:00
|
|
|
if (mediaIds.isNotEmpty) 'media_ids': mediaIds,
|
2023-03-21 01:55:47 +00:00
|
|
|
'visibility': visibility.toCreateStatusValue(),
|
2023-03-16 15:37:46 +00:00
|
|
|
'friendica': {
|
|
|
|
'title': '',
|
|
|
|
},
|
2022-11-22 18:55:50 +00:00
|
|
|
};
|
2023-03-22 01:14:58 +00:00
|
|
|
final result = await postUrl(url, body, headers: _headers);
|
2022-11-17 16:04:14 +00:00
|
|
|
if (result.isFailure) {
|
2022-11-22 16:43:16 +00:00
|
|
|
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());
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-03-16 15:37:46 +00:00
|
|
|
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': '',
|
|
|
|
},
|
|
|
|
};
|
2023-03-22 01:14:58 +00:00
|
|
|
final result = await putUrl(url, body, headers: _headers);
|
2023-03-16 15:37:46 +00:00
|
|
|
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());
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-11-22 16:43:16 +00:00
|
|
|
FutureResult<TimelineEntry, ExecError> resharePost(String id) async {
|
|
|
|
_logger.finest(() => 'Reshare post $id');
|
|
|
|
final url = Uri.parse('https://$serverName/api/v1/statuses/$id/reblog');
|
2023-03-22 01:14:58 +00:00
|
|
|
final result = await postUrl(url, {}, headers: _headers);
|
2022-11-22 16:43:16 +00:00
|
|
|
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');
|
2023-03-22 01:14:58 +00:00
|
|
|
final result = await postUrl(url, {}, headers: _headers);
|
2022-11-22 16:43:16 +00:00
|
|
|
if (result.isFailure) {
|
2022-11-17 16:04:14 +00:00
|
|
|
return result.errorCast();
|
|
|
|
}
|
|
|
|
|
|
|
|
final responseText = result.value;
|
|
|
|
|
|
|
|
return runCatching<TimelineEntry>(() {
|
|
|
|
final json = jsonDecode(responseText);
|
|
|
|
return Result.ok(TimelineEntryMastodonExtensions.fromJson(json));
|
|
|
|
}).mapError((error) {
|
2022-11-18 21:50:15 +00:00
|
|
|
return ExecError(type: ErrorType.parsingError, message: error.toString());
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-02-24 22:53:48 +00:00
|
|
|
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);
|
2023-03-22 01:14:58 +00:00
|
|
|
return (await deleteUrl(
|
|
|
|
request,
|
|
|
|
{},
|
|
|
|
headers: _headers,
|
|
|
|
))
|
|
|
|
.mapValue((_) => true);
|
2022-11-17 16:04:14 +00:00
|
|
|
}
|
2023-02-24 22:53:48 +00:00
|
|
|
}
|
2022-11-17 16:04:14 +00:00
|
|
|
|
2023-03-22 04:16:23 +00:00
|
|
|
class SearchClient extends FriendicaClient {
|
|
|
|
static final _logger = Logger('$StatusesClient');
|
|
|
|
|
|
|
|
SearchClient(super.credentials) : super();
|
|
|
|
|
|
|
|
FutureResult<PagedResponse<SearchResults>, ExecError> search(
|
|
|
|
SearchTypes type, String searchTerm, PagingData page) async {
|
|
|
|
_logger.finest(() => 'Searching $type for term: $searchTerm');
|
2023-03-22 13:13:19 +00:00
|
|
|
|
|
|
|
if (type == SearchTypes.directLink) {
|
|
|
|
final isFediverseResult = await getIt<FediverseServiceValidator>()
|
|
|
|
.checkIfFediverseLink(searchTerm);
|
|
|
|
if (isFediverseResult.isFailure) {
|
|
|
|
return isFediverseResult.errorCast();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!isFediverseResult.value) {
|
|
|
|
return buildErrorResult(
|
|
|
|
type: ErrorType.parsingError,
|
|
|
|
message: 'URL appears to not be to a fediverse server: $searchTerm',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2023-03-22 04:16:23 +00:00
|
|
|
_networkStatusService.startSearchLoading();
|
|
|
|
final url =
|
|
|
|
'https://$serverName/api/v1/search?${page.toQueryParameters()}&${type.toQueryParameters()}&q=$searchTerm';
|
|
|
|
final result = await _getApiPagedRequest(
|
|
|
|
Uri.parse(url),
|
|
|
|
);
|
|
|
|
|
|
|
|
_networkStatusService.finishSearchLoaing();
|
|
|
|
return result
|
|
|
|
.andThenSuccess((response) => response
|
|
|
|
.map((json) => SearchResultMastodonExtensions.fromJson(json)))
|
|
|
|
.execErrorCast();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-24 22:53:48 +00:00
|
|
|
class TimelineClient extends FriendicaClient {
|
|
|
|
static final _logger = Logger('$TimelineClient');
|
2022-12-19 18:59:33 +00:00
|
|
|
|
2023-02-24 22:53:48 +00:00
|
|
|
TimelineClient(super.credentials) : super();
|
2022-12-19 18:59:33 +00:00
|
|
|
|
2023-02-24 22:53:48 +00:00
|
|
|
// 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);
|
2023-04-29 01:07:05 +00:00
|
|
|
final result = await _getApiListRequest(request).transformAsync(
|
|
|
|
(postsJson) async => await _timelineEntriesFromJson(postsJson.data),
|
|
|
|
);
|
|
|
|
|
2023-02-24 22:53:48 +00:00
|
|
|
_networkStatusService.finishTimelineLoading();
|
2023-04-29 01:07:05 +00:00
|
|
|
return result.execErrorCast();
|
2022-12-14 15:50:17 +00:00
|
|
|
}
|
|
|
|
|
2023-02-24 22:53:48 +00:00
|
|
|
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 =
|
2023-02-25 22:39:33 +00:00
|
|
|
'$baseUrl?exclude_replies=true&${page.toQueryParameters()}&$timelineQPs';
|
2023-02-24 22:53:48 +00:00
|
|
|
final request = Uri.parse(url);
|
|
|
|
_logger.finest(() => 'Getting ${type.toHumanKey()} with paging data $page');
|
2023-04-29 01:07:05 +00:00
|
|
|
final result = await _getApiListRequest(request).transformAsync(
|
|
|
|
(postsJson) async => await _timelineEntriesFromJson(postsJson.data),
|
|
|
|
);
|
2023-02-24 22:53:48 +00:00
|
|
|
_networkStatusService.finishTimelineLoading();
|
2023-04-29 01:07:05 +00:00
|
|
|
return result.execErrorCast();
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<TimelineEntry>> _timelineEntriesFromJson(
|
|
|
|
List<dynamic> postsJson,
|
|
|
|
) async {
|
|
|
|
final finalResult = <TimelineEntry>[];
|
|
|
|
final st = Stopwatch()..start();
|
|
|
|
for (final json in postsJson) {
|
2023-05-08 18:24:45 +00:00
|
|
|
if (st.elapsedMilliseconds > maxProcessingMillis) {
|
|
|
|
await Future.delayed(processingSleep, () => st.reset());
|
2023-04-29 01:07:05 +00:00
|
|
|
}
|
|
|
|
finalResult.add(TimelineEntryMastodonExtensions.fromJson(json));
|
|
|
|
}
|
|
|
|
|
|
|
|
return finalResult;
|
2022-12-14 15:50:17 +00:00
|
|
|
}
|
|
|
|
|
2023-02-24 22:53:48 +00:00
|
|
|
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:
|
2023-02-27 03:12:40 +00:00
|
|
|
final myId = profile.userId;
|
2023-02-24 22:53:48 +00:00
|
|
|
return '/accounts/$myId/statuses';
|
|
|
|
}
|
2022-11-09 02:26:03 +00:00
|
|
|
}
|
|
|
|
|
2023-02-24 22:53:48 +00:00
|
|
|
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';
|
2022-12-26 20:26:30 +00:00
|
|
|
}
|
2023-02-24 22:53:48 +00:00
|
|
|
}
|
|
|
|
}
|
2022-12-26 20:26:30 +00:00
|
|
|
|
2023-02-24 22:53:48 +00:00
|
|
|
abstract class FriendicaClient {
|
2023-02-27 03:12:40 +00:00
|
|
|
final Profile _profile;
|
2023-02-08 15:41:29 +00:00
|
|
|
|
2023-02-24 22:53:48 +00:00
|
|
|
late final NetworkStatusService _networkStatusService;
|
2023-01-25 01:53:55 +00:00
|
|
|
|
2023-02-27 03:12:40 +00:00
|
|
|
String get serverName => _profile.serverName;
|
2023-01-25 17:26:29 +00:00
|
|
|
|
2023-02-27 03:12:40 +00:00
|
|
|
Profile get profile => _profile;
|
2023-01-25 18:06:46 +00:00
|
|
|
|
2023-02-27 03:12:40 +00:00
|
|
|
FriendicaClient(Profile credentials) : _profile = credentials {
|
2023-02-24 22:53:48 +00:00
|
|
|
_networkStatusService = getIt<NetworkStatusService>();
|
2023-01-25 18:06:46 +00:00
|
|
|
}
|
|
|
|
|
2023-01-24 03:37:09 +00:00
|
|
|
FutureResult<PagedResponse<List<dynamic>>, ExecError> _getApiListRequest(
|
|
|
|
Uri url) async {
|
2023-05-09 20:42:53 +00:00
|
|
|
return await getUrl(url, headers: _headers).transformAsync(
|
|
|
|
(response) async {
|
|
|
|
return response.map((data) => jsonDecode(data) as List<dynamic>);
|
|
|
|
},
|
|
|
|
).execErrorCastAsync();
|
2022-11-09 02:26:03 +00:00
|
|
|
}
|
|
|
|
|
2023-03-22 04:16:23 +00:00
|
|
|
FutureResult<PagedResponse<dynamic>, ExecError> _getApiPagedRequest(
|
|
|
|
Uri url) async {
|
2023-05-09 20:42:53 +00:00
|
|
|
return await getUrl(url, headers: _headers).transformAsync(
|
|
|
|
(response) async {
|
|
|
|
return response.map((data) => jsonDecode(data));
|
|
|
|
},
|
|
|
|
).execErrorCastAsync();
|
2023-03-22 04:16:23 +00:00
|
|
|
}
|
|
|
|
|
2022-11-09 02:26:03 +00:00
|
|
|
FutureResult<dynamic, ExecError> _getApiRequest(Uri url) async {
|
2023-05-09 20:42:53 +00:00
|
|
|
return await getUrl(url, headers: _headers).transformAsync(
|
|
|
|
(response) async {
|
|
|
|
return jsonDecode(response.data);
|
|
|
|
},
|
|
|
|
).execErrorCastAsync();
|
2022-11-09 02:26:03 +00:00
|
|
|
}
|
2023-03-20 13:58:02 +00:00
|
|
|
|
2023-03-22 01:14:58 +00:00
|
|
|
Map<String, String> get _headers => {
|
2023-03-20 13:58:02 +00:00
|
|
|
'Authorization': _profile.credentials.authHeaderValue,
|
|
|
|
'Content-Type': 'application/json; charset=UTF-8',
|
|
|
|
if (usePhpDebugging) 'Cookie': 'XDEBUG_SESSION=PHPSTORM;path=/',
|
|
|
|
};
|
2022-11-09 02:26:03 +00:00
|
|
|
}
|