Skip to content

Commit

Permalink
[ 0.9.0 Release ] Add support for uploading images through SubredditS…
Browse files Browse the repository at this point in the history
…tyleSheet (#167)
  • Loading branch information
bkonyi authored Nov 22, 2019
1 parent 4159e0a commit 94e7525
Show file tree
Hide file tree
Showing 24 changed files with 286 additions and 79 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
Change Log
=================================

## Version 0.9.0 (2019/11/22)

### Breaking changes:
* HTTP methods exposed through `Reddit` now only accept `Map<String, String>` for
body arguments.

### Other changes:
* Added support for uploading images for subreddit theming in `SubredditStyleSheet`.

## Version 0.8.0 (2019/10/01)
* Added support for Dart for Web and Flutter Web.

Expand Down
30 changes: 18 additions & 12 deletions lib/src/auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';

import 'package:http/http.dart' as http;
import 'package:http_parser/http_parser.dart';
import 'package:oauth2/oauth2.dart' as oauth2;
import "package:oauth2/src/handle_access_token_response.dart";

Expand Down Expand Up @@ -148,16 +150,18 @@ abstract class Authenticator {
///
/// [path] is the destination URI and [body] contains the POST parameters
/// that will be sent with the request.
Future<dynamic> post(Uri path, Map<String, String> body) async {
Future<dynamic> post(Uri path, Map<String, String> body,
{Map<String, Uint8List> files, Map params}) async {
_logger.info('POST: $path body: ${DRAWLoggingUtils.jsonify(body)}');
return _request(_kPostRequest, path, body: body);
return _request(_kPostRequest, path,
body: body, files: files, params: params);
}

/// Make a simple `PUT` request.
///
/// [path] is the destination URI and [body] contains the PUT parameters that
/// will be sent with the request.
Future<dynamic> put(Uri path, {/* Map<String,String>, String */ body}) async {
Future<dynamic> put(Uri path, {Map<String, String> body}) async {
_logger.info('PUT: $path body: ${DRAWLoggingUtils.jsonify(body)}');
return _request(_kPutRequest, path, body: body);
}
Expand All @@ -166,8 +170,7 @@ abstract class Authenticator {
///
/// [path] is the destination URI and [body] contains the DELETE parameters
/// that will be sent with the request.
Future<dynamic> delete(Uri path,
{/* Map<String,String>, String */ body}) async {
Future<dynamic> delete(Uri path, {Map<String, String> body}) async {
_logger.info('DELETE: $path body: ${DRAWLoggingUtils.jsonify(body)}');
return _request(_kDeleteRequest, path, body: body);
}
Expand All @@ -178,8 +181,9 @@ abstract class Authenticator {
/// request parameters. [body] is an optional parameter which contains the
/// body fields for a POST request.
Future<dynamic> _request(String type, Uri path,
{/* Map<String,String>, String */ body,
{Map<String, String> body,
Map<String, String> params,
Map<String, Uint8List> files,
bool followRedirects = false}) async {
if (_client == null) {
throw DRAWAuthenticationError(
Expand All @@ -189,7 +193,7 @@ abstract class Authenticator {
await refresh();
}
final finalPath = path.replace(queryParameters: params);
final request = http.Request(type, finalPath);
final request = http.MultipartRequest(type, finalPath);

// Some API requests initiate a redirect (i.e., random submission from a
// subreddit) but the redirect doesn't forward the OAuth credentials
Expand All @@ -198,11 +202,13 @@ abstract class Authenticator {
request.followRedirects = followRedirects;

if (body != null) {
if (body is Map<String, String>) {
request.bodyFields = body;
} else {
request.body = body;
}
request.fields.addAll(body);
}
if (files != null) {
request.files.addAll([
for (final key in files.keys)
http.MultipartFile.fromBytes(key, files[key], filename: 'filename')
]);
}
http.StreamedResponse responseStream;
try {
Expand Down
8 changes: 8 additions & 0 deletions lib/src/exceptions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,11 @@ class DRAWUnknownResponseException implements Exception {
DRAWUnknownResponseException(this.status, this.message);
String toString() => 'DRAWUnknownResponse: $message (status: $status)';
}

/// Thrown if an image upload fails.
class DRAWImageUploadException implements Exception {
final String error;
final String message;
DRAWImageUploadException(this.error, this.message);
String toString() => 'DRAWImageUploadException: ($error) $message';
}
7 changes: 7 additions & 0 deletions lib/src/image_file_reader.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/// Copyright (c) 2019, the Dart Reddit API Wrapper project authors.
/// Please see the AUTHORS file for details. All rights reserved.
/// Use of this source code is governed by a BSD-style license that
/// can be found in the LICENSE file.
export 'image_file_reader_unsupported.dart'
if (dart.library.io) 'image_file_reader_io.dart';
28 changes: 28 additions & 0 deletions lib/src/image_file_reader_io.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/// Copyright (c) 2019, the Dart Reddit API Wrapper project authors.
/// Please see the AUTHORS file for details. All rights reserved.
/// Use of this source code is governed by a BSD-style license that
/// can be found in the LICENSE file.
import 'dart:io';
import 'package:draw/src/models/subreddit.dart';
import 'package:collection/collection.dart';

const String _kJpegHeader = '\xff\xd8\xff';

Future<Map> loadImage(Uri imagePath) async {
final image = File.fromUri(imagePath);
if (!await image.exists()) {
throw FileSystemException('File does not exist', imagePath.toString());
}
final imageBytes = await image.readAsBytes();
if (imageBytes.length < _kJpegHeader.length) {
throw FormatException('Invalid image format for file $imagePath.');
}
final header = imageBytes.sublist(0, _kJpegHeader.length);
final isJpeg =
const IterableEquality().equals(_kJpegHeader.codeUnits, header);
return {
'imageBytes': imageBytes,
'imageType': isJpeg ? ImageFormat.jpeg : ImageFormat.png,
};
}
7 changes: 7 additions & 0 deletions lib/src/image_file_reader_unsupported.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/// Copyright (c) 2019, the Dart Reddit API Wrapper project authors.
/// Please see the AUTHORS file for details. All rights reserved.
/// Use of this source code is governed by a BSD-style license that
/// can be found in the LICENSE file.
Future<Map> loadImage(Uri imagePath) async =>
throw UnsupportedError('Loading images from disk is not supported on web.');
3 changes: 1 addition & 2 deletions lib/src/models/redditor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
// can be found in the LICENSE file.

import 'dart:async';
import 'dart:convert';

import 'package:draw/src/api_paths.dart';
import 'package:draw/src/base_impl.dart';
Expand Down Expand Up @@ -147,7 +146,7 @@ class RedditorRef extends RedditBase
Future<void> friend({String note = ''}) async =>
_throwOnInvalidRedditor(() async => await reddit.put(
apiPath['friend_v1'].replaceAll(_userRegExp, _name),
body: json.encode({'note': note})));
body: {'note': note}));

/// Returns a [Redditor] object with friend information populated.
///
Expand Down
145 changes: 97 additions & 48 deletions lib/src/models/subreddit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';

import 'package:draw/src/api_paths.dart';
import 'package:draw/src/base_impl.dart';
import 'package:draw/src/exceptions.dart';
import 'package:draw/src/image_file_reader.dart';
import 'package:draw/src/listing/listing_generator.dart';
import 'package:draw/src/listing/mixins/base.dart';
import 'package:draw/src/listing/mixins/gilded.dart';
Expand Down Expand Up @@ -1406,12 +1408,16 @@ class StyleSheetImage {
String toString() => name;
}

enum ImageFormat {
jpeg,
png,
}

/// Provides a set of stylesheet functions to a [Subreddit].
class SubredditStyleSheet {
// TODO(bkonyi): implement _uploadImage
// static const String _kJpegHeader = '\xff\xd8\xff';
// static const String _kUploadType = 'upload_type';
final Subreddit _subreddit;
static const String _kUploadType = 'upload_type';
final SubredditRef _subreddit;
SubredditStyleSheet(this._subreddit);

/// Return the stylesheet for the [Subreddit].
Expand All @@ -1421,27 +1427,37 @@ class SubredditStyleSheet {
return await _subreddit.reddit.get(uri);
}

// TODO(bkonyi): uploading files requires some pretty large changes to the
// authenticator implementation. Will do later.
/*
Future _uploadImage(Uri imagePath, Map<String, dynamic> data) async {
Future<Uri> _uploadImage(Uri imagePath, Uint8List imageBytes,
ImageFormat format, Map<String, dynamic> data) async {
const kImgType = 'img_type';
final image = File.fromUri(imagePath);
if (!await image.exists()) {
// TODO(bkonyi): throw
}
final imageRAF = await image.open();
if (await imageRAF.length() < _kJpegHeader.length) {
// TODO(bkonyi): throw
if ((imagePath == null) && (imageBytes == null)) {
throw DRAWArgumentError(
'Only one of imagePath or imageBytes can be provided.');
}
final header = (await imageRAF.read(_kJpegHeader.length));
data[kImgType] = (header == _kJpegHeader.codeUnits) ? 'jpg' : 'png';

if (imageBytes == null) {
final imageInfo = await loadImage(imagePath);
// ignore: parameter_assignments
format = imageInfo['imageType'];
// ignore: parameter_assignments
imageBytes = imageInfo['imageBytes'];
}
data[kImgType] = (format == ImageFormat.png) ? 'png' : 'jpeg';
data['api_type'] = 'json';
final uri = apiPath['upload_image']
.replaceAll(SubredditRef._subredditRegExp, _subreddit.displayName);
//final response =_subreddit.reddit.post(uri, data, )
final response = await _subreddit.reddit
.post(uri, data, files: {'file': imageBytes}, objectify: false) as Map;
const kImgSrc = 'img_src';
const kErrors = 'errors';
const kErrorsValues = 'errors_values';
if (response[kImgSrc].isNotEmpty) {
return Uri.parse(response[kImgSrc]);
} else {
throw DRAWImageUploadException(
response[kErrors].first, response[kErrorsValues].first);
}
}
*/

/// Remove the current header image for the [Subreddit].
Future<void> deleteHeader() async {
Expand Down Expand Up @@ -1496,48 +1512,81 @@ class SubredditStyleSheet {
return await _subreddit.reddit.post(uri, data);
}

// TODO(bkonyi): implement _uploadImage
// Upload an image to the [Subreddit].
//
// `name` is the name to be used for the image. If an image already exists
// with this name, it will be replaced.
//
// `imagePath` is the path to a JPEG or PNG image. This path must be local
// and accessible from disk.
/*
Future upload(String name, Uri imagePath) async =>
await _uploadImage(imagePath, <String, String>{
/// Upload an image to the [Subreddit].
///
/// `name` is the name to be used for the image. If an image already exists
/// with this name, it will be replaced.
///
/// `imagePath` is the path to a JPEG or PNG image. This path must be local
/// and accessible from disk. Will result in an [UnsupportedError] if provided
/// in a web application.
///
/// `bytes` is a list of bytes representing an image.
///
/// `format` is the format of the image defined by `bytes`.
///
/// On success, the [Uri] for the uploaded image is returned. On failure,
/// [DRAWImageUploadException] is thrown.
Future<Uri> upload(String name,
{Uri imagePath, Uint8List bytes, ImageFormat format}) async =>
await _uploadImage(imagePath, bytes, format, <String, String>{
'name': name,
_kUploadType: 'img',
});
*/

// TODO(bkonyi): implement _uploadImage
// Upload an image to be used as the header image for the [Subreddit].
/*
Future uploadHeader(Uri imagePath) async =>
await _uploadImage(imagePath, <String, String>{
/// Upload an image to be used as the header image for the [Subreddit].
///
/// `imagePath` is the path to a JPEG or PNG image. This path must be local
/// and accessible from disk. Will result in an [UnsupportedError] if provided
/// in a web application.
///
/// `bytes` is a list of bytes representing an image.
///
/// `format` is the format of the image defined by `bytes`.
///
/// On success, the [Uri] for the uploaded image is returned. On failure,
/// [DRAWImageUploadException] is thrown.
Future<Uri> uploadHeader(
{Uri imagePath, Uint8List bytes, ImageFormat format}) async =>
await _uploadImage(imagePath, bytes, format, <String, String>{
_kUploadType: 'header',
});
*/

// TODO(bkonyi): implement _uploadImage
// Upload an image to be used as the mobile header image for the [Subreddit].
/*
Future uploadMobileHeader(Uri imagePath) async =>
await _uploadImage(imagePath, <String, String>{
/// Upload an image to be used as the mobile header image for the [Subreddit].
///
/// `imagePath` is the path to a JPEG or PNG image. This path must be local
/// and accessible from disk. Will result in an [UnsupportedError] if provided
/// in a web application.
///
/// `bytes` is a list of bytes representing an image.
///
/// `format` is the format of the image defined by `bytes`.
///
/// On success, the [Uri] for the uploaded image is returned. On failure,
/// [DRAWImageUploadException] is thrown.
Future<Uri> uploadMobileHeader(
{Uri imagePath, Uint8List bytes, ImageFormat format}) async =>
await _uploadImage(imagePath, bytes, format, <String, String>{
_kUploadType: 'banner',
});
*/

// TODO(bkonyi): implement _uploadImage
// Upload an image to be used as the mobile icon image for the [Subreddit].
/*
Future uploadMobileIcon(Uri imagePath) async =>
await _uploadImage(imagePath, <String, String>{
/// Upload an image to be used as the mobile icon image for the [Subreddit].
///
/// `imagePath` is the path to a JPEG or PNG image. This path must be local
/// and accessible from disk. Will result in an [UnsupportedError] if provided
/// in a web application.
///
/// `bytes` is a list of bytes representing an image.
///
/// `format` is the format of the image defined by `bytes`.
///
/// On success, the [Uri] for the uploaded image is returned. On failure,
/// [DRAWImageUploadException] is thrown.
Future<Uri> uploadMobileIcon(
{Uri imagePath, Uint8List bytes, ImageFormat format}) async =>
await _uploadImage(imagePath, bytes, format, <String, String>{
_kUploadType: 'icon',
});
*/
}

/// Provides a set of wiki functions to a [Subreddit].
Expand Down
Loading

0 comments on commit 94e7525

Please sign in to comment.