Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 87 additions & 57 deletions lib/src/api/client.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import 'dart:async';
import 'dart:convert';

import 'package:http/http.dart' as http;
import 'package:http_parser/http_parser.dart';
import 'package:image_picker/image_picker.dart';
import 'package:interstellar/src/controller/server.dart';
import 'package:interstellar/src/utils/utils.dart';
import 'package:mime/mime.dart';
import 'package:path/path.dart';

class RestrictedAuthException implements Exception {
RestrictedAuthException(this.message, this.uri);
Expand Down Expand Up @@ -35,72 +40,91 @@ class ServerClient {

Future<http.Response> get(
String path, {
Map<String, String>? headers,
JsonMap? body,
Map<String, String?>? queryParams,
}) =>
send('GET', path, headers: headers, body: body, queryParams: queryParams);
JsonMap? body,
}) => _send('GET', path, queryParams: queryParams, body: body);

Future<http.Response> post(
String path, {
Map<String, String>? headers,
JsonMap? body,
Map<String, String?>? queryParams,
}) => send(
'POST',
path,
headers: headers,
body: body,
queryParams: queryParams,
);
JsonMap? body,
}) => _send('POST', path, queryParams: queryParams, body: body);

Future<http.Response> put(
String path, {
Map<String, String>? headers,
JsonMap? body,
Map<String, String?>? queryParams,
}) =>
send('PUT', path, headers: headers, body: body, queryParams: queryParams);
JsonMap? body,
}) => _send('PUT', path, queryParams: queryParams, body: body);

Future<http.Response> delete(
String path, {
Map<String, String>? headers,
Map<String, String?>? queryParams,
JsonMap? body,
}) => _send('DELETE', path, queryParams: queryParams, body: body);

Future<http.Response> _send(
String method,
String path, {
Map<String, String?>? queryParams,
}) => send(
'DELETE',
JsonMap? body,
}) async {
final request = http.Request(method, _uri(path, queryParams: queryParams));

if (body != null) {
request.body = jsonEncode(body);
request.headers['Content-Type'] = 'application/json';
}

return _sendRequest(request);
}

Future<http.Response> postMultipart(
String path, {
Map<String, String?>? queryParams,
Map<String, String>? fields,
Map<String, XFile>? files,
}) => _sendMultipart(
'POST',
path,
headers: headers,
body: body,
queryParams: queryParams,
fields: fields,
files: files,
);

