Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Various additions to create RedditBase objects and Stream<RedditBase> generators #6

Merged
merged 16 commits into from
Aug 4, 2017
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
4 changes: 3 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ dart:
dart_task:
- test: -p vm
- dartfmt
- dartanalyzer --strong --fatal-warnings --fatal-lints

script:
- dartanalyzer --strong --fatal-warnings --fatal-lints .
166 changes: 166 additions & 0 deletions lib/src/api_paths.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// Copyright (c) 2017, 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.

/// A [Map] containing all of the Reddit API paths.
final Map apiPath = {
'about_edited': 'r/{subreddit}/about/edited/',
'about_log': 'r/{subreddit}/about/log/',
'about_modqueue': 'r/{subreddit}/about/modqueue/',
'about_reports': 'r/{subreddit}/about/reports/',
'about_spam': 'r/{subreddit}/about/spam/',
'about_sticky': 'r/{subreddit}/about/sticky/',
'about_stylesheet': 'r/{subreddit}/about/stylesheet/',
'about_traffic': 'r/{subreddit}/about/traffic/',
'about_unmoderated': 'r/{subreddit}/about/unmoderated/',
'accept_mod_invite': 'r/{subreddit}/api/accept_moderator_invite',
'approve': 'api/approve/',
'block': 'api/block',
'blocked': 'prefs/blocked/',
'comment': 'api/comment/',
'comment_replies': 'message/comments/',
'compose': 'api/compose/',
'contest_mode': 'api/set_contest_mode/',
'del': 'api/del/',
'deleteflair': 'r/{subreddit}/api/deleteflair',
'delete_sr_banner': 'r/{subreddit}/api/delete_sr_banner',
'delete_sr_header': 'r/{subreddit}/api/delete_sr_header',
'delete_sr_icon': 'r/{subreddit}/api/delete_sr_icon',
'delete_sr_image': 'r/{subreddit}/api/delete_sr_img',
'distinguish': 'api/distinguish/',
'domain': 'domain/{domain}/',
'duplicates': 'duplicates/{submission_id}/',
'edit': 'api/editusertext/',
'flair': 'r/{subreddit}/api/flair/',
'flairconfig': 'r/{subreddit}/api/flairconfig/',
'flaircsv': 'r/{subreddit}/api/flaircsv/',
'flairlist': 'r/{subreddit}/api/flairlist/',
'flairselector': 'r/{subreddit}/api/flairselector/',
'flairtemplate': 'r/{subreddit}/api/flairtemplate/',
'flairtemplateclear': 'r/{subreddit}/api/clearflairtemplates/',
'flairtemplatedelete': 'r/{subreddit}/api/deleteflairtemplate/',
'friend': 'r/{subreddit}/api/friend/',
'friend_v1': 'api/v1/me/friends/{user}',
'friends': 'api/v1/me/friends/',
'gild_thing': 'api/v1/gold/gild/{fullname}/',
'gild_user': 'api/v1/gold/give/{username}/',
'hide': 'api/hide/',
'ignore_reports': 'api/ignore_reports/',
'inbox': 'message/inbox/',
'info': 'api/info/',
'karma': 'api/v1/me/karma',
'leavecontributor': 'api/leavecontributor',
'leavemoderator': 'api/leavemoderator',
'list_banned': 'r/{subreddit}/about/banned/',
'list_contributor': 'r/{subreddit}/about/contributors/',
'list_moderator': 'r/{subreddit}/about/moderators/',
'list_muted': 'r/{subreddit}/about/muted/',
'list_wikibanned': 'r/{subreddit}/about/wikibanned/',
'list_wikicontributor': 'r/{subreddit}/about/wikicontributors/',
'live_accept_invite': 'api/live/{id}/accept_contributor_invite',
'live_add_update': 'api/live/{id}/update',
'live_close': 'api/live/{id}/close_thread',
'live_contributors': 'live/{id}/contributors',
'live_discussions': 'live/{id}/discussions',
'live_info': 'api/live/by_id/{ids}',
'live_invite': 'api/live/{id}/invite_contributor',
'live_leave': 'api/live/{id}/leave_contributor',
'live_now': 'api/live/happening_now',
'live_remove_update': 'api/live/{id}/delete_update',
'live_remove_contrib': 'api/live/{id}/rm_contributor',
'live_remove_invite': 'api/live/{id}/rm_contributor_invite',
'live_report': 'api/live/{id}/report',
'live_strike': 'api/live/{id}/strike_update',
'live_update_perms': 'api/live/{id}/set_contributor_permissions',
'live_update_thread': 'api/live/{id}/edit',
'live_updates': 'live/{id}',
'liveabout': 'api/live/{id}/about/',
'livecreate': 'api/live/create',
'lock': 'api/lock/',
'me': 'api/v1/me',
'mentions': 'message/mentions',
'message': 'message/messages/{id}/',
'messages': 'message/messages/',
'moderator_messages': 'r/{subreddit}/message/moderator/',
'moderator_unread': 'r/{subreddit}/message/moderator/unread/',
'morechildren': 'api/morechildren/',
'my_contributor': 'subreddits/mine/contributor/',
'my_moderator': 'subreddits/mine/moderator/',
'my_multireddits': 'api/multi/mine/',
'my_subreddits': 'subreddits/mine/subscriber/',
'marknsfw': 'api/marknsfw/',
'modmail_archive': 'api/mod/conversations/{id}/archive',
'modmail_conversation': 'api/mod/conversations/{id}',
'modmail_conversations': 'api/mod/conversations/',
'modmail_highlight': 'api/mod/conversations/{id}/highlight',
'modmail_mute': 'api/mod/conversations/{id}/mute',
'modmail_read': 'api/mod/conversations/read',
'modmail_unarchive': 'api/mod/conversations/{id}/unarchive',
'modmail_unmute': 'api/mod/conversations/{id}/unmute',
'modmail_unread': 'api/mod/conversations/unread',
'multireddit': 'user/{user}/m/{multi}/',
'multireddit_api': 'api/multi/user/{user}/m/{multi}/',
'multireddit_base': 'api/multi/',
'multireddit_copy': 'api/multi/copy/',
'multireddit_rename': 'api/multi/rename/',
'multireddit_update': 'api/multi/user/{user}/m/{multi}/r/{subreddit}',
'multireddit_user': 'api/multi/user/{user}/',
'mute_sender': 'api/mute_message_author/',
'quarantine_opt_in': 'api/quarantine_optin',
'quarantine_opt_out': 'api/quarantine_optout',
'read_message': 'api/read_message/',
'remove': 'api/remove/',
'report': 'api/report/',
'rules': 'r/{subreddit}/about/rules',
'save': 'api/save/',
'search': 'r/{subreddit}/search/',
'select_flair': 'r/{subreddit}/api/selectflair/',
'sent': 'message/sent/',
'setpermissions': 'r/{subreddit}/api/setpermissions/',
'spoiler': 'api/spoiler/',
'site_admin': 'api/site_admin/',
'sticky_submission': 'api/set_subreddit_sticky/',
'sub_recommended': 'api/recommend/sr/{subreddits}',
'submission': 'comments/{id}/',
'submission_replies': 'message/selfreply/',
'submit': 'api/submit/',
'subreddit': 'r/{subreddit}/',
'subreddit_about': 'r/{subreddit}/about/',
'subreddit_filter': ('api/filter/user/{user}/f/{special}/'
'r/{subreddit}'),
'subreddit_filter_list': 'api/filter/user/{user}/f/{special}',
'subreddit_random': 'r/{subreddit}/random/',
'subreddit_settings': 'r/{subreddit}/about/edit/',
'subreddit_stylesheet': 'r/{subreddit}/api/subreddit_stylesheet/',
'subreddits_by_topic': 'api/subreddits_by_topic',
'subreddits_default': 'subreddits/default/',
'subreddits_gold': 'subreddits/gold/',
'subreddits_new': 'subreddits/new/',
'subreddits_popular': 'subreddits/popular/',
'subreddits_name_search': 'api/search_reddit_names/',
'subreddits_search': 'subreddits/search/',
'subscribe': 'api/subscribe/',
'suggested_sort': 'api/set_suggested_sort/',
'unfriend': 'r/{subreddit}/api/unfriend/',
'unhide': 'api/unhide/',
'unignore_reports': 'api/unignore_reports/',
'unlock': 'api/unlock/',
'unmarknsfw': 'api/unmarknsfw/',
'unmute_sender': 'api/unmute_message_author/',
'unread': 'message/unread/',
'unread_message': 'api/unread_message/',
'unsave': 'api/unsave/',
'unspoiler': 'api/unspoiler/',
'upload_image': 'r/{subreddit}/api/upload_sr_img',
'user': 'user/{user}/',
'user_about': 'user/{user}/about/',
'vote': 'api/vote/',
'wiki_edit': 'r/{subreddit}/api/wiki/edit/',
'wiki_page': 'r/{subreddit}/wiki/{page}',
'wiki_page_editor': 'r/{subreddit}/api/wiki/alloweditor/{method}',
'wiki_page_revisions': 'r/{subreddit}/wiki/revisions/{page}',
'wiki_page_settings': 'r/{subreddit}/wiki/settings/{page}',
'wiki_pages': 'r/{subreddit}/wiki/pages/',
'wiki_revisions': 'r/{subreddit}/wiki/revisions/'
};
12 changes: 6 additions & 6 deletions lib/src/auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,14 @@ abstract class Authenticator {

/// Make a simple `GET` request. [path] is the destination URI that the
/// request will be made to.
Future<Map> get(Uri path) async {
return _request(kGetRequest, path);
Future<Map> get(Uri path, {Map params}) async {
return _request(kGetRequest, path, params: params);
}

/// Make a simple `POST` request. [path] is the destination URI and [body]
/// contains the POST parameters that will be sent with the request.
Future<Map> post(Uri path, Map<String, String> body) async {
return _request(kPostRequest, path, body);
return _request(kPostRequest, path, body: body);
}

/// Request data from Reddit using our OAuth2 client.
Expand All @@ -127,15 +127,16 @@ abstract class Authenticator {
/// request parameters. [body] is an optional parameter which contains the
/// body fields for a POST request.
Future<Map> _request(String type, Uri path,
[Map<String, String> body]) async {
{Map<String, String> body, Map params}) async {
if (_client == null) {
throw new DRAWAuthenticationError(
'The authenticator does not have a valid token.');
}
if (!isValid) {
await refresh();
}
final request = new http.Request(type, path);
final finalPath = path.replace(queryParameters: params);
final request = new http.Request(type, finalPath);
if (body != null) {
request.bodyFields = body;
}
Expand All @@ -161,7 +162,6 @@ abstract class Authenticator {

final httpClient = new http.Client();
final start = new DateTime.now();

final headers = new Map<String, String>();
headers[kUserAgentKey] = _userAgent;

Expand Down
44 changes: 44 additions & 0 deletions lib/src/base.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) 2017, 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 'reddit.dart';

class RedditBase {
final Reddit reddit;
final Map _data;
final RegExp _snakecaseRegexp = new RegExp("[A-Z]");

RedditBase(this.reddit) : _data = null;

RedditBase.loadData(this.reddit, Map data) : _data = data;

String _snakeCase(String name, [separator = '_']) => name.replaceAllMapped(
_snakecaseRegexp,
(Match match) =>
(match.start != 0 ? separator : '') + match.group(0).toLowerCase());

dynamic noSuchMethod(Invocation invocation) {
// This is a dirty hack to allow for dynamic field population based on the
// API response instead of hardcoding these fields and having to update them
// when the API updates. Invocation.memberName is a Symbol, which
// unfortunately doesn't have a getName method due to code minification
// restrictions in dart2js, so the only way to get the name properly is
// using the dart:mirrors library. Unfortunately, dart:mirrors isn't
// supported in Flutter/Dart AOT, which makes it unacceptable to use in this
// library. However, Symbol.toString() returns a string in the form of
// Symbol("memberName") consistently on the Dart VM. We're abusing this
// behaviour here, and there's no promise that this will work in the future,
// but there's no reason to assume that this behaviour will change any time
// soon.
var name = invocation.memberName.toString();
name = _snakeCase(name.substring(8, name.length - 2));
if (!invocation.isGetter || (_data == null) || !_data.containsKey(name)) {
// Check that the accessed field is a getter and the property exists.
throw new NoSuchMethodError(this, invocation.memberName,
invocation.positionalArguments, invocation.namedArguments);
}
return _data[name];
}
}
19 changes: 17 additions & 2 deletions lib/src/exceptions.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
// Copyright (c) 2017, the Dart Reddit API Wrapper project authors.
// Copyright (c) 2017, 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.

// TODO(bkonyi) document properly.
/// Thrown when there is an error during the authentication flow.
class DRAWAuthenticationError implements Exception {
DRAWAuthenticationError(this.message);
String message;
String toString() => 'DRAWAuthenticationError: $message';
}

/// Thrown by unfinished code that hasn't yet implemented all the features it
/// needs.
class DRAWUnimplementedError extends UnimplementedError {
DRAWUnimplementedError([String message]) : super(message);
}

/// Thrown due to a fatal error encountered inside DRAW. If you're not adding
/// functionality to DRAW you should never see this. Otherwise, please file a
/// bug at github.com/draw-dev/DRAW/issues.
class DRAWInternalError implements Exception {
DRAWInternalError(this.message);
String message;
String toString() => 'DRAWInternalError: $message';
}
57 changes: 57 additions & 0 deletions lib/src/listing/listing_generator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) 2017, 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:async';

import '../reddit.dart';

/// An abstract static class used to generate [Stream]s of [RedditBase] objects.
/// This class should not be used directly, as it is used by various methods
/// defined in children of [RedditBase].
abstract class ListingGenerator {
static const defaultRequestLimit = 100;

/// An asynchronous iterator method used to make Reddit API calls as defined
/// by [api] in blocks of size [limit]. The default [limit] is specified by
/// [defaultRequestLimit]. Returns a [Stream<T>] which can be iterated over
/// using an asynchronous for-loop.
static Stream<T> generator<T>(final Reddit reddit, final String api,
{int limit, Map params}) async* {
final kLimitKey = 'limit';
final nullLimit = 1024;
final paramsInternal = params ?? new Map();
paramsInternal[kLimitKey] = (limit ?? nullLimit).toString();
int yielded = 0;
int index = 0;
List<T> listing;

Future<List<T>> _nextBatch() async {
final response = (await reddit.get(api, params: paramsInternal)) as Map;
final newListing = response['listing'];
paramsInternal['after'] = response['after'];
if (newListing.length == 0) {
return null;
}
index = 0;
return newListing;
}

while (yielded < limit) {
if ((listing == null) || (index >= listing.length)) {
if (listing != null &&
listing.length < int.parse(paramsInternal[kLimitKey])) {
break;
}
listing = await _nextBatch();
if (listing == null) {
break;
}
}
yield listing[index];
++index;
++yielded;
}
}
}
21 changes: 21 additions & 0 deletions lib/src/models/redditor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) 2017, 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 '../base.dart';
import '../exceptions.dart';
import '../reddit.dart';

class Redditor extends RedditBase {
String _name;
Uri _path;

Redditor.parse(Reddit reddit, Map data) : super.loadData(reddit, data) {
if (!data.containsKey('name')) {
// TODO(bkonyi) throw invalid object exception
throw new DRAWUnimplementedError();
}
_name = data['name'];
}
}
13 changes: 13 additions & 0 deletions lib/src/models/subreddit.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) 2017, 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 '../base.dart';
import '../reddit.dart';

// TODO(bkonyi) implement
class Subreddit extends RedditBase {
Subreddit.parse(Reddit reddit, Map data)
: super.loadData(reddit, data['data']);
}
Loading