relatica/lib/friendica_client.dart

352 wiersze
13 KiB
Dart

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:logging/logging.dart';
import 'package:result_monad/result_monad.dart';
import 'models/TimelineIdentifiers.dart';
import 'models/connection.dart';
import 'models/credentials.dart';
import 'models/exec_error.dart';
import 'models/group_data.dart';
import 'models/timeline_entry.dart';
import 'models/user_notification.dart';
import 'serializers/friendica/connection_friendica_extensions.dart';
import 'serializers/mastodon/group_data_mastodon_extensions.dart';
import 'serializers/mastodon/notification_mastodon_extension.dart';
import 'serializers/mastodon/timeline_entry_mastodon_extensions.dart';
class FriendicaClient {
static final _logger = Logger('$FriendicaClient');
final Credentials _credentials;
late final String _authHeader;
String get serverName => _credentials.serverName;
Credentials get credentials => _credentials;
FriendicaClient({required Credentials credentials})
: _credentials = credentials {
final authenticationString =
"${_credentials.username}:${_credentials.password}";
final encodedAuthString = base64Encode(utf8.encode(authenticationString));
_authHeader = "Basic $encodedAuthString";
}
FutureResult<List<UserNotification>, ExecError> getNotifications() async {
final url = 'https://$serverName/api/v1/notifications?include_all=true';
final request = Uri.parse(url);
_logger.finest(() => 'Getting new notifications');
return (await _getApiListRequest(request).andThenSuccessAsync(
(notificationsJson) async => notificationsJson
.map((json) => NotificationMastodonExtension.fromJson(json))
.toList()))
.mapError((error) {
if (error is ExecError) {
return error;
}
return ExecError(type: ErrorType.localError, message: error.toString());
});
}
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);
}
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
.map((json) => GroupDataMastodonExtensions.fromJson(json))
.toList()))
.mapError((error) => error is ExecError
? error
: ExecError(type: ErrorType.localError, message: error.toString()));
}
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
.map((json) => GroupDataMastodonExtensions.fromJson(json))
.toList()))
.mapError((error) => error as ExecError);
}
FutureResult<List<TimelineEntry>, ExecError> getUserTimeline(
{String userId = '', int page = 1, int count = 10}) async {
_logger.finest(() => 'Getting user timeline for $userId');
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);
return (await _getApiListRequest(request).andThenSuccessAsync(
(postsJson) async => postsJson
.map((json) => TimelineEntryMastodonExtensions.fromJson(json))
.toList()))
.mapError((error) => error as ExecError);
}
FutureResult<List<TimelineEntry>, ExecError> getTimeline(
{required TimelineIdentifiers type,
int sinceId = 0,
int maxId = 0,
int limit = 20}) async {
final String timelinePath = _typeToTimelinePath(type);
final String timelineQPs = _typeToTimelineQueryParameters(type);
final baseUrl = 'https://$serverName/api/v1/$timelinePath';
var pagingData = 'limit=$limit';
if (maxId > 0) {
pagingData = '$pagingData&max_id=$maxId';
}
if (sinceId > 0) {
pagingData = '&since_id=$sinceId';
}
final url = '$baseUrl?exclude_replies=true&$pagingData&$timelineQPs';
final request = Uri.parse(url);
_logger.finest(() =>
'Getting ${type.toHumanKey()} limit $limit sinceId: $sinceId maxId: $maxId : $url');
return (await _getApiListRequest(request).andThenSuccessAsync(
(postsJson) async => postsJson
.map((json) => TimelineEntryMastodonExtensions.fromJson(json))
.toList()))
.mapError((error) => error as ExecError);
}
FutureResult<List<TimelineEntry>, ExecError> getPostOrComment(String id,
{bool fullContext = false}) async {
return (await runCatchingAsync(() async {
final baseUrl = 'https://$serverName/api/v1/statuses/$id';
final url = fullContext ? '$baseUrl/context' : baseUrl;
final request = Uri.parse(url);
_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)];
}
}));
}))
.mapError((error) => ExecError(
type: ErrorType.parsingError,
message: error.toString(),
));
}
FutureResult<TimelineEntry, ExecError> createNewStatus(
{required String text,
String spoilerText = '',
String inReplyToId = ''}) async {
_logger.finest(() =>
'Creating status ${inReplyToId.isNotEmpty ? "In Reply to: " : ""} $inReplyToId');
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,
};
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> 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<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());
});
}
FutureResult<Connection, ExecError> getMyProfile() async {
_logger.finest(() => 'Getting logged in user profile');
final request = Uri.parse('https://$serverName/api/friendica/profile/show');
return (await _getApiRequest(request)).mapValue((json) =>
ConnectionFriendicaExtensions.fromJson(json['friendica_owner'])
.copy(status: ConnectionStatus.you, network: 'friendica'));
}
FutureResult<String, ExecError> _getUrl(Uri url) async {
_logger.finest('GET: $url');
try {
final response = await http.get(
url,
headers: {
'Authorization': _authHeader,
'Content-Type': 'application/json; charset=UTF-8'
},
);
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> _postUrl(
Uri url, Map<String, dynamic> body) async {
_logger.finest('POST: $url');
try {
final response = await http.post(
url,
headers: {
'Authorization': _authHeader,
'Content-Type': 'application/json; charset=UTF-8'
},
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<List<dynamic>, ExecError> _getApiListRequest(Uri url) async {
return (await _getUrl(url).andThenSuccessAsync(
(jsonText) async => jsonDecode(jsonText) as List<dynamic>))
.mapError((error) => error as ExecError);
}
FutureResult<dynamic, ExecError> _getApiRequest(Uri url) async {
return (await _getUrl(url)
.andThenSuccessAsync((jsonText) async => jsonDecode(jsonText)))
.mapError((error) => error as ExecError);
}
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:
throw UnimplementedError('These types are not supported yet');
}
}
String _typeToTimelineQueryParameters(TimelineIdentifiers type) {
switch (type.timeline) {
case TimelineType.home:
case TimelineType.global:
case TimelineType.profile:
case TimelineType.group:
return '';
case TimelineType.local:
return 'local=true';
case TimelineType.self:
throw UnimplementedError('These types are not supported yet');
}
}
}