Future<http.Response> send(
Future<http.Response> _sendMultipart(
String method,
String path, {
Map<String, String>? headers,
JsonMap? body,
Map<String, String?>? queryParams,
Map<String, String>? fields,
Map<String, XFile>? files,
}) async {
final request = http.Request(
final request = http.MultipartRequest(
method,
Uri.https(
domain,
software.apiPathPrefix + path,
queryParams == null ? null : _normalizeQueryParams(queryParams),
),
_uri(path, queryParams: queryParams),
);

if (body != null) {
request.body = jsonEncode(body);
request.headers['Content-Type'] = 'application/json';
if (fields != null) request.fields.addAll(fields);
for (final entry in (files ?? {}).entries) {
final name = entry.key;
final file = entry.value;

final filename = basename(file.path);
final mime = lookupMimeType(filename);

request.files.add(
http.MultipartFile.fromBytes(
name,
await file.readAsBytes(),
filename: filename,
contentType: mime == null ? null : MediaType.parse(mime),
),
);
}
if (headers != null) request.headers.addAll(headers);

return sendRequest(request);
return _sendRequest(request);
}

Future<http.Response> sendRequest(http.BaseRequest request) async {
Future<http.Response> _sendRequest(http.BaseRequest request) async {
final response = await http.Response.fromStream(
await httpClient.send(request),
);
Expand All @@ -110,6 +134,12 @@ class ServerClient {
return response;
}

Uri _uri(String path, {Map<String, String?>? queryParams}) => Uri.https(
domain,
software.apiPathPrefix + path,
queryParams == null ? null : _normalizeQueryParams(queryParams),
);

/// Remove null and empty values.
Map<String, String> _normalizeQueryParams(Map<String, String?> queryParams) =>
Map<String, String>.from(
Expand All @@ -120,26 +150,6 @@ class ServerClient {
),
);

/// Throws an error if [response] is not successful.
static void checkResponseSuccess(Uri url, http.Response response) {
if (response.statusCode < 400) return;
if (response.statusCode == 401) {
throw RestrictedAuthException(response.body, url);
}

var message = 'Request failed with status ${response.statusCode}';

if (response.reasonPhrase != null) {
message = '$message: ${response.reasonPhrase}';
}

if (response.body.isNotEmpty) {
message = '$message: ${response.body}';
}

throw http.ClientException(message, url);
}

Future<List<(String, int)>> languageCodeIdPairs() async {
if (_langCodeIdPairs == null) {
List<dynamic> allLanguages;
Expand Down Expand Up @@ -180,6 +190,26 @@ class ServerClient {
}
return null;
}

/// Throws an error if [response] is not successful.
static void checkResponseSuccess(Uri url, http.Response response) {
if (response.statusCode < 400) return;
if (response.statusCode == 401) {
throw RestrictedAuthException(response.body, url);
}

var message = 'Request failed with status ${response.statusCode}';

if (response.reasonPhrase != null) {
message = '$message: ${response.reasonPhrase}';
}

if (response.body.isNotEmpty) {
message = '$message: ${response.body}';
}

throw http.ClientException(message, url);
}
}

extension BodyJson on http.Response {
Expand Down
25 changes: 9 additions & 16 deletions lib/src/api/comments.dart
Original file line number Diff line number Diff line change
Expand Up @@ -299,23 +299,16 @@ class APIComments {
final path =
'/${_postTypeMbin[postType]}/$postId/comments${parentCommentId != null ? '/$parentCommentId/reply' : ''}/image';

final request = http.MultipartRequest(
'POST',
Uri.https(client.domain, client.software.apiPathPrefix + path),
);
final file = http.MultipartFile.fromBytes(
'uploadImage',
await image.readAsBytes(),
filename: basename(image.path),
contentType: MediaType.parse(lookupMimeType(image.path)!),
final response = await client.postMultipart(
path,
fields: {
'body': body,
'lang': lang,
'isAdult': isAdult.toString(),
'alt': alt ?? '',
},
files: {'uploadImage': image},
);
request.files.add(file);
request.fields['body'] = body;
request.fields['lang'] = lang;
request.fields['isAdult'] = isAdult.toString();
request.fields['alt'] = alt ?? '';

final response = await client.sendRequest(request);

return CommentModel.fromMbin(response.bodyJson);
}
Expand Down
42 changes: 16 additions & 26 deletions lib/src/api/images.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:http_parser/http_parser.dart';
import 'package:image_picker/image_picker.dart';
import 'package:interstellar/src/api/client.dart';
import 'package:interstellar/src/controller/server.dart';
import 'package:interstellar/src/utils/globals.dart';
import 'package:interstellar/src/utils/utils.dart';
import 'package:mime/mime.dart';
import 'package:path/path.dart';
Expand Down Expand Up @@ -35,19 +36,10 @@ class APIImages {
case ServerSoftware.lemmy:
const path = '/pictrs/image';

final request = http.MultipartRequest(
'POST',
Uri.https(client.domain, path),
final response = await client.postMultipart(
path,
files: {'images[]': image},
);
final file = http.MultipartFile.fromBytes(
'images[]',
await image.readAsBytes(),
filename: basename(image.path),
contentType: MediaType.parse(lookupMimeType(image.path)!),
);
request.files.add(file);

final response = await client.sendRequest(request);

final imageName =
((response.bodyJson['files']! as List<dynamic>).first
Expand All @@ -59,19 +51,10 @@ class APIImages {
case ServerSoftware.piefed:
const path = '/upload/image';

final request = http.MultipartRequest(
'POST',
Uri.https(client.domain, client.software.apiPathPrefix + path),
final response = await client.postMultipart(
path,
files: {'file': image},
);
final file = http.MultipartFile.fromBytes(
'file',
await image.readAsBytes(),
filename: basename(image.path),
contentType: MediaType.parse(lookupMimeType(image.path)!),
);
request.files.add(file);

final response = await client.sendRequest(request);

return response.bodyJson['url']! as String;
}
Expand All @@ -89,7 +72,11 @@ class APIImages {
request.fields['reqtype'] = 'fileupload';
request.files.add(file);

final response = await client.sendRequest(request);
final response = await http.Response.fromStream(
await appHttpClient.send(request),
);
ServerClient.checkResponseSuccess(request.url, response);

return response.body;
case ImageStore.imgLink:
const path = 'https://imglink.io/upload';
Expand All @@ -104,7 +91,10 @@ class APIImages {

request.files.add(file);

final response = await client.sendRequest(request);
final response = await http.Response.fromStream(
await appHttpClient.send(request),
);
ServerClient.checkResponseSuccess(request.url, response);

return ((response.bodyJson['images']! as List<dynamic>).first
as JsonMap)['direct_link']!
Expand Down
25 changes: 9 additions & 16 deletions lib/src/api/microblogs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -116,23 +116,16 @@ class MbinAPIMicroblogs {
}) async {
final path = '/magazine/$communityId/posts/image';

final request = http.MultipartRequest(
'POST',
Uri.https(client.domain, client.software.apiPathPrefix + path),
);

final multipartFile = http.MultipartFile.fromBytes(
'uploadImage',
await image.readAsBytes(),
filename: image.name,
contentType: MediaType.parse(image.mimeType!),
final response = await client.postMultipart(
path,
fields: {
'body': body,
'lang': lang,
'isAdult': isAdult.toString(),
'alt': alt,
},
files: {'uploadImage': image},
);
request.files.add(multipartFile);
request.fields['body'] = body;
request.fields['lang'] = lang;
request.fields['isAdult'] = isAdult.toString();
request.fields['alt'] = alt;
final response = await client.sendRequest(request);

return PostModel.fromMbinPost(response.bodyJson);
}
Expand Down
Loading