├── .gitignore ├── .travis.yml ├── AUTHORS ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example ├── draw.ini └── example.dart ├── lib ├── draw.dart └── src │ ├── api_paths.dart │ ├── auth.dart │ ├── base.dart │ ├── base_impl.dart │ ├── config_file_reader.dart │ ├── config_file_reader_io.dart │ ├── config_file_reader_unsupported.dart │ ├── draw_config_context.dart │ ├── exception_objector.dart │ ├── exceptions.dart │ ├── frontpage.dart │ ├── getter_utils.dart │ ├── image_file_reader.dart │ ├── image_file_reader_io.dart │ ├── image_file_reader_unsupported.dart │ ├── listing │ ├── listing_generator.dart │ └── mixins │ │ ├── base.dart │ │ ├── gilded.dart │ │ ├── redditor.dart │ │ ├── rising.dart │ │ └── subreddit.dart │ ├── logging.dart │ ├── models │ ├── comment.dart │ ├── comment_forest.dart │ ├── comment_impl.dart │ ├── flair.dart │ ├── inbox.dart │ ├── message.dart │ ├── mixins │ │ ├── editable.dart │ │ ├── gildable.dart │ │ ├── inboxable.dart │ │ ├── inboxtoggleable.dart │ │ ├── messageable.dart │ │ ├── replyable.dart │ │ ├── reportable.dart │ │ ├── saveable.dart │ │ ├── user_content_mixin.dart │ │ ├── user_content_moderation.dart │ │ └── voteable.dart │ ├── multireddit.dart │ ├── redditor.dart │ ├── submission.dart │ ├── submission_impl.dart │ ├── subreddit.dart │ ├── subreddit_moderation.dart │ ├── subreddits.dart │ ├── trophy.dart │ ├── user_content.dart │ └── wikipage.dart │ ├── modmail.dart │ ├── objector.dart │ ├── platform_utils.dart │ ├── platform_utils_io.dart │ ├── platform_utils_unsupported.dart │ ├── reddit.dart │ ├── user.dart │ └── util.dart ├── pubspec.yaml ├── test ├── auth │ ├── credentials.dart │ ├── read_only.dart │ ├── run_live_auth_tests.dart │ ├── script_auth.dart │ └── web_auth.dart ├── comment │ ├── comment_ref_test.json │ ├── comment_test.dart │ ├── continue_test.json │ ├── continue_test_expected.out │ ├── invalid_test.json │ ├── more_comment_count_test.json │ ├── more_comment_expand_test │ ├── more_comment_expand_test_expected.out │ ├── tons_of_comments_expected.out │ └── tons_of_comments_test.json ├── draw.ini ├── frontpage │ ├── frontpage_test.dart │ ├── lib_frontpage_best.json │ └── lib_frontpage_sanity.json ├── gilded_listing_mixin │ ├── gilded_listing_mixin_test.dart │ └── lib_gilded_listing_mixin_frontpage.json ├── images │ ├── 10by3.jpg │ ├── 256.jpg │ ├── bad.jpg │ └── dart_header.png ├── inbox │ ├── inbox_test.dart │ ├── lib_inbox_all.json │ ├── lib_inbox_collapse_uncollapse.json │ ├── lib_inbox_comment_replies.json │ ├── lib_inbox_mark_read_unread.json │ ├── lib_inbox_mentions.json │ ├── lib_inbox_message.json │ ├── lib_inbox_messages.json │ ├── lib_inbox_sent.json │ ├── lib_inbox_submission_replies.json │ └── lib_inbox_unread.json ├── messageable_mixin │ ├── basic_message.json │ ├── invalid_redditor.json │ ├── invalid_subreddit.json │ ├── messageable_mixin_test.dart │ └── subreddit_message.json ├── multireddit │ ├── lib_multireddit_add_subreddit.json │ ├── lib_multireddit_copy.json │ ├── lib_multireddit_copy_2.json │ ├── lib_multireddit_delete.json │ ├── lib_user_multireddits.json │ └── multireddit_test.dart ├── redditor │ ├── lib_reddit_downvoted.json │ ├── lib_reddit_gild.json │ ├── lib_reddit_gild_insufficient.json │ ├── lib_reddit_hidden.json │ ├── lib_reddit_upvoted.json │ ├── lib_redditor_bad_friend.json │ ├── lib_redditor_bad_friend_info.json │ ├── lib_redditor_bad_gild.json │ ├── lib_redditor_bad_unfriend.json │ ├── lib_redditor_comments.json │ ├── lib_redditor_controversial.json │ ├── lib_redditor_friend.json │ ├── lib_redditor_gildings.json │ ├── lib_redditor_hot.json │ ├── lib_redditor_invalid.json │ ├── lib_redditor_invalid_unblock.json │ ├── lib_redditor_multireddits.json │ ├── lib_redditor_multireddits_bad_redditor.json │ ├── lib_redditor_new.json │ ├── lib_redditor_properties.json │ ├── lib_redditor_saved.json │ ├── lib_redditor_submissions.json │ ├── lib_redditor_top.json │ ├── lib_redditor_trophies.json │ ├── lib_redditor_unblock.json │ ├── redditor_test.dart │ └── testing.json ├── rising_listing_mixin │ ├── lib_rising_listing_mixin_random_rising.json │ ├── lib_rising_listing_mixin_rising.json │ └── rising_listing_mixin_test.dart ├── submission │ ├── lib_submission_crosspost.json │ ├── lib_submission_flair.json │ ├── lib_submission_hide_unhide.json │ ├── lib_submission_hide_unhide_multiple.json │ ├── lib_submission_invalid.json │ ├── lib_submission_properties.json │ ├── lib_submission_refresh_comments_sanity.json │ ├── lib_submission_reply.json │ └── submission_test.dart ├── subreddit │ ├── lib_subreddit_banned.json │ ├── lib_subreddit_comments_stream.json │ ├── lib_subreddit_contributor.json │ ├── lib_subreddit_filter.json │ ├── lib_subreddit_flair_call.json │ ├── lib_subreddit_flair_configure.json │ ├── lib_subreddit_flair_delete.json │ ├── lib_subreddit_flair_delete_all.json │ ├── lib_subreddit_flair_link_templates.json │ ├── lib_subreddit_flair_redditor_templates.json │ ├── lib_subreddit_flair_set_flair.json │ ├── lib_subreddit_flair_update.json │ ├── lib_subreddit_invalid.json │ ├── lib_subreddit_listing_mixin_sanity.json │ ├── lib_subreddit_moderator_relationship_add_invite.json │ ├── lib_subreddit_moderator_relationship_leave.json │ ├── lib_subreddit_moderator_relationship_mods.json │ ├── lib_subreddit_moderator_relationship_remove.json │ ├── lib_subreddit_quarantine.json │ ├── lib_subreddit_random.json │ ├── lib_subreddit_rules.json │ ├── lib_subreddit_sticky.json │ ├── lib_subreddit_stylesheet_call.json │ ├── lib_subreddit_stylesheet_delete_header.json │ ├── lib_subreddit_stylesheet_delete_image.json │ ├── lib_subreddit_stylesheet_delete_mobile_header.json │ ├── lib_subreddit_stylesheet_delete_mobile_icon.json │ ├── lib_subreddit_stylesheet_update.json │ ├── lib_subreddit_stylesheet_upload.json │ ├── lib_subreddit_stylesheet_upload_bytes.json │ ├── lib_subreddit_stylesheet_upload_header.json │ ├── lib_subreddit_stylesheet_upload_invalid_cases.json │ ├── lib_subreddit_stylesheet_upload_mobile_header.json │ ├── lib_subreddit_stylesheet_upload_mobile_icon.json │ ├── lib_subreddit_submit.json │ ├── lib_subreddit_subscribe_and_unsubscribe.json │ ├── lib_subreddit_traffic.json │ ├── lib_subreddit_wiki_create_wiki_page.json │ ├── lib_subreddit_wiki_edit_wiki_page.json │ ├── lib_subreddit_wiki_equality.json │ ├── lib_subreddit_wiki_invalid_wiki_page.json │ ├── lib_subreddit_wiki_page_moderation_add_remove.json │ ├── lib_subreddit_wiki_page_moderation_invalid_add_remove.json │ ├── lib_subreddit_wiki_page_moderation_settings.json │ ├── lib_subreddit_wiki_page_revisions.json │ ├── lib_subreddit_wiki_revisions.json │ ├── lib_subreddits_default.json │ ├── lib_subreddits_gold.json │ ├── lib_subreddits_newest.json │ ├── lib_subreddits_popular.json │ ├── lib_subreddits_recommended.json │ ├── lib_subreddits_search.json │ ├── lib_subreddits_search_by_name.json │ ├── lib_subreddits_stream.json │ ├── subreddit_listing_mixin_test.dart │ ├── subreddit_mod_queue.json │ ├── subreddit_mod_reports.json │ ├── subreddit_mod_spam.json │ ├── subreddit_mod_unmoderated.json │ ├── subreddit_moderation_edited.json │ ├── subreddit_moderation_inbox.json │ ├── subreddit_moderation_log.json │ ├── subreddit_moderation_settings.json │ ├── subreddit_moderation_test.dart │ ├── subreddit_moderation_unread.json │ ├── subreddit_modmail_archive_unarchive.json │ ├── subreddit_modmail_bulkread.json │ ├── subreddit_modmail_conversation.json │ ├── subreddit_modmail_create.json │ ├── subreddit_modmail_highlight_unhighlight.json │ ├── subreddit_modmail_mute_unmute.json │ ├── subreddit_modmail_read_unread.json │ ├── subreddit_modmail_reply.json │ ├── subreddit_modmail_subreddits.json │ ├── subreddit_modmail_unread_count.json │ ├── subreddit_test.dart │ ├── subreddit_update_settings.json │ └── subreddits_test.dart ├── test_all.dart ├── test_authenticator.dart ├── test_config.dart ├── test_utils.dart ├── unit_tests │ ├── enum_stringify_test.dart │ ├── test.ini │ ├── test_draw_config_context.dart │ └── utils_test.dart ├── user │ ├── lib_user_contributorSubreddits.json │ ├── lib_user_friends.json │ ├── lib_user_karma.json │ ├── lib_user_me.json │ ├── lib_user_moderatorSubreddits.json │ ├── lib_user_multireddits.json │ ├── lib_user_subreddits.json │ └── user_test.dart ├── user_content │ ├── lib_user_content_editable.json │ ├── lib_user_content_replyable.json │ ├── lib_user_content_reportable.json │ ├── lib_user_content_saveable.json │ ├── lib_user_content_toggleable.json │ ├── lib_user_content_votes.json │ └── user_content_test.dart └── user_content_moderation │ ├── lib_user_content_moderation_contest_mode.json │ ├── lib_user_content_moderation_distinguish.json │ ├── lib_user_content_moderation_lock_unlock.json │ ├── lib_user_content_moderation_nsfw_sfw.json │ ├── lib_user_content_moderation_remove_approve.json │ ├── lib_user_content_moderation_reports.json │ ├── lib_user_content_moderation_set_flair.json │ ├── lib_user_content_moderation_spoiler.json │ ├── lib_user_content_moderation_sticky.json │ ├── lib_user_content_moderation_suggested_sort.json │ └── user_content_moderation_test.dart ├── third_party └── reply │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── lib │ ├── reply.dart │ └── src │ │ ├── conclusion_builder.dart │ │ ├── recorder.dart │ │ ├── recording.dart │ │ └── response_builder.dart │ ├── pubspec.yaml │ └── test │ └── reply_test.dart └── tool └── travis.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/tools/private-files.html 2 | 3 | # Files and directories created by pub 4 | .buildlog 5 | .packages 6 | .project 7 | .pub/ 8 | build/ 9 | **/packages/ 10 | 11 | # Files and directories created by other tools 12 | .idea 13 | *.swp 14 | 15 | # Files created by dart2js 16 | # (Most Dart developers will use pub build to compile Dart, use/modify these 17 | # rules if you intend to use dart2js directly 18 | # Convention is to use extension '.dart.js' for Dart compiled to Javascript to 19 | # differentiate from explicit Javascript files) 20 | *.dart.js 21 | *.part.js 22 | *.js.deps 23 | *.js.map 24 | *.info.json 25 | 26 | # Directory created by dartdoc 27 | doc/api/ 28 | 29 | # Don't commit pubspec lock file 30 | # (Library packages only! Remove pattern if developing an application package) 31 | pubspec.lock 32 | 33 | # Intellij Files 34 | DRAW.iml 35 | draw.ini 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: dart 2 | sudo: false 3 | 4 | os: 5 | - linux 6 | 7 | dart: 8 | # - stable 9 | - dev 10 | 11 | script: 12 | - ./tool/travis.sh 13 | - dart --no-sound-null-safety test/test_all.dart 14 | - dartfmt -n --set-exit-if-changed lib/ test/ 15 | - dartanalyzer --fatal-warnings --fatal-lints lib/ test/ 16 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Below is a list of people and organizations that have contributed 2 | # to the DRAW project. Names should be added to the list like so: 3 | # 4 | # Name/Organization 5 | 6 | Ben Konyi 7 | Kartik Chopra 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at bkonyi@google.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | DRAW is always looking for contributors (the more the merrier, right?), 4 | and if you're reading this document you must be interested in helping out! 5 | If you're a first time contributor, welcome aboard! Otherwise, welcome back! 6 | 7 | This document contains all the information needed to contribute to DRAW, including 8 | how to write tests and how to prepare to create a pull request. 9 | 10 | # Checkout 11 | The project uses the line seperator LF in tests. In order to run tests locally on Windows, you have to configure Git to use LF on checkout, as described [here](https://help.github.com/en/articles/dealing-with-line-endings). 12 | 13 | # Adding new features 14 | *TODO* 15 | 16 | # Testing 17 | 18 | Most changes should include test cases for newly added features and bug fixes. 19 | 20 | ## Writing new tests 21 | As you may have noticed, existing tests for DRAW do not require network access 22 | or Reddit credentials to run. This is made possible through the use of the 23 | [reply package](https://pub.dartlang.org/packages/reply), which allows for replaying 24 | interactions which have requests and responses. 25 | 26 | Any new test for DRAW which requires a network request to be made will need to create 27 | a recording of the requests so they can be replayed later. To create a new test, first 28 | determine in which test directory the test belongs. If adding functionality to an existing 29 | class, there's likely already a directory for the tests for the class. Otherwise, create a 30 | new directory under `$DRAW_ROOT/test/`. 31 | 32 | DRAW uses Dart's [test package](https://pub.dartlang.org/packages/test) to handle running tests. 33 | All new tests should (generally) have the following format: 34 | 35 | ```dart 36 | test('lib/$CLASS_NAME/$TEST_NAME', () async { 37 | final reddit = 38 | await createRedditTestInstance('test/$CLASS_NAME/$TEST_NAME.json', 39 | live: true); // Perform network requests. 40 | // ... 41 | // TEST BODY HERE 42 | // ... 43 | 44 | // Write requests recording to 'test/$CLASS_NAME/$TEST_NAME.json'. 45 | // TODO: remove after recording is made. 46 | await writeRecording(reddit); 47 | }); 48 | ``` 49 | 50 | Both `createRedditTestInstance` and `writeRecording` are defined in `test/test_utils.dart`. 51 | `createRedditTestInstance` creates an instance of `Reddit` which has a special authenticator 52 | that can either record network requests or replay them, depending on the state of the `live` parameter. 53 | `writeRecording` writes the recorded network requests to the JSON file specified when calling 54 | `createRedditTestInstance`. 55 | 56 | **NOTE:** Credentials used to authenticate with Reddit when running a 57 | test in 'live' mode *are not recorded as part of the replays*, so there shouldn't be any issues 58 | using a personal account for writing tests. However, it's highly recommended that you create a 59 | dedicated testing account to avoid any risk of personal information being exposed. 60 | 61 | # Creating your PR 62 | 63 | Before creating a PR, be sure to run the following from the root of the project: 64 | 1. `dartfmt -w .`: formats all Dart code throughout the repository. 65 | 2. `dartanalyzer lib/`: run static analysis on the project's library code and ensure there are no failures 66 | and warnings. Lints are *okay*, but fixing lint issues is preferrable to leaving them. 67 | 3. `dart test/test_all.dart`: runs all the tests for the project. All tests (except for the live 68 | tests which authenticate with Reddit) should be passing before submission. 69 | 70 | Checks are run for each commit and PR for the repository and changes will not be accepted if 71 | any of the above commands produces failures. 72 | 73 | All changes to DRAW are reviewed by a designated contributor (typically bkonyi@) before being 74 | merged into the master branch. 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, the Dart Reddit API Wrapper authors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of Google Inc. nor the names of its contributors may be used 15 | to endorse or promote products derived from this software without specific 16 | prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:pedantic/analysis_options.yaml 2 | linter: 3 | rules: 4 | - always_declare_return_types 5 | - avoid_empty_else 6 | - avoid_init_to_null 7 | - avoid_return_types_on_setters 8 | - await_only_futures 9 | - camel_case_types 10 | - cancel_subscriptions 11 | - constant_identifier_names 12 | - control_flow_in_finally 13 | - empty_catches 14 | - empty_constructor_bodies 15 | - empty_statements 16 | - hash_and_equals 17 | - iterable_contains_unrelated_type 18 | - library_names 19 | - library_prefixes 20 | - list_remove_unrelated_type 21 | - non_constant_identifier_names 22 | - only_throw_errors 23 | - overridden_fields 24 | - package_names 25 | - package_prefixed_library_names 26 | - parameter_assignments 27 | - prefer_equal_for_default_values 28 | - prefer_final_fields 29 | - prefer_final_locals 30 | - prefer_is_not_empty 31 | - prefer_is_empty 32 | - slash_for_doc_comments 33 | - sort_unnamed_constructors_first 34 | - test_types_in_equals 35 | - type_init_formals 36 | - throw_in_finally 37 | - unnecessary_getters_setters 38 | - unrelated_type_equality_checks 39 | - valid_regexps 40 | -------------------------------------------------------------------------------- /example/draw.ini: -------------------------------------------------------------------------------- 1 | default=default 2 | reddit_url='https://www.reddit.com' 3 | client_id=Y4PJOclpDQy3xZ 4 | client_secret=UkGLTe6oqsMk5nHCJTHLrwgvHpr 5 | password=pni9ubeht4wd50gk 6 | username=fakebot1 7 | 8 | [section] 9 | password=different 10 | oauth_url=https://oauth.reddit.com 11 | 12 | [section1] 13 | password=pni9ubeht4wd50gk 14 | username=sectionbot 15 | 16 | [emptyTest] 17 | username= 18 | 19 | [testUpdateCheck1] 20 | check_for_updates=1 21 | 22 | [testUpdateCheckOn] 23 | check_for_updates=on 24 | 25 | [testUpdateCheckTrue] 26 | check_for_updates=true 27 | 28 | [testUpdateCheckYes] 29 | check_for_updates=yes 30 | 31 | [testUpdateCheckFalse] 32 | check_for_updates=false 33 | -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | import 'package:draw/draw.dart'; 8 | 9 | String? kClientId; 10 | String? kSecret; 11 | String? kAgentName; 12 | 13 | Future main() async { 14 | // Create the `Reddit` instance and authenticate 15 | final reddit = await Reddit.createScriptInstance( 16 | clientId: kClientId, 17 | clientSecret: kSecret, 18 | userAgent: kAgentName, 19 | username: 'DRAWApiOfficial', 20 | password: 'hunter12', // Fake 21 | ); 22 | 23 | // Retrieve information for the currently authenticated user 24 | final currentUser = (await reddit.user.me()) as Redditor; 25 | // Outputs: My name is DRAWApiOfficial 26 | print('My name is ${currentUser.displayName}'); 27 | } 28 | -------------------------------------------------------------------------------- /lib/draw.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | library draw; 7 | 8 | export 'package:draw/src/reddit.dart'; 9 | export 'package:draw/src/auth.dart'; 10 | export 'package:draw/src/base.dart'; 11 | export 'package:draw/src/exceptions.dart'; 12 | export 'package:draw/src/frontpage.dart'; 13 | export 'package:draw/src/objector.dart'; 14 | export 'package:draw/src/user.dart'; 15 | export 'package:draw/src/models/comment.dart'; 16 | export 'package:draw/src/models/comment_forest.dart' hide setSubmission; 17 | export 'package:draw/src/models/flair.dart' 18 | hide flairPositionToString, stringToFlairPosition; 19 | export 'package:draw/src/models/inbox.dart'; 20 | export 'package:draw/src/listing/mixins/base.dart' 21 | show BaseListingMixin, Sort, TimeFilter; 22 | export 'package:draw/src/listing/mixins/gilded.dart'; 23 | export 'package:draw/src/listing/mixins/redditor.dart'; 24 | export 'package:draw/src/listing/mixins/rising.dart'; 25 | export 'package:draw/src/listing/mixins/subreddit.dart'; 26 | export 'package:draw/src/models/message.dart'; 27 | export 'package:draw/src/models/multireddit.dart' 28 | hide iconNameToString, visibilityToString, weightingSchemeToString; 29 | export 'package:draw/src/models/redditor.dart'; 30 | export 'package:draw/src/models/submission.dart'; 31 | export 'package:draw/src/models/subreddit.dart' 32 | hide searchSyntaxToString, modmailSortToString, modmailStateToString; 33 | export 'package:draw/src/models/subreddit_moderation.dart' 34 | hide 35 | buildModeratorAction, 36 | moderatorActionTypesToString, 37 | stringToModeratorActionType, 38 | stringToSubredditType, 39 | subredditTypeToString; 40 | export 'package:draw/src/models/trophy.dart'; 41 | export 'package:draw/src/models/user_content.dart'; 42 | export 'package:draw/src/models/mixins/editable.dart'; 43 | export 'package:draw/src/models/mixins/gildable.dart'; 44 | export 'package:draw/src/models/mixins/inboxable.dart'; 45 | export 'package:draw/src/models/mixins/inboxtoggleable.dart'; 46 | export 'package:draw/src/models/mixins/messageable.dart'; 47 | export 'package:draw/src/models/mixins/replyable.dart'; 48 | export 'package:draw/src/models/mixins/reportable.dart'; 49 | export 'package:draw/src/models/mixins/saveable.dart'; 50 | export 'package:draw/src/models/mixins/user_content_mixin.dart'; 51 | export 'package:draw/src/models/mixins/user_content_moderation.dart' 52 | show DistinctionType, UserContentModerationMixin; 53 | export 'package:draw/src/models/mixins/voteable.dart'; 54 | export 'package:draw/src/modmail.dart'; 55 | export 'package:draw/src/models/wikipage.dart' hide revisionGenerator; 56 | -------------------------------------------------------------------------------- /lib/src/base.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | export 'base_impl.dart' hide setData; 7 | -------------------------------------------------------------------------------- /lib/src/base_impl.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | import 'dart:convert'; 8 | 9 | import 'package:draw/src/exceptions.dart'; 10 | import 'package:draw/src/reddit.dart'; 11 | 12 | void setData(RedditBaseInitializedMixin base, Map? data) { 13 | base._data = data; 14 | } 15 | 16 | abstract class RedditBaseInitializedMixin { 17 | Reddit get reddit; 18 | String get infoPath; 19 | Map? get infoParams => null; 20 | 21 | /// Returns the raw properties dictionary for this object. 22 | /// 23 | /// This getter returns null if the object is lazily initialized. 24 | Map? get data => _data; 25 | Map? _data; 26 | 27 | /// The fullname of a Reddit object. 28 | /// 29 | /// Reddit object fullnames take the form of 't3_15bfi0'. 30 | String? get fullname => (data == null) ? null : data!['name']; 31 | 32 | /// The id of a Reddit object. 33 | /// 34 | /// Reddit object ids take the form of '15bfi0'. 35 | String? get id => (data == null) ? null : data!['id']; 36 | 37 | /// Requests updated information from the Reddit API and updates the current 38 | /// object properties. 39 | Future refresh() async { 40 | final response = await reddit.get(infoPath, params: infoParams); 41 | if (response is Map) { 42 | _data = response; 43 | } else if (response is List) { 44 | // TODO(bkonyi): this is for populating a Submission, since requesting 45 | // Submission returns a list of listings, containing a Submission at [0] 46 | // and a listing of Comments at [1]. This probably needs to be changed 47 | // at some point to be a bit more robust, but it works for now. 48 | _data = response[0]['listing'][0].data; 49 | return [this, response[1]['listing']]; 50 | } else { 51 | throw DRAWInternalError('Refresh response is of unknown type: ' 52 | '${response.runtimeType}.'); 53 | } 54 | return this; 55 | } 56 | 57 | @override 58 | String toString() { 59 | if (_data != null) { 60 | final encoder = JsonEncoder.withIndent(' '); 61 | return encoder.convert(_data); 62 | } 63 | return 'null'; 64 | } 65 | } 66 | 67 | /// A base class for most DRAW objects which handles lazy-initialization of 68 | /// objects and Reddit API request state. 69 | abstract class RedditBase { 70 | /// The current [Reddit] instance. 71 | final Reddit reddit; 72 | 73 | Map? get infoParams => null; 74 | 75 | /// The base request format for the current object. 76 | String get infoPath => _infoPath; 77 | late String _infoPath; 78 | 79 | RedditBase(this.reddit); 80 | 81 | RedditBase.withPath(this.reddit, String infoPath) : _infoPath = infoPath; 82 | 83 | /// Requests the data associated with the current object. 84 | Future fetch() async => reddit.get(infoPath, params: infoParams); 85 | } 86 | -------------------------------------------------------------------------------- /lib/src/config_file_reader.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2019, the Dart Reddit API Wrapper project authors. 2 | /// Please see the AUTHORS file for details. All rights reserved. 3 | /// Use of this source code is governed by a BSD-style license that 4 | /// can be found in the LICENSE file. 5 | 6 | export 'config_file_reader_unsupported.dart' 7 | if (dart.library.io) 'config_file_reader_io.dart'; 8 | -------------------------------------------------------------------------------- /lib/src/config_file_reader_io.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2019, the Dart Reddit API Wrapper project authors. 2 | /// Please see the AUTHORS file for details. All rights reserved. 3 | /// Use of this source code is governed by a BSD-style license that 4 | /// can be found in the LICENSE file. 5 | 6 | import 'dart:io'; 7 | import 'package:path/path.dart' as path; 8 | import 'exceptions.dart'; 9 | 10 | const String kLinuxEnvVar = 'XDG_CONFIG_HOME'; 11 | const String kLinuxHomeEnvVar = 'HOME'; 12 | const String kMacEnvVar = 'HOME'; 13 | const String kFileName = 'draw.ini'; 14 | const String kWindowsEnvVar = 'APPDATA'; 15 | 16 | class ConfigFileReader { 17 | /// Path to Local, User, Global Configuration Files, with matching precedence. 18 | late Uri _localConfigPath; 19 | late Uri _userConfigPath; 20 | 21 | String? _configUrl; 22 | 23 | ConfigFileReader(String? configUrl) { 24 | _configUrl = configUrl; 25 | _localConfigPath = _getLocalConfigPath(); 26 | _userConfigPath = _getUserConfigPath(); 27 | } 28 | 29 | /// Loads file from [_localConfigPath] or [_userConfigPath]. 30 | File? loadCorrectFile() { 31 | if (_configUrl != null) { 32 | final primaryFile = File(_configUrl!); 33 | if (primaryFile.existsSync()) { 34 | return primaryFile; 35 | } 36 | } 37 | // Check if file exists locally. 38 | var primaryFile = File(_localConfigPath.toFilePath()); 39 | if (primaryFile.existsSync()) { 40 | _configUrl = _localConfigPath.toString(); 41 | return primaryFile; 42 | } 43 | 44 | // Check if file exists in user directory. 45 | primaryFile = File(_userConfigPath.toFilePath()); 46 | if (primaryFile.existsSync()) { 47 | _configUrl = _userConfigPath.toString(); 48 | return primaryFile; 49 | } 50 | return null; 51 | } 52 | 53 | /// Returns path to user level configuration file. 54 | /// Special Behaviour: if User Config Environment var unset, 55 | /// uses [$HOME] or the corresponding root path for the os. 56 | Uri _getUserConfigPath() { 57 | final environment = Platform.environment; 58 | String? osConfigPath; 59 | // Load correct path for user level configuration paths based on operating system. 60 | if (Platform.isMacOS) { 61 | osConfigPath = path.join(environment[kMacEnvVar]!, '.config'); 62 | } else if (Platform.isLinux) { 63 | osConfigPath = environment[kLinuxEnvVar] ?? environment[kLinuxHomeEnvVar]; 64 | } else if (Platform.isWindows) { 65 | osConfigPath = environment[kWindowsEnvVar]; 66 | } else if (Platform.isIOS) { 67 | // TODO(ckartik): Look into supporting plists. May need to reorg workflow. 68 | } else if (Platform.isAndroid) { 69 | // TODO(ckartik): Look into supporting android configuration files. 70 | } else { 71 | throw DRAWInternalError('OS not Recognized by DRAW'); 72 | } 73 | if (osConfigPath == null) { 74 | // Sets osConfigPath to the corresponding root path 75 | // based on the os. 76 | final osDir = path.Context(); 77 | final cwd = osDir.current; 78 | osConfigPath = osDir.rootPrefix(cwd); 79 | } 80 | return path.toUri(path.join(osConfigPath, kFileName)); 81 | } 82 | 83 | /// Returns path to local configuration file. 84 | Uri _getLocalConfigPath() { 85 | final osDir = path.Context(); 86 | final cwd = osDir.current; 87 | return path.toUri(path.join(cwd, kFileName)); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/src/config_file_reader_unsupported.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2019, the Dart Reddit API Wrapper project authors. 2 | /// Please see the AUTHORS file for details. All rights reserved. 3 | /// Use of this source code is governed by a BSD-style license that 4 | /// can be found in the LICENSE file. 5 | 6 | class ConfigFileReader { 7 | ConfigFileReader(String? configUrl); 8 | 9 | /// Loads file from [_localConfigPath] or [_userConfigPath]. 10 | Null loadCorrectFile() => null; 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/exception_objector.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'package:draw/src/exceptions.dart'; 7 | 8 | const int kHttpBadRequest = 400; 9 | const int kHttpNotFound = 404; 10 | 11 | void parseAndThrowError(int status, Map response) { 12 | switch (status) { 13 | case kHttpBadRequest: 14 | case kHttpNotFound: 15 | throw DRAWNotFoundException(response['reason'], response['message']); 16 | default: 17 | throw DRAWUnknownResponseException(status, response.toString()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/frontpage.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'package:draw/draw.dart'; 7 | import 'package:draw/src/base.dart'; 8 | import 'package:draw/src/listing/listing_generator.dart'; 9 | import 'package:draw/src/listing/mixins/base.dart'; 10 | import 'package:draw/src/listing/mixins/gilded.dart'; 11 | import 'package:draw/src/listing/mixins/rising.dart'; 12 | import 'package:draw/src/reddit.dart'; 13 | 14 | /// The [FrontPage] class provides methods to access listings of content on the 15 | /// Reddit front page. 16 | class FrontPage extends RedditBase 17 | with BaseListingMixin, GildedListingMixin, RisingListingMixin { 18 | @override 19 | String path = '/'; 20 | FrontPage(Reddit reddit) : super(reddit); 21 | 22 | /// Returns a [UserContent] that is "best". 23 | /// 24 | /// `limit` is the maximum number of objects returned by Reddit per request 25 | /// (the default is 100). If provided, `after` specifies from which point 26 | /// Reddit will return objects of the requested type. `params` is a set of 27 | /// additional parameters that will be forwarded along with the request. 28 | Stream best( 29 | {int? limit, String? after, Map? params}) => 30 | ListingGenerator.createBasicGenerator(reddit, path + 'best', 31 | limit: limit, after: after, params: params); 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/getter_utils.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'package:draw/src/models/redditor.dart'; 7 | import 'package:draw/src/models/subreddit.dart'; 8 | import 'package:draw/src/reddit.dart'; 9 | 10 | abstract class GetterUtils { 11 | static DateTime? dateTimeOrNull(double? time) { 12 | if (time == null) { 13 | return null; 14 | } 15 | return DateTime.fromMillisecondsSinceEpoch(time.round() * 1000, 16 | isUtc: true); 17 | } 18 | 19 | static DateTime? dateTimeOrNullFromString(String? time) { 20 | if (time == null) { 21 | return null; 22 | } 23 | return DateTime.parse(time); 24 | } 25 | 26 | static RedditorRef? redditorRefOrNull(Reddit reddit, String? redditor) => 27 | (redditor == null) ? null : reddit.redditor(redditor); 28 | 29 | static SubredditRef? subredditRefOrNull(Reddit reddit, String? subreddit) => 30 | (subreddit == null) ? null : reddit.subreddit(subreddit); 31 | 32 | static Uri? uriOrNull(String? uri) => (uri == null) ? null : Uri.parse(uri); 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/image_file_reader.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2019, the Dart Reddit API Wrapper project authors. 2 | /// Please see the AUTHORS file for details. All rights reserved. 3 | /// Use of this source code is governed by a BSD-style license that 4 | /// can be found in the LICENSE file. 5 | 6 | export 'image_file_reader_unsupported.dart' 7 | if (dart.library.io) 'image_file_reader_io.dart'; 8 | -------------------------------------------------------------------------------- /lib/src/image_file_reader_io.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2019, the Dart Reddit API Wrapper project authors. 2 | /// Please see the AUTHORS file for details. All rights reserved. 3 | /// Use of this source code is governed by a BSD-style license that 4 | /// can be found in the LICENSE file. 5 | 6 | import 'dart:io'; 7 | import 'package:draw/src/models/subreddit.dart'; 8 | import 'package:collection/collection.dart'; 9 | 10 | const String _kJpegHeader = '\xff\xd8\xff'; 11 | 12 | Future loadImage(Uri imagePath) async { 13 | final image = File.fromUri(imagePath); 14 | if (!await image.exists()) { 15 | throw FileSystemException('File does not exist', imagePath.toString()); 16 | } 17 | final imageBytes = await image.readAsBytes(); 18 | if (imageBytes.length < _kJpegHeader.length) { 19 | throw FormatException('Invalid image format for file $imagePath.'); 20 | } 21 | final header = imageBytes.sublist(0, _kJpegHeader.length); 22 | final isJpeg = 23 | const IterableEquality().equals(_kJpegHeader.codeUnits, header); 24 | return { 25 | 'imageBytes': imageBytes, 26 | 'imageType': isJpeg ? ImageFormat.jpeg : ImageFormat.png, 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/image_file_reader_unsupported.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2019, the Dart Reddit API Wrapper project authors. 2 | /// Please see the AUTHORS file for details. All rights reserved. 3 | /// Use of this source code is governed by a BSD-style license that 4 | /// can be found in the LICENSE file. 5 | 6 | Future loadImage(Uri? imagePath) async => 7 | throw UnsupportedError('Loading images from disk is not supported on web.'); 8 | -------------------------------------------------------------------------------- /lib/src/listing/listing_generator.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | 8 | import 'package:draw/src/reddit.dart'; 9 | 10 | /// An abstract static class used to generate [Stream]s of [RedditBase] objects. 11 | /// This class should not be used directly, as it is used by various methods 12 | /// defined in children of [RedditBase]. 13 | abstract class ListingGenerator { 14 | static const defaultRequestLimit = 100; 15 | 16 | static int? getLimit(Map? params) { 17 | if ((params != null) && params.containsKey('limit')) { 18 | return int.tryParse(params['limit']!); 19 | } 20 | return null; 21 | } 22 | 23 | static Stream createBasicGenerator( 24 | final Reddit reddit, final String path, 25 | {int? limit, 26 | String? after, 27 | Map? params, 28 | bool objectify = true}) => 29 | generator(reddit, path, 30 | limit: limit ?? getLimit(params), 31 | after: after, 32 | params: params, 33 | objectify: objectify); 34 | 35 | /// An asynchronous iterator method used to make Reddit API calls as defined 36 | /// by [api] in blocks of size [limit]. The default [limit] is specified by 37 | /// [defaultRequestLimit]. [after] specifies which fullname should be used as 38 | /// an anchor point for the slice. Returns a [Stream] which can be iterated 39 | /// over using an asynchronous for-loop. 40 | static Stream generator(final Reddit reddit, final String api, 41 | {int? limit, 42 | String? after, 43 | Map? params, 44 | bool objectify = true}) async* { 45 | final kLimitKey = 'limit'; 46 | final kAfterKey = 'after'; 47 | final nullLimit = 100; 48 | final Map paramsInternal = (params == null) 49 | ? {} 50 | : Map.from(params); 51 | final _limit = limit; 52 | paramsInternal[kLimitKey] = (_limit ?? nullLimit).toString(); 53 | 54 | // If after is provided, we'll start getting objects older than the object 55 | // ID specified. 56 | if (after != null) { 57 | paramsInternal[kAfterKey] = after; 58 | } 59 | 60 | var yielded = 0; 61 | var index = 0; 62 | List? listing; 63 | var exhausted = false; 64 | 65 | Future _nextBatch() async { 66 | if (exhausted) { 67 | return null; 68 | } 69 | var response = 70 | await reddit.get(api, params: paramsInternal, objectify: objectify); 71 | var newListing; 72 | if (response is List) { 73 | newListing = response; 74 | exhausted = true; 75 | } else { 76 | response = response as Map?; 77 | newListing = response!['listing'].cast(); 78 | if (response[kAfterKey] == null) { 79 | exhausted = true; 80 | } else { 81 | paramsInternal[kAfterKey] = response[kAfterKey]; 82 | } 83 | } 84 | if (newListing.length == 0) { 85 | return null; 86 | } 87 | index = 0; 88 | return newListing; 89 | } 90 | 91 | while ((_limit == null) || (yielded < _limit)) { 92 | if ((listing == null) || (index >= listing.length)) { 93 | listing = (await _nextBatch())?.cast(); 94 | if (listing == null) { 95 | break; 96 | } 97 | } 98 | yield listing[index]; 99 | ++index; 100 | ++yielded; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/src/listing/mixins/gilded.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | 8 | import 'package:draw/src/listing/listing_generator.dart'; 9 | import 'package:draw/src/models/mixins/user_content_mixin.dart'; 10 | import 'package:draw/src/reddit.dart'; 11 | 12 | /// A mixin which contains the functionality required to get a [Stream] of 13 | /// gilded content. 14 | mixin GildedListingMixin { 15 | Reddit get reddit; 16 | String get path; 17 | 18 | /// Returns a [Stream] of content that has been gilded. 19 | /// 20 | /// `limit` is the maximum number of objects returned by Reddit per request 21 | /// (the default is 100). If provided, `after` specifies from which point 22 | /// Reddit will return objects of the requested type. `params` is a set of 23 | /// additional parameters that will be forwarded along with the request. 24 | Stream gilded( 25 | {int? limit, String? after, Map? params}) => 26 | ListingGenerator.createBasicGenerator( 27 | reddit, path + 'gilded', 28 | limit: limit, after: after, params: params); 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/listing/mixins/redditor.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | 8 | import '../../models/user_content.dart'; 9 | import '../../reddit.dart'; 10 | import '../listing_generator.dart'; 11 | import 'base.dart'; 12 | 13 | /// A mixin which provides the ability to get [Redditor] related streams. 14 | mixin RedditorListingMixin { 15 | Reddit get reddit; 16 | String get path; 17 | SubListing? _comments; 18 | SubListing? _submissions; 19 | 20 | /// Provides an instance of [SubListing], used to make requests for 21 | /// [Comment]s. 22 | SubListing get comments { 23 | _comments ??= SubListing(reddit, path, 'comments/'); 24 | return _comments!; 25 | } 26 | 27 | /// Provides an instance of [SubListing], used to make requests for 28 | /// [Submission]s. 29 | SubListing get submissions { 30 | _submissions ??= SubListing(reddit, path, 'submitted/'); 31 | return _submissions!; 32 | } 33 | 34 | /// Returns a [Stream] of content that the user has downvoted. 35 | /// 36 | /// `limit` is the maximum number of objects returned by Reddit per request 37 | /// (the default is 100). If provided, `after` specifies from which point 38 | /// Reddit will return objects of the requested type. `params` is a set of 39 | /// additional parameters that will be forwarded along with the request. 40 | /// 41 | /// May raise an exception on access if the current user is not authorized to 42 | /// access this list. 43 | Stream downvoted( 44 | {int? limit, String? after, Map? params}) => 45 | ListingGenerator.createBasicGenerator(reddit, path + 'downvoted', 46 | limit: limit, after: after, params: params); 47 | 48 | /// Returns a [Stream] of content that the user has gilded. 49 | /// 50 | /// `limit` is the maximum number of objects returned by Reddit per request 51 | /// (the default is 100). If provided, `after` specifies from which point 52 | /// Reddit will return objects of the requested type. `params` is a set of 53 | /// additional parameters that will be forwarded along with the request. 54 | /// 55 | /// May raise an exception on access if the current user is not authorized to 56 | /// access this list. 57 | Stream gildings( 58 | {int? limit, String? after, Map? params}) => 59 | ListingGenerator.createBasicGenerator(reddit, path + 'gilded/given', 60 | limit: limit, after: after, params: params); 61 | 62 | /// Returns a [Stream] of content that the user has hidden. 63 | /// 64 | /// `limit` is the maximum number of objects returned by Reddit per request 65 | /// (the default is 100). If provided, `after` specifies from which point 66 | /// Reddit will return objects of the requested type. `params` is a set of 67 | /// additional parameters that will be forwarded along with the request. 68 | /// 69 | /// May raise an exception on access if the current user is not authorized to 70 | /// access this list. 71 | Stream hidden( 72 | {int? limit, String? after, Map? params}) => 73 | ListingGenerator.createBasicGenerator(reddit, path + 'hidden', 74 | limit: limit, after: after, params: params); 75 | 76 | /// Returns a [Stream] of content that the user has saved. 77 | /// 78 | /// `limit` is the maximum number of objects returned by Reddit per request 79 | /// (the default is 100). If provided, `after` specifies from which point 80 | /// Reddit will return objects of the requested type. `params` is a set of 81 | /// additional parameters that will be forwarded along with the request. 82 | /// 83 | /// May raise an exception on access if the current user is not authorized to 84 | /// access this list. 85 | Stream saved( 86 | {int? limit, String? after, Map? params}) => 87 | ListingGenerator.createBasicGenerator(reddit, path + 'saved', 88 | limit: limit, after: after, params: params); 89 | 90 | /// Returns a [Stream] of content that the user has upvoted. 91 | /// 92 | /// `limit` is the maximum number of objects returned by Reddit per request 93 | /// (the default is 100). If provided, `after` specifies from which point 94 | /// Reddit will return objects of the requested type. `params` is a set of 95 | /// additional parameters that will be forwarded along with the request. 96 | /// 97 | /// May raise an exception on access if the current user is not authorized to 98 | /// access this list. 99 | Stream upvoted( 100 | {int? limit, String? after, Map? params}) => 101 | ListingGenerator.createBasicGenerator(reddit, path + 'upvoted', 102 | limit: limit, after: after, params: params); 103 | } 104 | 105 | class SubListing extends Object with BaseListingMixin { 106 | @override 107 | final Reddit reddit; 108 | late String _path; 109 | @override 110 | String get path => _path; 111 | 112 | SubListing(this.reddit, final String path, final String api) { 113 | _path = path + api; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /lib/src/listing/mixins/rising.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | 8 | import '../listing_generator.dart'; 9 | import '../../reddit.dart'; 10 | import '../../models/user_content.dart'; 11 | 12 | mixin RisingListingMixin { 13 | Reddit get reddit; 14 | String get path; 15 | 16 | /// Returns a random [UserContent] that is "rising". 17 | /// 18 | /// `limit` is the maximum number of objects returned by Reddit per request 19 | /// (the default is 100). If provided, `after` specifies from which point 20 | /// Reddit will return objects of the requested type. `params` is a set of 21 | /// additional parameters that will be forwarded along with the request. 22 | Stream randomRising( 23 | {int? limit, String? after, Map? params}) => 24 | ListingGenerator.createBasicGenerator(reddit, path + 'randomrising', 25 | limit: limit, after: after, params: params); 26 | 27 | /// Returns a [UserContent] that is "rising". 28 | /// 29 | /// `limit` is the maximum number of objects returned by Reddit per request 30 | /// (the default is 100). If provided, `after` specifies from which point 31 | /// Reddit will return objects of the requested type. `params` is a set of 32 | /// additional parameters that will be forwarded along with the request. 33 | Stream rising( 34 | {int? limit, String? after, Map? params}) => 35 | ListingGenerator.createBasicGenerator(reddit, path + 'rising', 36 | limit: limit, after: after, params: params); 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/listing/mixins/subreddit.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | 8 | import '../listing_generator.dart'; 9 | import '../../base.dart'; 10 | import '../../models/comment.dart'; 11 | import '../../models/subreddit.dart'; 12 | import '../../reddit.dart'; 13 | import 'gilded.dart'; 14 | 15 | mixin SubredditListingMixin { 16 | Reddit get reddit; 17 | String get path; 18 | CommentHelper? _commentHelper; 19 | CommentHelper get comments { 20 | _commentHelper ??= CommentHelper(this as SubredditRef); 21 | return _commentHelper!; 22 | } 23 | } 24 | 25 | class CommentHelper extends RedditBase with GildedListingMixin { 26 | @override 27 | String get path => _subreddit.path; 28 | final SubredditRef _subreddit; 29 | CommentHelper(this._subreddit) : super(_subreddit.reddit); 30 | 31 | // TODO(bkonyi): document. 32 | Stream call( 33 | {int? limit, String? after, Map? params}) => 34 | ListingGenerator.generator(reddit, _path(), 35 | limit: limit ?? ListingGenerator.getLimit(params), 36 | after: after, 37 | params: params); 38 | 39 | String _path() => _subreddit.path + 'comments/'; 40 | } 41 | -------------------------------------------------------------------------------- /lib/src/logging.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:convert'; 7 | 8 | import 'package:logging/logging.dart'; 9 | export 'package:logging/logging.dart' show Level, Logger; 10 | 11 | const JsonEncoder e = JsonEncoder.withIndent(' '); 12 | 13 | abstract class DRAWLoggingUtils { 14 | static void initialize() { 15 | Logger.root.level = Level.OFF; 16 | Logger.root.onRecord.listen((LogRecord rec) { 17 | print('${rec.level.name} (${rec.loggerName}): ${rec.message}'); 18 | }); 19 | } 20 | 21 | static void setLogLevel(Level l) => Logger.root.level = l; 22 | 23 | static String jsonify(jObj) => e.convert(jObj); 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/models/comment.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | export 'comment_impl.dart' hide setSubmissionInternal, setRepliesInternal; 7 | -------------------------------------------------------------------------------- /lib/src/models/flair.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:draw/src/exceptions.dart'; 4 | import 'package:draw/src/reddit.dart'; 5 | import 'package:draw/src/models/redditor.dart'; 6 | 7 | enum FlairPosition { left, right, disabled } 8 | 9 | String flairPositionToString(FlairPosition p) { 10 | switch (p) { 11 | case FlairPosition.left: 12 | return 'left'; 13 | case FlairPosition.right: 14 | return 'right'; 15 | default: 16 | throw DRAWUnimplementedError(); 17 | } 18 | } 19 | 20 | FlairPosition stringToFlairPosition(String? p) { 21 | switch (p) { 22 | case 'left': 23 | return FlairPosition.left; 24 | case 'right': 25 | return FlairPosition.right; 26 | default: 27 | return FlairPosition.disabled; 28 | } 29 | } 30 | 31 | class _FlairBase { 32 | String? get flairCssClass => _data['flair_css_class']; 33 | String? get flairText => _data['flair_text']; 34 | 35 | final Map _data; 36 | 37 | _FlairBase(String flairCssClass, String flairText) 38 | : _data = { 39 | 'flair_css_class': flairCssClass, 40 | 'flair_text': flairText 41 | }; 42 | 43 | _FlairBase.parse(Map data) : _data = data; 44 | 45 | @override 46 | String toString() => JsonEncoder.withIndent(' ').convert(_data); 47 | } 48 | 49 | /// A simple representation of a template for Reddit flair. 50 | class FlairTemplate extends _FlairBase { 51 | String get flairTemplateId => _data['flair_template_id']; 52 | bool get flairTextEditable => _data['flair_text_editable']; 53 | final FlairPosition position; 54 | 55 | FlairTemplate(String flairCssClass, String flairTemplateId, 56 | bool flairTextEditable, String flairText, this.position) 57 | : super(flairCssClass, flairText) { 58 | _data['flair_template_id'] = flairTemplateId; 59 | _data['flair_text_editable'] = flairTextEditable; 60 | } 61 | 62 | FlairTemplate.parse(Map data) 63 | : position = stringToFlairPosition(data['flair_position']), 64 | super.parse(data); 65 | } 66 | 67 | /// A simple representation of Reddit flair. 68 | class Flair extends _FlairBase { 69 | final RedditorRef user; 70 | 71 | Flair(this.user, {String flairCssClass = '', String flairText = ''}) 72 | : super(flairCssClass, flairText); 73 | 74 | Flair.parse(Reddit reddit, Map data) 75 | : user = RedditorRef.name(reddit, data['user']), 76 | super.parse(data); 77 | 78 | @override 79 | String toString() => JsonEncoder.withIndent(' ').convert(_data); 80 | } 81 | -------------------------------------------------------------------------------- /lib/src/models/inbox.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | import 'dart:math'; 8 | 9 | import 'package:draw/src/api_paths.dart'; 10 | import 'package:draw/src/base_impl.dart'; 11 | import 'package:draw/src/reddit.dart'; 12 | import 'package:draw/src/util.dart'; 13 | import 'package:draw/src/listing/listing_generator.dart'; 14 | import 'package:draw/src/models/comment.dart'; 15 | import 'package:draw/src/models/message.dart'; 16 | 17 | /// A utility class used to interact with the Reddit inbox. 18 | class Inbox extends RedditBase { 19 | static final _messagesRegExp = RegExp(r'{id}'); 20 | 21 | Inbox(Reddit reddit) : super(reddit); 22 | 23 | /// Returns a [Stream] of all inbox comments and messages. 24 | Stream all() => 25 | ListingGenerator.createBasicGenerator(reddit, apiPath['inbox']); 26 | 27 | /// Marks a list of [Comment] and/or [Message] objects as collapsed. 28 | Future collapse(List items) => _itemHelper(apiPath['collapse'], items); 29 | 30 | /// Returns a [Stream] of all comment replies. 31 | Stream commentReplies() => 32 | ListingGenerator.createBasicGenerator(reddit, apiPath['comment_replies']); 33 | 34 | /// Marks a list of [Comment] and/or [Message] objects as read. 35 | Future markRead(List items) => 36 | _itemHelper(apiPath['read_message'], items); 37 | 38 | /// Marks a list of [Comment] and/or [Message] objects as unread. 39 | Future markUnread(List items) => 40 | _itemHelper(apiPath['unread_message'], items); 41 | 42 | Future _itemHelper(String api, List items) async { 43 | var start = 0; 44 | var end = min(items.length, 25); 45 | while (true) { 46 | final sublist = items.sublist(start, end); 47 | final nameList = []; 48 | for (final m in sublist) { 49 | nameList.add(await m.fullname); 50 | } 51 | final data = { 52 | 'id': nameList.join(','), 53 | }; 54 | await reddit.post(api, data, discardResponse: true); 55 | start = end; 56 | end = min(items.length, end + 25); 57 | if (start == end) { 58 | break; 59 | } 60 | } 61 | } 62 | 63 | /// Returns a [Stream] of comments in which the currently 64 | /// authenticated user was mentioned. 65 | /// 66 | /// A mention is a [Comment] in which the authorized user is named in the 67 | /// form: ```/u/redditor_name```. 68 | Stream mentions() => 69 | ListingGenerator.createBasicGenerator(reddit, apiPath['mentions']); 70 | 71 | /// Returns a [Message] associated with a given fullname. 72 | /// 73 | /// If [messageId] is not a valid fullname for a message, null is returned. 74 | Future message(String messageId) async { 75 | final listing = await reddit 76 | .get(apiPath['message'].replaceAll(_messagesRegExp, messageId)); 77 | final messages = []; 78 | final message = listing['listing'][0]; 79 | messages.add(message); 80 | messages.addAll(message.replies); 81 | for (final m in messages) { 82 | if (m.fullname == messageId) { 83 | return m; 84 | } 85 | } 86 | return null; 87 | } 88 | 89 | /// Returns a [Stream] of inbox messages. 90 | Stream messages() => 91 | ListingGenerator.createBasicGenerator(reddit, apiPath['messages']); 92 | 93 | /// Returns a [Stream] of sent messages. 94 | Stream sent() => 95 | ListingGenerator.createBasicGenerator(reddit, apiPath['sent']); 96 | 97 | /// Returns a live [Stream] of [Comment] and [Message] objects. 98 | /// 99 | /// If [pauseAfter] is provided, the [Stream] will close after [pauseAfter] 100 | /// results are yielded. Oldest items are yielded first. 101 | Stream stream({int? pauseAfter}) => 102 | streamGenerator(unread, pauseAfter: pauseAfter); 103 | 104 | /// Returns a [Stream] of replies to submissions made by the 105 | /// currently authenticated user. 106 | Stream submissionReplies() => ListingGenerator.createBasicGenerator( 107 | reddit, apiPath['submission_replies']); 108 | 109 | /// Marks a list of [Comment] and/or [Message] objects as uncollapsed. 110 | Future uncollapse(List items) => 111 | _itemHelper(apiPath['uncollapse'], items); 112 | 113 | /// Returns a [Stream] of [Comment] and/or [Message] objects that have not yet 114 | /// been read. 115 | /// 116 | /// [markRead] specifies whether or not to mark the inbox as having no new 117 | /// messages. 118 | Stream unread({int? limit, bool markRead = false}) { 119 | final params = { 120 | 'mark': markRead.toString(), 121 | }; 122 | return ListingGenerator.generator(reddit, apiPath['unread'], 123 | params: params, limit: limit); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /lib/src/models/message.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2018, the Dart Reddit API Wrapper project authors. 2 | /// Please see the AUTHORS file for details. All rights reserved. 3 | /// Use of this source code is governed by a BSD-style license that 4 | /// can be found in the LICENSE file. 5 | 6 | import 'package:draw/src/base_impl.dart'; 7 | import 'package:draw/src/getter_utils.dart'; 8 | import 'package:draw/src/models/mixins/inboxable.dart'; 9 | import 'package:draw/src/models/subreddit.dart'; 10 | import 'package:draw/src/reddit.dart'; 11 | 12 | /// A fully initialized class which represents a Message from the [Inbox]. 13 | class Message extends RedditBase 14 | with InboxableMixin, RedditBaseInitializedMixin { 15 | var _replies; 16 | 17 | Message.parse(Reddit reddit, Map? data) : super(reddit) { 18 | setData(this, data); 19 | } 20 | 21 | /// The author of the [Message]. 22 | String get author => data!['author']; 23 | 24 | /// The body of the [Message]. 25 | String get body => data!['body']; 26 | 27 | /// When was this [Message] created. 28 | DateTime get createdUtc => 29 | DateTime.fromMillisecondsSinceEpoch(data!['created_utc'].round() * 1000, 30 | isUtc: true); 31 | 32 | /// Who is this [Message] for. 33 | /// 34 | /// Can be for either a [Redditor] or [Subreddit]. 35 | String get destination => data!['dest']; 36 | 37 | /// The type of distinguishment that is assigned to this message. 38 | /// 39 | /// Can be `null` if the [Message] isn't distinguished. An example value for 40 | /// this field is 'moderator' 41 | String get distinguished => data!['distinguished']; 42 | 43 | /// The [List] of replies to this [Message]. 44 | /// 45 | /// Returns and empty list if there are no replies. 46 | List get replies { 47 | if (_replies == null) { 48 | _replies = []; 49 | final repliesListing = data!['replies']; 50 | if (repliesListing == null) { 51 | return []; 52 | } 53 | final replies = repliesListing['data']['children']; 54 | for (final reply in replies) { 55 | _replies.add(reddit.objector.objectify(reply)); 56 | } 57 | } 58 | return _replies; 59 | } 60 | 61 | /// A [SubredditRef] representing the subreddit this message was sent to, 62 | /// for ModMail [Message]s. 63 | /// 64 | /// Returns `null` if this is not a ModMail [Message]. 65 | SubredditRef? get subreddit => 66 | GetterUtils.subredditRefOrNull(reddit, data!['subreddit']); 67 | } 68 | -------------------------------------------------------------------------------- /lib/src/models/mixins/editable.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | 8 | import '../../api_paths.dart'; 9 | import '../../base_impl.dart'; 10 | import '../user_content.dart'; 11 | 12 | /// Interface for classes that can be edited and deleted. 13 | mixin EditableMixin implements RedditBaseInitializedMixin { 14 | /// Delete the object. 15 | Future delete() async => 16 | reddit.post(apiPath['del'], {'id': fullname}, discardResponse: true); 17 | 18 | /// Replace the body of the object with [body]. 19 | /// 20 | /// [body] is the markdown formatted content for the updated object. Returns 21 | /// the current instance of the object after updating its fields. 22 | Future edit(String body) async { 23 | final data = {'text': body, 'thing_id': fullname, 'api_type': 'json'}; 24 | // TODO(bkonyi): figure out what needs to be done here. 25 | final updated = await reddit.post(apiPath['edit'], data); 26 | setData(this, updated[0].data); 27 | return this as UserContent; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/models/mixins/gildable.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | 8 | import 'package:draw/src/api_paths.dart'; 9 | import 'package:draw/src/base_impl.dart'; 10 | 11 | /// Interface for classes that can be gilded. 12 | mixin GildableMixin implements RedditBaseInitializedMixin { 13 | /// Gild the author of the item. 14 | Future gild() async => reddit.post( 15 | apiPath['gild_thing'].replaceAll(RegExp(r'{fullname}'), fullname), null); 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/models/mixins/inboxable.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | 8 | import 'package:draw/src/api_paths.dart'; 9 | import 'package:draw/src/base_impl.dart'; 10 | 11 | /// A mixin containing inbox functionality. 12 | mixin InboxableMixin implements RedditBaseInitializedMixin { 13 | /// Returns true if the [Inbox] item is new. 14 | bool get newItem => data!['new']; 15 | 16 | /// The subject of the [Inbox] item. 17 | /// 18 | /// If this item was not retrieved via the [Inbox], this may be null. 19 | String? get subject => data!['subject']; 20 | 21 | /// Returns true if the current [Inbox] reply was from a [Comment]. 22 | bool get wasComment => data!['was_comment']; 23 | 24 | /// Block the user who sent the item. 25 | /// 26 | /// Note: Reddit does not permit blocking users unless you have a [Comment] or 27 | /// [Message] from them in your inbox. 28 | Future block() async => 29 | await reddit.post(apiPath['block'], {'id': fullname}); 30 | 31 | /// Mark the item as collapsed. 32 | /// 33 | /// This method pertains only to objects which were retrieved via the inbox. 34 | Future collapse() async => await reddit.inbox.collapse([this]); 35 | 36 | /// Mark the item as read. 37 | /// 38 | /// This method pertains only to objects which were retrieved via the inbox. 39 | Future markRead() async => await reddit.inbox.markRead([this]); 40 | 41 | /// Mark the item as unread. 42 | /// 43 | /// This method pertains only to objects which were retrieved via the inbox. 44 | Future markUnread() async => await reddit.inbox.markUnread([this]); 45 | 46 | /// Mark the item as collapsed. 47 | /// 48 | /// This method pertains only to objects which were retrieved via the inbox. 49 | Future uncollapse() async => await reddit.inbox.uncollapse([this]); 50 | 51 | /// Removes the message from the recipient's view of their inbox. 52 | Future remove() async => 53 | await reddit.post(apiPath['del_msg'], {'id': fullname!}); 54 | } 55 | -------------------------------------------------------------------------------- /lib/src/models/mixins/inboxtoggleable.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | 8 | import 'package:draw/src/api_paths.dart'; 9 | import 'package:draw/src/base_impl.dart'; 10 | 11 | /// Interface for classes that can optionally receive inbox replies. 12 | mixin InboxToggleableMixin implements RedditBaseInitializedMixin { 13 | /// Disable inbox replies for the item. 14 | Future disableInboxReplies() async => 15 | reddit.post(apiPath['sendreplies'], {'id': fullname, 'state': 'false'}, 16 | discardResponse: true); 17 | 18 | /// Enable inbox replies for the item. 19 | Future enableInboxReplies() async => 20 | reddit.post(apiPath['sendreplies'], {'id': fullname, 'state': 'true'}, 21 | discardResponse: true); 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/models/mixins/messageable.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | 8 | import 'package:draw/src/api_paths.dart'; 9 | import 'package:draw/src/exceptions.dart'; 10 | import 'package:draw/src/reddit.dart'; 11 | import 'package:draw/src/models/subreddit.dart'; 12 | 13 | /// A mixin containing functionality to send messages to other [Redditor]s or 14 | /// [Subreddit] moderators. 15 | mixin MessageableMixin { 16 | Reddit get reddit; 17 | String get displayName; 18 | 19 | /// Send a message. 20 | /// 21 | /// [subject] is the subject of the message, [message] is the content of the 22 | /// message, and [fromSubreddit] is a [Subreddit] that the message should be 23 | /// sent from. [fromSubreddit] must be a subreddit that the current user is a 24 | /// moderator of and has permissions to send mail on behalf of the subreddit. 25 | // TODO(bkonyi): error handling 26 | Future message(String subject, String message, 27 | {SubredditRef? fromSubreddit}) async { 28 | var messagePrefix = ''; 29 | if (this is Subreddit) { 30 | messagePrefix = '#'; 31 | } 32 | final data = { 33 | 'subject': subject, 34 | 'text': message, 35 | 'to': messagePrefix + displayName, 36 | 'api_type': 'json', 37 | }; 38 | 39 | if (fromSubreddit != null) { 40 | data['from_sr'] = fromSubreddit.displayName; 41 | } 42 | 43 | try { 44 | await reddit.post(apiPath['compose'], data); 45 | } on DRAWInvalidSubredditException catch (e) { 46 | String name; 47 | if (e.subredditName == 'from_sr') { 48 | name = fromSubreddit!.displayName; 49 | } else { 50 | name = displayName; 51 | } 52 | throw DRAWInvalidSubredditException(name); 53 | // ignore: unused_catch_clause 54 | } on DRAWInvalidRedditorException catch (e) { 55 | throw DRAWInvalidRedditorException(displayName); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/src/models/mixins/replyable.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | 8 | import 'package:draw/src/api_paths.dart'; 9 | import 'package:draw/src/base_impl.dart'; 10 | import 'package:draw/src/models/comment.dart'; 11 | 12 | /// A mixin for RedditBase classes that can be replied to. 13 | mixin ReplyableMixin implements RedditBaseInitializedMixin { 14 | // TODO(bkonyi): check if we actually need to access an array element. 15 | /// Reply to the object. 16 | /// 17 | /// [body] is the markdown formatted content for a comment. Returns a 18 | /// [Comment] for the newly created comment. 19 | Future reply(String body) async => (await reddit.post( 20 | apiPath['comment'], 21 | {'text': body, 'thing_id': fullname, 'api_type': 'json'}))[0]; 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/models/mixins/reportable.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | 8 | import 'package:draw/src/api_paths.dart'; 9 | import 'package:draw/src/base_impl.dart'; 10 | 11 | /// Interface for ReddieBase classes that can be reported. 12 | mixin ReportableMixin implements RedditBaseInitializedMixin { 13 | /// Report this object to the moderators of its [Subreddit]. 14 | /// 15 | /// [reason] is the reason for the report. 16 | Future report(String reason) async => reddit.post( 17 | apiPath['report'], {'id': fullname, 'reason': reason, 'api_type': 'json'}, 18 | discardResponse: true); 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/models/mixins/saveable.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | 8 | import 'package:draw/src/api_paths.dart'; 9 | import 'package:draw/src/base_impl.dart'; 10 | 11 | /// Mixin for RedditBase classes that can be saved. 12 | mixin SaveableMixin implements RedditBaseInitializedMixin { 13 | /// Save the object. 14 | /// 15 | /// [category] (Gold only) is the category to save the object to. If your user does not 16 | /// have gold, this value is ignored. 17 | Future save({String? category}) async => 18 | reddit.post(apiPath['save'], {'category': category ?? '', 'id': fullname}, 19 | discardResponse: true); 20 | 21 | /// Unsave the object. 22 | Future unsave() async => 23 | reddit.post(apiPath['unsave'], {'id': fullname}, discardResponse: true); 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/models/mixins/user_content_mixin.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'package:draw/src/base_impl.dart'; 7 | 8 | mixin UserContentInitialized implements RedditBaseInitializedMixin { 9 | /// The amount of silver gilded to this [UserContent]. 10 | int get silver => data!['gildings']['gid_1']; 11 | 12 | /// The amount of gold gilded to this [UserContent]. 13 | int get gold => data!['gildings']['gid_2']; 14 | 15 | /// The amount of platinum gilded to this [UserContent]. 16 | int get platinum => data!['gildings']['gid_3']; 17 | 18 | /// A [List] of reports made by moderators. 19 | /// 20 | /// Each report consists of a list with two entries. The first entry is the 21 | /// name of the moderator who submitted the report. The second is the report 22 | /// reason. 23 | List> get modReports { 24 | final reports = data!['mod_reports'] as List; 25 | return reports.map>((e) => e.cast()).toList(); 26 | } 27 | 28 | /// A [List] of reports made by users. 29 | /// 30 | /// Each report consists of a list with two entries. The first entry is the 31 | /// report reason. The second is the number of times this reason has been 32 | /// reported. 33 | List> get userReports => 34 | (data!['user_reports'] as List).cast>(); 35 | 36 | /// True if the currently authenticated user has marked this content as saved. 37 | bool get saved => data!['saved']; 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/models/mixins/user_content_moderation.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | 8 | import 'package:draw/src/api_paths.dart'; 9 | import 'package:draw/src/base_impl.dart'; 10 | import 'package:draw/src/exceptions.dart'; 11 | import 'package:draw/src/models/comment.dart'; 12 | 13 | enum DistinctionType { 14 | yes, 15 | no, 16 | admin, 17 | special, 18 | } 19 | 20 | String distinctionTypeToString(DistinctionType type) { 21 | switch (type) { 22 | case DistinctionType.yes: 23 | return 'yes'; 24 | case DistinctionType.no: 25 | return 'no'; 26 | case DistinctionType.admin: 27 | return 'admin'; 28 | case DistinctionType.special: 29 | return 'special'; 30 | default: 31 | throw DRAWInternalError('Invalid user content distinction type: $type.'); 32 | } 33 | } 34 | 35 | /// Provides moderation methods for [Comment]s and [Submission]s. 36 | mixin UserContentModerationMixin { 37 | RedditBaseInitializedMixin get content; 38 | 39 | /// Approve a [Comment] or [Submission]. 40 | /// 41 | /// Approving a comment or submission reverts removal, resets the report 42 | /// counter, adds a green check mark which is visible to other moderators 43 | /// on the site, and sets the `approvedBy` property to the authenticated 44 | /// user. 45 | Future approve() async => content.reddit.post( 46 | apiPath['approve'], {'id': content.fullname!}, 47 | discardResponse: true); 48 | 49 | /// Distinguish a [Comment] or [Submission]. 50 | /// 51 | /// `how` is a [DistinctionType] value, where `yes` distinguishes the content 52 | /// as a moderator, and `no` removes distinction. `admin` and `special` 53 | /// require special privileges. 54 | /// 55 | /// If `sticky` is `true` and the comment is top-level, the [Comment] will be 56 | /// placed at the top of the comment thread. If the item to be distinguished 57 | /// is not a [Comment] or is not a top-level [Comment], this parameter is 58 | /// ignored. 59 | Future distinguish( 60 | {required DistinctionType how, bool sticky = false}) async { 61 | final data = { 62 | 'how': distinctionTypeToString(how), 63 | 'id': content.fullname!, 64 | 'api_type': 'json' 65 | }; 66 | if (sticky && (content is Comment) && (content as Comment).isRoot) { 67 | data['sticky'] = 'true'; 68 | } 69 | return content.reddit.post(apiPath['distinguish'], data); 70 | } 71 | 72 | /// Ignore Future reports on a [Comment] or [Submission]. 73 | /// 74 | /// Prevents Future reports on this [Comment] or [Submission] from triggering 75 | /// notifications and appearing in the mod queue. The report count will 76 | /// continue to increment. 77 | Future ignoreReports() async => content.reddit.post( 78 | apiPath['ignore_reports'], {'id': content.fullname!}, 79 | discardResponse: true); 80 | 81 | /// Remove a [Comment] or [Submission]. 82 | /// 83 | /// Set `spam` to `true` to help train the subreddit's spam filter. 84 | Future remove({bool spam = false}) async => content.reddit.post( 85 | apiPath['remove'], 86 | {'id': content.fullname!, 'spam': spam.toString()}, 87 | discardResponse: true); 88 | 89 | /// Remove distinguishing on a [Comment] or [Submission]. 90 | Future undistinguish() async => distinguish(how: DistinctionType.no); 91 | 92 | /// Resume receiving Future reports for a [Comment] or [Submission]. 93 | /// 94 | /// Future reports will trigger notifications and appear in the mod queue. 95 | Future unignoreReports() async => content.reddit.post( 96 | apiPath['unignore_reports'], {'id': content.fullname!}, 97 | discardResponse: true); 98 | } 99 | -------------------------------------------------------------------------------- /lib/src/models/mixins/voteable.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | 8 | import 'package:draw/src/api_paths.dart'; 9 | import 'package:draw/src/base.dart'; 10 | 11 | enum VoteState { 12 | none, 13 | upvoted, 14 | downvoted, 15 | } 16 | 17 | int _voteStateToIndex(VoteState vote) { 18 | switch (vote) { 19 | case VoteState.none: 20 | return 0; 21 | case VoteState.upvoted: 22 | return 1; 23 | case VoteState.downvoted: 24 | return -1; 25 | } 26 | } 27 | 28 | /// A mixin which provides voting functionality for [Comment] and [Submission]. 29 | mixin VoteableMixin implements RedditBaseInitializedMixin { 30 | /// The author of the item. 31 | String get author => data!['author']; 32 | 33 | /// The body of the item. 34 | /// 35 | /// Returns null for non-text [Submission]s. 36 | String? get body => data!['body']; 37 | 38 | /// The karma score of the voteable item. 39 | int get score => data!['score']; 40 | 41 | /// Has the currently authenticated [User] voted on this [UserContent]. 42 | /// 43 | /// Returns [VoteState.upvoted] if the content has been upvoted, 44 | /// [VoteState.downvoted] if it has been downvoted, and [VoteState.none] 45 | /// otherwise. 46 | VoteState get vote { 47 | if (data!['likes'] == null) { 48 | return VoteState.none; 49 | } else if (data!['likes']) { 50 | return VoteState.upvoted; 51 | } else { 52 | return VoteState.downvoted; 53 | } 54 | } 55 | 56 | void _updateScore(VoteState newVote) { 57 | if (vote == VoteState.upvoted) { 58 | if (newVote == VoteState.downvoted) { 59 | data!['score'] = score - 2; 60 | } else if (newVote == VoteState.none) { 61 | data!['score'] = score - 1; 62 | } 63 | } else if (vote == VoteState.none) { 64 | if (newVote == VoteState.downvoted) { 65 | data!['score'] = score - 1; 66 | } else if (newVote == VoteState.upvoted) { 67 | data!['score'] = score + 1; 68 | } 69 | } else if (vote == VoteState.downvoted) { 70 | if (newVote == VoteState.upvoted) { 71 | data!['score'] = score + 2; 72 | } else if (newVote == VoteState.none) { 73 | data!['score'] = score + 1; 74 | } 75 | } 76 | } 77 | 78 | Future _vote(VoteState direction, bool waitForResponse) async { 79 | if (vote == direction) { 80 | return; 81 | } 82 | final response = reddit.post( 83 | apiPath['vote'], 84 | { 85 | 'dir': _voteStateToIndex(direction).toString(), 86 | 'id': fullname, 87 | }, 88 | discardResponse: true); 89 | if (waitForResponse) { 90 | await response; 91 | } 92 | 93 | _updateScore(direction); 94 | 95 | switch (direction) { 96 | case VoteState.none: 97 | data!['likes'] = null; 98 | break; 99 | case VoteState.upvoted: 100 | data!['likes'] = true; 101 | break; 102 | case VoteState.downvoted: 103 | data!['likes'] = false; 104 | } 105 | } 106 | 107 | /// Clear the authenticated user's vote on the object. 108 | /// 109 | /// Note: votes must be cast on behalf of a human user (i.e., no automatic 110 | /// voting by bots). See Reddit rules for more details on what is considered 111 | /// vote cheating or manipulation. 112 | Future clearVote({bool waitForResponse = true}) async => 113 | _vote(VoteState.none, waitForResponse); 114 | 115 | /// Clear the authenticated user's vote on the object. 116 | /// 117 | /// Note: votes must be cast on behalf of a human user (i.e., no automatic 118 | /// voting by bots). See Reddit rules for more details on what is considered 119 | /// vote cheating or manipulation. 120 | Future downvote({bool waitForResponse = true}) async => 121 | _vote(VoteState.downvoted, waitForResponse); 122 | 123 | /// Clear the authenticated user's vote on the object. 124 | /// 125 | /// Note: votes must be cast on behalf of a human user (i.e., no automatic 126 | /// voting by bots). See Reddit rules for more details on what is considered 127 | /// vote cheating or manipulation. 128 | Future upvote({bool waitForResponse = true}) async => 129 | _vote(VoteState.upvoted, waitForResponse); 130 | } 131 | -------------------------------------------------------------------------------- /lib/src/models/submission.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | export 'submission_impl.dart' 7 | hide commentSortTypeToString, getCommentByIdInternal, insertCommentById; 8 | -------------------------------------------------------------------------------- /lib/src/models/subreddits.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | 8 | import 'package:draw/src/api_paths.dart'; 9 | import 'package:draw/src/exceptions.dart'; 10 | import 'package:draw/src/listing/listing_generator.dart'; 11 | import 'package:draw/src/models/subreddit.dart'; 12 | import 'package:draw/src/reddit.dart'; 13 | import 'package:draw/src/util.dart'; 14 | 15 | /// Contains functionality which allows for querying information related to 16 | /// Subreddits. 17 | class Subreddits { 18 | static final RegExp _kSubredditsRegExp = RegExp(r'{subreddits}'); 19 | final Reddit reddit; 20 | 21 | Subreddits(this.reddit); 22 | 23 | /// Returns a [Stream] of default subreddits. 24 | Stream defaults({int? limit, Map? params}) => 25 | ListingGenerator.createBasicGenerator( 26 | reddit, apiPath['subreddits_default'], 27 | limit: limit, params: params); 28 | 29 | /// Returns a [Stream] of gold subreddits. 30 | Stream gold({int? limit, Map? params}) => 31 | ListingGenerator.createBasicGenerator(reddit, apiPath['subreddits_gold'], 32 | limit: limit, params: params); 33 | 34 | /// Returns a [Stream] of new subreddits. 35 | Stream newest({int? limit, Map? params}) => 36 | ListingGenerator.createBasicGenerator(reddit, apiPath['subreddits_new'], 37 | limit: limit, params: params); 38 | 39 | /// Returns a [Stream] of popular subreddits. 40 | Stream popular({int? limit, Map? params}) => 41 | ListingGenerator.createBasicGenerator( 42 | reddit, apiPath['subreddits_popular'], 43 | limit: limit, params: params); 44 | 45 | /// Returns a [List] of subreddit recommendedations based on the given list 46 | /// of subreddits. 47 | /// 48 | /// `subreddits` is a list of [SubredditRef]s and/or subreddit names as 49 | /// [String]s. `omitSubreddits` is a list of [SubredditRef]s and/or subreddit 50 | /// names as [String]s which are to be excluded from the results. 51 | Future> recommended(List subreddits, 52 | {List? omitSubreddits}) async { 53 | String subredditsToString(List? subs) { 54 | if (subs == null) { 55 | return ''; 56 | } 57 | final strings = []; 58 | for (final s in subs) { 59 | if (s is SubredditRef) { 60 | strings.add(s.displayName); 61 | } else if (s is String) { 62 | strings.add(s); 63 | } else { 64 | throw DRAWArgumentError('A subreddit list must contain either ' 65 | 'instances of SubredditRef or String. Got type: ${s.runtimeType}.'); 66 | } 67 | } 68 | return strings.join(','); 69 | } 70 | 71 | final params = {'omit': subredditsToString(omitSubreddits)}; 72 | final url = apiPath['sub_recommended'] 73 | .replaceAll(_kSubredditsRegExp, subredditsToString(subreddits)); 74 | 75 | return [ 76 | for (final sub 77 | in (await reddit.get(url, params: params, objectify: false)) 78 | .cast()) 79 | SubredditRef.name(reddit, sub['sr_name']) 80 | ]; 81 | } 82 | 83 | /// Returns a [Stream] of subreddits matching `query`. 84 | /// 85 | /// This search is performed using both the title and description of 86 | /// subreddits. To search solely by name, see [Subreddits.searchByName]. 87 | Stream search(String query, 88 | {int? limit, Map? params}) { 89 | params ??= {}; 90 | params['q'] = query; 91 | return ListingGenerator.createBasicGenerator( 92 | reddit, apiPath['subreddits_search'], 93 | limit: limit, params: params); 94 | } 95 | 96 | /// Returns a [List] of subreddits whose names being with 97 | /// `query`. 98 | /// 99 | /// If `includeNsfw` is true, NSFW subreddits will be included in the 100 | /// results. If `exact` is true, only results which are directly matched by 101 | /// `query` will be returned. 102 | Future> searchByName(String query, 103 | {bool includeNsfw = true, bool exact = false}) async { 104 | final params = {}; 105 | params['query'] = query; 106 | params['exact'] = exact.toString(); 107 | params['include_over_18'] = includeNsfw.toString(); 108 | return [ 109 | for (final s in (await reddit.post( 110 | apiPath['subreddits_name_search'], params, 111 | objectify: false))['names']) 112 | reddit.subreddit(s) 113 | ]; 114 | } 115 | 116 | /// Returns a [Stream] which is populated as new subreddits are created. 117 | Stream stream({int? limit, int? pauseAfter}) => 118 | streamGenerator(newest, itemLimit: limit, pauseAfter: pauseAfter); 119 | } 120 | -------------------------------------------------------------------------------- /lib/src/models/trophy.dart: -------------------------------------------------------------------------------- 1 | import 'package:draw/draw.dart'; 2 | import 'package:draw/src/base_impl.dart'; 3 | 4 | /// A Class representing an award or trophy 5 | class Trophy extends RedditBase with RedditBaseInitializedMixin { 6 | Trophy.parse(Reddit reddit, Map? data) : super(reddit) { 7 | setData(this, data); 8 | } 9 | 10 | /// The ID of the [Trophy] (Can be None). 11 | String? get awardId => data!['award_id']; 12 | 13 | /// The description of the [Trophy] (Can be None). 14 | String? get description => data!['description']; 15 | 16 | /// The URL of a 41x41 px icon for the [Trophy]. 17 | String? get icon_40 => data!['icon_40']; 18 | 19 | /// The URL of a 71x71 px icon for the [Trophy]. 20 | String? get icon_70 => data!['icon_70']; 21 | 22 | /// The name of the [Trophy]. 23 | String? get name => data!['name']; 24 | 25 | /// A relevant URL (Can be None). 26 | String? get url => data!['url']; 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/models/user_content.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'package:draw/src/base.dart'; 7 | import 'package:draw/src/reddit.dart'; 8 | 9 | /// An abstract base class for user created content, which is one of 10 | /// [Submission] or [Comment]. 11 | abstract class UserContent extends RedditBase { 12 | UserContent.withPath(Reddit reddit, String infoPath) 13 | : super.withPath(reddit, infoPath); 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/platform_utils.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2019, the Dart Reddit API Wrapper project authors. 2 | /// Please see the AUTHORS file for details. All rights reserved. 3 | /// Use of this source code is governed by a BSD-style license that 4 | /// can be found in the LICENSE file. 5 | 6 | export 'platform_utils_unsupported.dart' 7 | if (dart.library.io) 'platform_utils_io.dart'; 8 | -------------------------------------------------------------------------------- /lib/src/platform_utils_io.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2019, the Dart Reddit API Wrapper project authors. 2 | /// Please see the AUTHORS file for details. All rights reserved. 3 | /// Use of this source code is governed by a BSD-style license that 4 | /// can be found in the LICENSE file. 5 | 6 | import 'dart:io' as io; 7 | 8 | class Platform { 9 | static bool get isAndroid => io.Platform.isAndroid; 10 | static bool get isFuchsia => io.Platform.isFuchsia; 11 | static bool get isIOS => io.Platform.isIOS; 12 | static bool get isLinux => io.Platform.isLinux; 13 | static bool get isMacOS => io.Platform.isMacOS; 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/platform_utils_unsupported.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2019, the Dart Reddit API Wrapper project authors. 2 | /// Please see the AUTHORS file for details. All rights reserved. 3 | /// Use of this source code is governed by a BSD-style license that 4 | /// can be found in the LICENSE file. 5 | 6 | class Platform { 7 | static bool get isAndroid => false; 8 | static bool get isFuchsia => false; 9 | static bool get isIOS => false; 10 | static bool get isLinux => false; 11 | static bool get isMacOS => false; 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/user.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | 8 | import 'package:draw/src/api_paths.dart'; 9 | import 'package:draw/src/base.dart'; 10 | import 'package:draw/src/reddit.dart'; 11 | import 'package:draw/src/listing/listing_generator.dart'; 12 | import 'package:draw/src/models/multireddit.dart'; 13 | import 'package:draw/src/models/redditor.dart'; 14 | import 'package:draw/src/models/subreddit.dart'; 15 | 16 | /// The [User] class provides methods to access information about the currently 17 | /// authenticated user. 18 | class User extends RedditBase { 19 | User(Reddit reddit) : super(reddit); 20 | 21 | /// Returns a [Future>] of blocked Redditors. 22 | Future> blocked() async { 23 | return (await reddit.get(apiPath['blocked'])).cast(); 24 | } 25 | 26 | /// Returns a [Stream] of [Subreddit]s the currently authenticated user is a 27 | /// contributor of. 28 | /// 29 | /// [limit] is the number of [Subreddit]s to request, and [params] should 30 | /// contain any additional parameters that should be sent as part of the API 31 | /// request. 32 | Stream contributorSubreddits( 33 | {int limit = ListingGenerator.defaultRequestLimit, 34 | Map? params}) => 35 | ListingGenerator.generator(reddit, apiPath['my_contributor'], 36 | limit: limit, params: params); 37 | 38 | /// Returns a [Future>] of friends. 39 | Future> friends() async { 40 | return (await reddit.get(apiPath['friends'])).cast(); 41 | } 42 | 43 | /// Returns a [Future] mapping subreddits to karma earned on the given 44 | /// subreddit. 45 | Future>?> karma() async { 46 | return (await reddit.get(apiPath['karma'])) 47 | as Map>?; 48 | } 49 | 50 | // TODO(bkonyi): actually do something with [useCache]. 51 | /// Returns a [Future] which represents the current user. 52 | Future me({useCache = true}) async { 53 | return (await reddit.get(apiPath['me'])) as Redditor?; 54 | } 55 | 56 | /// Returns a [Stream] of [Subreddit]s the currently authenticated user is a 57 | /// moderator of. 58 | /// 59 | /// [limit] is the number of [Subreddit]s to request, and [params] should 60 | /// contain any additional parameters that should be sent as part of the API 61 | /// request. 62 | Stream moderatorSubreddits( 63 | {int limit = ListingGenerator.defaultRequestLimit, 64 | Map? params}) => 65 | ListingGenerator.generator(reddit, apiPath['my_moderator'], 66 | limit: limit, params: params); 67 | 68 | /// Returns a [Stream] of [Multireddit]s that belongs to the currently 69 | /// authenticated user. 70 | /// 71 | /// [limit] is the number of [Subreddit]s to request, and [params] should 72 | /// contain any additional parameters that should be sent as part of the API 73 | /// request. 74 | Future?> multireddits() async { 75 | return (await reddit.get(apiPath['my_multireddits'])).cast(); 76 | } 77 | 78 | /// Returns a [Stream] of [Subreddit]s the currently authenticated user is a 79 | /// subscriber of. 80 | /// 81 | /// [limit] is the number of [Subreddit]s to request, and [params] should 82 | /// contain any additional parameters that should be sent as part of the API 83 | /// request. If provided, `after` specifies from which point 84 | /// Reddit will return objects of the requested type. 85 | Stream subreddits( 86 | {int limit = ListingGenerator.defaultRequestLimit, 87 | String? after, 88 | Map? params}) => 89 | ListingGenerator.generator(reddit, apiPath['my_subreddits'], 90 | limit: limit, after: after, params: params); 91 | } 92 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: draw 2 | version: 1.1.0 3 | description: 'A fully-featured Reddit API wrapper for Dart, inspired by PRAW.' 4 | homepage: 'https://github.com/draw-dev/DRAW' 5 | 6 | environment: 7 | sdk: '>=2.12.0 <4.0.0' 8 | 9 | dependencies: 10 | collection: ^1.15.0 11 | color: '^3.0.0' 12 | http: '^0.13.0' 13 | http_parser: '^4.0.0' 14 | ini: '^2.1.0' 15 | logging: '^1.0.1' 16 | oauth2: '^2.0.0' 17 | path: '^1.4.2' 18 | quiver: '^3.0.0' 19 | 20 | dev_dependencies: 21 | mockito: '^5.0.0' 22 | pedantic: '1.11.0' 23 | reply: '^0.1.2-dev' 24 | test: '^1.16.0' 25 | 26 | # Use reply with null-safety fork in third_party folder, 27 | # to allow sound null-safety tests 28 | dependency_overrides: 29 | reply: 30 | path: third_party/reply 31 | -------------------------------------------------------------------------------- /test/auth/credentials.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:io'; 7 | 8 | final kApplicationClientID = Platform.environment['APP_CLIENT_ID']; 9 | final kScriptClientID = Platform.environment['SCRIPT_CLIENT_ID']; 10 | final kScriptClientSecret = Platform.environment['SCRIPT_CLIENT_SECRET']; 11 | final kWebClientID = Platform.environment['WEB_CLIENT_ID']; 12 | final kWebClientSecret = Platform.environment['WEB_CLIENT_SECRET']; 13 | const kUsername = 'DRAWApiOfficial'; 14 | final kPassword = Platform.environment['PASSWORD']; 15 | 16 | bool isScriptAuthConfigured = 17 | (kScriptClientID != null) && (kScriptClientSecret != null); 18 | 19 | bool isWebAuthConfigured = (kWebClientID != null) && (kWebClientSecret != null); 20 | -------------------------------------------------------------------------------- /test/auth/read_only.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | 8 | import 'package:draw/draw.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | import 'credentials.dart'; 12 | 13 | Future main() async { 14 | test('read-only', () async { 15 | final reddit = await Reddit.createReadOnlyInstance( 16 | clientId: kScriptClientID, 17 | clientSecret: kScriptClientSecret, 18 | userAgent: 'readonly-client'); 19 | expect(reddit.readOnly, isTrue); 20 | expect(await reddit.front.hot().first, isNotNull); 21 | }); 22 | 23 | test('read-only untrusted', () async { 24 | final reddit = await Reddit.createUntrustedReadOnlyInstance( 25 | clientId: kApplicationClientID, 26 | deviceId: 'DO_NOT_TRACK_THIS_DEVICE', 27 | userAgent: 'readonly-client'); 28 | expect(reddit.readOnly, isTrue); 29 | expect(await reddit.front.hot().first, isNotNull); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /test/auth/run_live_auth_tests.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'credentials.dart'; 7 | import 'read_only.dart' as read_only; 8 | import 'script_auth.dart' as script; 9 | import 'web_auth.dart' as web_auth; 10 | 11 | void main() { 12 | if (isScriptAuthConfigured) { 13 | script.main(); 14 | read_only.main(); 15 | } 16 | if (isWebAuthConfigured) { 17 | web_auth.main(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/auth/script_auth.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | 8 | import 'package:draw/draw.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | import 'credentials.dart'; 12 | 13 | Future main() async { 14 | test('script authenticator', () async { 15 | if (kScriptClientSecret == null) return; 16 | final reddit = await Reddit.createScriptInstance( 17 | clientId: kScriptClientID, 18 | clientSecret: kScriptClientSecret, 19 | userAgent: 'script-client-DRAW-live-testing', 20 | username: kUsername, 21 | password: kPassword); 22 | expect(reddit.readOnly, isFalse); 23 | expect(await reddit.user.me(), isNotNull); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /test/comment/continue_test_expected.out: -------------------------------------------------------------------------------- 1 | Testing 2 | 1 Needs more comments. 3 | 2 2 Still need more... 4 | 3 3 3 More... 5 | 4 4 4 4 More...... 6 | 5 5 5 5 5 How many more are needed here? 7 | 6 6 6 6 6 6 Deeper... 8 | 7 7 7 7 7 7 7 Deeper..... 9 | 8 8 8 8 8 8 8 8 Nest already! :( 10 | 9 9 9 9 9 9 9 9 9 :( :( 11 | 10 10 10 10 10 10 10 10 10 10 :( :( :( 12 | 11 11 11 11 11 11 11 11 11 11 11 ): ): ): 13 | 12 12 12 12 12 12 12 12 12 12 12 12 Surely this must nest now... 14 | 9 9 9 9 9 9 9 9 9 Come on Reddit, you can do it! 15 | -------------------------------------------------------------------------------- /test/comment/invalid_test.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/api/info/", 6 | "{id: t1_abc123}" 7 | ], 8 | "response": { 9 | "kind": "Listing", 10 | "data": { 11 | "modhash": null, 12 | "dist": 0, 13 | "children": [], 14 | "after": null, 15 | "before": null 16 | } 17 | } 18 | } 19 | ] -------------------------------------------------------------------------------- /test/draw.ini: -------------------------------------------------------------------------------- 1 | default=default 2 | reddit_url='https://www.reddit.com' 3 | client_id=Y4PJOclpDQy3xZ 4 | client_secret=UkGLTe6oqsMk5nHCJTHLrwgvHpr 5 | password=pni9ubeht4wd50gk 6 | username=fakebot1 7 | 8 | [section] 9 | password=different 10 | oauth_url=https://oauth.reddit.com 11 | 12 | [section1] 13 | password=pni9ubeht4wd50gk 14 | username=sectionbot 15 | 16 | [emptyTest] 17 | username= 18 | 19 | [testUpdateCheck1] 20 | check_for_updates=1 21 | 22 | [testUpdateCheckOn] 23 | check_for_updates=on 24 | 25 | [testUpdateCheckTrue] 26 | check_for_updates=true 27 | 28 | [testUpdateCheckYes] 29 | check_for_updates=yes 30 | 31 | [testUpdateCheckFalse] 32 | check_for_updates=false 33 | -------------------------------------------------------------------------------- /test/frontpage/frontpage_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | 8 | import 'package:test/test.dart'; 9 | import 'package:draw/draw.dart'; 10 | 11 | import '../test_utils.dart'; 12 | 13 | Future main() async { 14 | // TODO(bkonyi): this is more of a sanity check, but all the functionality in 15 | // [FrontPage] should have been tested elsewhere. Might want to expand this 16 | // coverage anyway. 17 | test('lib/frontpage/sanity', () async { 18 | final reddit = await createRedditTestInstance( 19 | 'test/frontpage/lib_frontpage_sanity.json'); 20 | await for (final hot in reddit.front.hot(params: {'limit': '10'})) { 21 | expect(hot is Submission, isTrue); 22 | } 23 | }); 24 | 25 | test('lib/frontpage/best', () async { 26 | final reddit = await createRedditTestInstance( 27 | 'test/frontpage/lib_frontpage_best.json'); 28 | await for (final hot in reddit.front.best(params: {'limit': '10'})) { 29 | expect(hot is Submission, isTrue); 30 | } 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /test/gilded_listing_mixin/gilded_listing_mixin_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | 8 | import 'package:test/test.dart'; 9 | import 'package:draw/draw.dart'; 10 | 11 | import '../test_utils.dart'; 12 | 13 | Future main() async { 14 | test('lib/gilded_listing_mixin/front_page_gilded', () async { 15 | final reddit = await createRedditTestInstance( 16 | 'test/gilded_listing_mixin/lib_gilded_listing_mixin_frontpage.json'); 17 | await for (final content in reddit.front.gilded(params: {'limit': '10'})) { 18 | expect(content is UserContentInitialized, isTrue); 19 | expect(content.silver >= 0, isTrue); 20 | expect(content.gold >= 0, isTrue); 21 | expect(content.platinum >= 0, isTrue); 22 | } 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /test/images/10by3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/draw-dev/DRAW/d2be045b3a60ccc3ad395d2bdc03e256f0119c4f/test/images/10by3.jpg -------------------------------------------------------------------------------- /test/images/256.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/draw-dev/DRAW/d2be045b3a60ccc3ad395d2bdc03e256f0119c4f/test/images/256.jpg -------------------------------------------------------------------------------- /test/images/bad.jpg: -------------------------------------------------------------------------------- 1 | a -------------------------------------------------------------------------------- /test/images/dart_header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/draw-dev/DRAW/d2be045b3a60ccc3ad395d2bdc03e256f0119c4f/test/images/dart_header.png -------------------------------------------------------------------------------- /test/inbox/lib_inbox_comment_replies.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/message/comments/", 6 | "{limit: 100}" 7 | ], 8 | "response": { 9 | "kind": "Listing", 10 | "data": { 11 | "after": null, 12 | "dist": 1, 13 | "modhash": null, 14 | "whitelist_status": "all_ads", 15 | "children": [ 16 | { 17 | "kind": "t1", 18 | "data": { 19 | "first_message": null, 20 | "first_message_name": null, 21 | "subreddit": "drawapitesting", 22 | "likes": null, 23 | "replies": "", 24 | "id": "du3wqft", 25 | "subject": "comment reply", 26 | "was_comment": true, 27 | "score": 1, 28 | "author": "XtremeCheese", 29 | "num_comments": 2, 30 | "parent_id": "t1_dpn1bjc", 31 | "subreddit_name_prefixed": "r/drawapitesting", 32 | "new": true, 33 | "body": "Testing reply inbox", 34 | "link_title": "Testing", 35 | "dest": "DRAWApiOfficial", 36 | "body_html": "<!-- SC_OFF --><div class=\"md\"><p>Testing reply inbox</p>\n</div><!-- SC_ON -->", 37 | "name": "t1_du3wqft", 38 | "created": 1518429962.0, 39 | "created_utc": 1518401162.0, 40 | "context": "/r/drawapitesting/comments/7c480b/testing/du3wqft/?context=3", 41 | "distinguished": null 42 | } 43 | } 44 | ], 45 | "before": null 46 | } 47 | } 48 | } 49 | ] -------------------------------------------------------------------------------- /test/inbox/lib_inbox_mark_read_unread.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/message/unread/", 6 | "{mark: false, limit: 100}" 7 | ], 8 | "response": { 9 | "kind": "Listing", 10 | "data": { 11 | "after": null, 12 | "dist": 1, 13 | "modhash": null, 14 | "whitelist_status": "all_ads", 15 | "children": [ 16 | { 17 | "kind": "t1", 18 | "data": { 19 | "first_message": null, 20 | "first_message_name": null, 21 | "subreddit": "drawapitesting", 22 | "likes": null, 23 | "replies": "", 24 | "id": "du5um0y", 25 | "subject": "post reply", 26 | "was_comment": true, 27 | "score": 1, 28 | "author": "XtremeCheese", 29 | "num_comments": 1, 30 | "parent_id": "t3_7x6ew7", 31 | "subreddit_name_prefixed": "r/drawapitesting", 32 | "new": true, 33 | "body": "Great talk!", 34 | "link_title": "DRAW: Using Dart to Moderate Reddit Comments (DartConf 2018)", 35 | "dest": "DRAWApiOfficial", 36 | "body_html": "<!-- SC_OFF --><div class=\"md\"><p>Great talk!</p>\n</div><!-- SC_ON -->", 37 | "name": "t1_du5um0y", 38 | "created": 1518519909.0, 39 | "created_utc": 1518491109.0, 40 | "context": "/r/drawapitesting/comments/7x6ew7/draw_using_dart_to_moderate_reddit_comments/du5um0y/?context=3", 41 | "distinguished": null 42 | } 43 | } 44 | ], 45 | "before": null 46 | } 47 | } 48 | }, 49 | { 50 | "always": false, 51 | "request": [ 52 | "https://oauth.reddit.com/api/read_message/", 53 | "{id: t1_du5um0y}" 54 | ], 55 | "response": {} 56 | }, 57 | { 58 | "always": false, 59 | "request": [ 60 | "https://oauth.reddit.com/message/unread/", 61 | "{mark: false, limit: 100}" 62 | ], 63 | "response": { 64 | "kind": "Listing", 65 | "data": { 66 | "after": null, 67 | "dist": 0, 68 | "modhash": null, 69 | "whitelist_status": "all_ads", 70 | "children": [], 71 | "before": null 72 | } 73 | } 74 | }, 75 | { 76 | "always": false, 77 | "request": [ 78 | "https://oauth.reddit.com/api/unread_message/", 79 | "{id: t1_du5um0y}" 80 | ], 81 | "response": {} 82 | }, 83 | { 84 | "always": false, 85 | "request": [ 86 | "https://oauth.reddit.com/message/unread/", 87 | "{mark: false, limit: 100}" 88 | ], 89 | "response": { 90 | "kind": "Listing", 91 | "data": { 92 | "after": null, 93 | "dist": 1, 94 | "modhash": null, 95 | "whitelist_status": "all_ads", 96 | "children": [ 97 | { 98 | "kind": "t1", 99 | "data": { 100 | "first_message": null, 101 | "first_message_name": null, 102 | "subreddit": "drawapitesting", 103 | "likes": null, 104 | "replies": "", 105 | "id": "du5um0y", 106 | "subject": "post reply", 107 | "was_comment": true, 108 | "score": 1, 109 | "author": "XtremeCheese", 110 | "num_comments": 1, 111 | "parent_id": "t3_7x6ew7", 112 | "subreddit_name_prefixed": "r/drawapitesting", 113 | "new": true, 114 | "body": "Great talk!", 115 | "link_title": "DRAW: Using Dart to Moderate Reddit Comments (DartConf 2018)", 116 | "dest": "DRAWApiOfficial", 117 | "body_html": "<!-- SC_OFF --><div class=\"md\"><p>Great talk!</p>\n</div><!-- SC_ON -->", 118 | "name": "t1_du5um0y", 119 | "created": 1518519909.0, 120 | "created_utc": 1518491109.0, 121 | "context": "/r/drawapitesting/comments/7x6ew7/draw_using_dart_to_moderate_reddit_comments/du5um0y/?context=3", 122 | "distinguished": null 123 | } 124 | } 125 | ], 126 | "before": null 127 | } 128 | } 129 | } 130 | ] -------------------------------------------------------------------------------- /test/inbox/lib_inbox_mentions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/message/mentions", 6 | "{limit: 100}" 7 | ], 8 | "response": { 9 | "kind": "Listing", 10 | "data": { 11 | "after": null, 12 | "dist": 1, 13 | "modhash": null, 14 | "whitelist_status": "all_ads", 15 | "children": [ 16 | { 17 | "kind": "t1", 18 | "data": { 19 | "first_message": null, 20 | "first_message_name": null, 21 | "subreddit": "drawapitesting", 22 | "likes": null, 23 | "replies": "", 24 | "id": "du3xc17", 25 | "subject": "username mention", 26 | "was_comment": true, 27 | "score": 1, 28 | "author": "XtremeCheese", 29 | "num_comments": 3, 30 | "parent_id": "t1_du3wqft", 31 | "subreddit_name_prefixed": "r/drawapitesting", 32 | "new": true, 33 | "body": "/u/DRAWApiOfficial mention test", 34 | "link_title": "Testing", 35 | "dest": "DRAWApiOfficial", 36 | "body_html": "<!-- SC_OFF --><div class=\"md\"><p><a href=\"/u/DRAWApiOfficial\">/u/DRAWApiOfficial</a> mention test</p>\n</div><!-- SC_ON -->", 37 | "name": "t1_du3xc17", 38 | "created": 1518430696.0, 39 | "created_utc": 1518401896.0, 40 | "context": "/r/drawapitesting/comments/7c480b/testing/du3xc17/?context=3", 41 | "distinguished": null 42 | } 43 | } 44 | ], 45 | "before": null 46 | } 47 | } 48 | } 49 | ] -------------------------------------------------------------------------------- /test/inbox/lib_inbox_submission_replies.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/message/selfreply/", 6 | "{limit: 100}" 7 | ], 8 | "response": { 9 | "kind": "Listing", 10 | "data": { 11 | "after": null, 12 | "dist": 1, 13 | "modhash": null, 14 | "whitelist_status": "all_ads", 15 | "children": [ 16 | { 17 | "kind": "t1", 18 | "data": { 19 | "first_message": null, 20 | "first_message_name": null, 21 | "subreddit": "drawapitesting", 22 | "likes": null, 23 | "replies": "", 24 | "id": "du5um0y", 25 | "subject": "post reply", 26 | "was_comment": true, 27 | "score": 1, 28 | "author": "XtremeCheese", 29 | "num_comments": 1, 30 | "parent_id": "t3_7x6ew7", 31 | "subreddit_name_prefixed": "r/drawapitesting", 32 | "new": true, 33 | "body": "Great talk!", 34 | "link_title": "DRAW: Using Dart to Moderate Reddit Comments (DartConf 2018)", 35 | "dest": "DRAWApiOfficial", 36 | "body_html": "<!-- SC_OFF --><div class=\"md\"><p>Great talk!</p>\n</div><!-- SC_ON -->", 37 | "name": "t1_du5um0y", 38 | "created": 1518519909.0, 39 | "created_utc": 1518491109.0, 40 | "context": "/r/drawapitesting/comments/7x6ew7/draw_using_dart_to_moderate_reddit_comments/du5um0y/?context=3", 41 | "distinguished": null 42 | } 43 | } 44 | ], 45 | "before": null 46 | } 47 | } 48 | } 49 | ] -------------------------------------------------------------------------------- /test/inbox/lib_inbox_unread.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/message/unread/", 6 | "{mark: false, limit: 100}" 7 | ], 8 | "response": { 9 | "kind": "Listing", 10 | "data": { 11 | "after": null, 12 | "dist": 1, 13 | "modhash": null, 14 | "whitelist_status": "all_ads", 15 | "children": [ 16 | { 17 | "kind": "t1", 18 | "data": { 19 | "first_message": null, 20 | "first_message_name": null, 21 | "subreddit": "drawapitesting", 22 | "likes": null, 23 | "replies": "", 24 | "id": "du5um0y", 25 | "subject": "post reply", 26 | "was_comment": true, 27 | "score": 1, 28 | "author": "XtremeCheese", 29 | "num_comments": 1, 30 | "parent_id": "t3_7x6ew7", 31 | "subreddit_name_prefixed": "r/drawapitesting", 32 | "new": true, 33 | "body": "Great talk!", 34 | "link_title": "DRAW: Using Dart to Moderate Reddit Comments (DartConf 2018)", 35 | "dest": "DRAWApiOfficial", 36 | "body_html": "<!-- SC_OFF --><div class=\"md\"><p>Great talk!</p>\n</div><!-- SC_ON -->", 37 | "name": "t1_du5um0y", 38 | "created": 1518519909.0, 39 | "created_utc": 1518491109.0, 40 | "context": "/r/drawapitesting/comments/7x6ew7/draw_using_dart_to_moderate_reddit_comments/du5um0y/?context=3", 41 | "distinguished": null 42 | } 43 | } 44 | ], 45 | "before": null 46 | } 47 | } 48 | } 49 | ] -------------------------------------------------------------------------------- /test/messageable_mixin/basic_message.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/api/compose/", 6 | "{subject: Test message, text: Hello XtremeCheese!, to: XtremeCheese, api_type: json}" 7 | ], 8 | "response": { 9 | "json": { 10 | "errors": [] 11 | } 12 | } 13 | } 14 | ] -------------------------------------------------------------------------------- /test/messageable_mixin/invalid_redditor.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/api/compose/", 6 | "{subject: Test message, text: Hello Toxicity-Moderator!, to: Toxicity-Moderator2, api_type: json, from_sr: drawapitesting}" 7 | ], 8 | "response": { 9 | "json": { 10 | "errors": [ 11 | [ 12 | "USER_DOESNT_EXIST", 13 | "that user doesn't exist", 14 | "to" 15 | ] 16 | ] 17 | } 18 | } 19 | } 20 | ] -------------------------------------------------------------------------------- /test/messageable_mixin/invalid_subreddit.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/api/compose/", 6 | "{subject: Test message, text: Hello Toxicity-Moderator!, to: Toxicity-Moderator, api_type: json, from_sr: drawapitesting2}" 7 | ], 8 | "response": { 9 | "json": { 10 | "errors": [ 11 | [ 12 | "SUBREDDIT_NOEXIST", 13 | "that subreddit doesn't exist", 14 | "from_sr" 15 | ] 16 | ] 17 | } 18 | } 19 | } 20 | ] -------------------------------------------------------------------------------- /test/messageable_mixin/messageable_mixin_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | 8 | import 'package:draw/draw.dart'; 9 | 10 | import 'package:test/test.dart'; 11 | import '../test_utils.dart'; 12 | 13 | Future main() async { 14 | // TODO(bkonyi): rewrite these to actually check that the messages were 15 | // received. Manually confirmed for now. 16 | test('lib/messageable_mixin/basic_message', () async { 17 | final reddit = await createRedditTestInstance( 18 | 'test/messageable_mixin/basic_message.json'); 19 | final receiver = reddit.redditor('XtremeCheese'); 20 | await receiver.message('Test message', 'Hello XtremeCheese!'); 21 | }); 22 | 23 | test('lib/messageable_mixin/subreddit_message', () async { 24 | final reddit = await createRedditTestInstance( 25 | 'test/messageable_mixin/subreddit_message.json'); 26 | final receiver = reddit.redditor('Toxicity-Moderator'); 27 | final subreddit = reddit.subreddit('drawapitesting'); 28 | await receiver.message('Test message', 'Hello Toxicity-Moderator!', 29 | fromSubreddit: subreddit); 30 | }); 31 | 32 | test('lib/messageable_mixin/invalid_subreddit', () async { 33 | final reddit = await createRedditTestInstance( 34 | 'test/messageable_mixin/invalid_subreddit.json'); 35 | final receiver = reddit.redditor('Toxicity-Moderator'); 36 | final subreddit = reddit.subreddit('drawapitesting2'); 37 | await expectLater( 38 | () async => await receiver.message( 39 | 'Test message', 'Hello Toxicity-Moderator!', 40 | fromSubreddit: subreddit), 41 | throwsA(TypeMatcher())); 42 | }); 43 | 44 | test('lib/messageable_mixin/invalid_redditor', () async { 45 | final reddit = await createRedditTestInstance( 46 | 'test/messageable_mixin/invalid_redditor.json'); 47 | final receiver = reddit.redditor('Toxicity-Moderator2'); 48 | final subreddit = reddit.subreddit('drawapitesting'); 49 | await expectLater( 50 | () async => await receiver.message( 51 | 'Test message', 'Hello Toxicity-Moderator!', 52 | fromSubreddit: subreddit), 53 | throwsA(TypeMatcher())); 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /test/messageable_mixin/subreddit_message.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/api/compose/", 6 | "{subject: Test message, text: Hello Toxicity-Moderator!, to: Toxicity-Moderator, api_type: json, from_sr: drawapitesting}" 7 | ], 8 | "response": { 9 | "json": { 10 | "errors": [] 11 | } 12 | } 13 | } 14 | ] -------------------------------------------------------------------------------- /test/multireddit/multireddit_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | import 'package:color/color.dart'; 8 | import 'package:draw/draw.dart'; 9 | import 'package:test/test.dart'; 10 | import '../test_utils.dart'; 11 | 12 | Future main() async { 13 | final expectedSubredditList = [ 14 | {'name': 'solotravel'}, 15 | {'name': 'Fishing'}, 16 | {'name': 'geocaching'}, 17 | {'name': 'Yosemite'}, 18 | {'name': 'CampingandHiking'}, 19 | {'name': 'whitewater'}, 20 | {'name': 'remoteplaces'}, 21 | {'name': 'Adirondacks'}, 22 | {'name': 'caving'}, 23 | {'name': 'travelphotos'}, 24 | {'name': 'canyoneering'}, 25 | {'name': 'Backcountry'}, 26 | {'name': 'Bushcraft'}, 27 | {'name': 'Kayaking'}, 28 | {'name': 'scuba'}, 29 | {'name': 'bouldering'}, 30 | {'name': 'CampingGear'}, 31 | {'name': 'urbanexploration'}, 32 | {'name': 'WWOOF'}, 33 | {'name': 'adventures'}, 34 | {'name': 'climbing'}, 35 | {'name': 'Mountaineering'}, 36 | {'name': 'sailing'}, 37 | {'name': 'hiking'}, 38 | {'name': 'iceclimbing'}, 39 | {'name': 'tradclimbing'}, 40 | {'name': 'backpacking'}, 41 | {'name': 'trailmeals'}, 42 | {'name': 'Climbingvids'}, 43 | {'name': 'camping'}, 44 | {'name': 'climbharder'}, 45 | {'name': 'skiing'}, 46 | {'name': 'Outdoors'}, 47 | {'name': 'alpinism'}, 48 | {'name': 'PacificCrestTrail'}, 49 | {'name': 'Ultralight'}, 50 | {'name': 'Hammocks'}, 51 | {'name': 'AppalachianTrail'} 52 | ]; 53 | 54 | test('lib/multireddit/copy_parse_constructor_basic', () async { 55 | final reddit = await createRedditTestInstance( 56 | 'test/multireddit/lib_multireddit_copy.json'); 57 | final data = { 58 | 'kind': 'LabeledMulti', 59 | 'data': { 60 | 'can_edit': false, 61 | 'display_name': 'adventure', 62 | 'name': 'adventure', 63 | 'subreddits': expectedSubredditList, 64 | 'path': '/user/MyFifthOne/m/adventure/', 65 | } 66 | }; 67 | final multireddit = Multireddit.parse(reddit, data); 68 | final newMulti = await multireddit.copy('test-copy-adventure-2'); 69 | expect(newMulti.displayName, 'test_copy_adventure_2'); 70 | expect(newMulti.subreddits, multireddit.subreddits); 71 | }); 72 | 73 | test('lib/multireddit/copy_without_parsing_basic', () async { 74 | final newMultiName = 'CopyOfMultireddit'; 75 | final newMultiNameSlug = 'copyofmultireddit'; 76 | final reddit = await createRedditTestInstance( 77 | 'test/multireddit/lib_multireddit_copy_2.json'); 78 | final oldMulti = (await reddit.user.multireddits())![1]; 79 | final newMulti = await oldMulti.copy(newMultiName); 80 | expect(newMulti.displayName, newMultiNameSlug); 81 | expect(newMulti.subreddits, oldMulti.subreddits); 82 | }); 83 | 84 | test('lib/multireddit/multis_from_user_non_trival', () async { 85 | final reddit = await createRedditTestInstance( 86 | 'test/multireddit/lib_user_multireddits.json'); 87 | final multis = (await reddit.user.multireddits()) as List; 88 | expect(multis.length, 9); 89 | final multi = multis[1]; 90 | 91 | // Testing using data variable. 92 | expect(multi.data!['name'], 'drawtestingmulti'); 93 | expect(multi.data!['display_name'], 'drawtestingmulti'); 94 | expect(multi.data!['can_edit'], isTrue); 95 | expect(multi.data!['subreddits'].length, 81); 96 | 97 | // Testing using getters. 98 | expect(multi.author.displayName, (await reddit.user.me())!.displayName); 99 | expect(multi.over18, isFalse); 100 | expect(multi.keyColor, HexColor('#cee3f8')); 101 | expect(multi.visibility, Visibility.public); 102 | expect(multi.weightingScheme, WeightingScheme.classic); 103 | expect(multi.iconName, isNull); 104 | expect(multi.displayName, 'drawtestingmulti'); 105 | expect(multi.fullname, 'drawtestingmulti'); 106 | expect(multi.canEdit, isTrue); 107 | expect(multi.subreddits.length, 81); 108 | }); 109 | 110 | test('lib/multireddit/delete_multi_basic', () async { 111 | final reddit = await createRedditTestInstance( 112 | 'test/multireddit/lib_multireddit_delete.json', 113 | ); 114 | final multis = (await reddit.user.multireddits()) as List; 115 | expect(multis.length, 5); 116 | await multis[1].delete(); 117 | final newMultis = (await reddit.user.multireddits()) as List; 118 | expect(newMultis.length, 4); 119 | }); 120 | 121 | test('lib/multireddit/add_subreddit_basic', () async { 122 | final reddit = await createRedditTestInstance( 123 | 'test/multireddit/lib_multireddit_add_subreddit.json', 124 | ); 125 | final multis = (await reddit.user.multireddits()) as List; 126 | final multi = multis[1]; 127 | await multi.add('camping'); 128 | }); 129 | } 130 | -------------------------------------------------------------------------------- /test/redditor/lib_reddit_gild.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/user/XtremeCheese/about/", 6 | "null" 7 | ], 8 | "response": { 9 | "kind": "t2", 10 | "data": { 11 | "is_employee": false, 12 | "icon_img": "https://www.redditstatic.com/avatars/avatar_default_01_FFD635.png", 13 | "pref_show_snoovatar": false, 14 | "name": "XtremeCheese", 15 | "is_friend": false, 16 | "created": 1324019663.0, 17 | "has_subscribed": true, 18 | "hide_from_robots": true, 19 | "created_utc": 1323990863.0, 20 | "link_karma": 5155, 21 | "comment_karma": 1892, 22 | "is_gold": true, 23 | "is_mod": true, 24 | "verified": false, 25 | "subreddit": null, 26 | "has_verified_email": true, 27 | "id": "6g5vc" 28 | } 29 | } 30 | }, 31 | { 32 | "always": false, 33 | "request": [ 34 | "https://oauth.reddit.com/api/v1/gold/give/XtremeCheese/", 35 | "{username: XtremeCheese, months: 1}" 36 | ], 37 | "response": "" 38 | } 39 | ] 40 | -------------------------------------------------------------------------------- /test/redditor/lib_redditor_bad_friend.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/api/v1/me/friends/drawapiofficial2", 6 | "{note: }" 7 | ], 8 | "response": [ 9 | "DRAWNotFoundException", 10 | "USER_DOESNT_EXIST", 11 | "Bad Request" 12 | ] 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /test/redditor/lib_redditor_bad_friend_info.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/api/v1/me/friends/drawapiofficial2", 6 | "null" 7 | ], 8 | "response": [ 9 | "DRAWNotFoundException", 10 | "USER_DOESNT_EXIST", 11 | "Bad Request" 12 | ] 13 | } 14 | ] -------------------------------------------------------------------------------- /test/redditor/lib_redditor_bad_gild.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/api/v1/gold/give/drawapiofficial2/", 6 | "{username: drawapiofficial2, months: 1}" 7 | ], 8 | "response": [ 9 | "DRAWNotFoundException", 10 | "USER_DOESNT_EXIST", 11 | "Bad Request" 12 | ] 13 | } 14 | ] -------------------------------------------------------------------------------- /test/redditor/lib_redditor_bad_unfriend.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/api/v1/me/friends/drawapiofficial2", 6 | "null" 7 | ], 8 | "response": [ 9 | "DRAWNotFoundException", 10 | "USER_DOESNT_EXIST", 11 | "Bad Request" 12 | ] 13 | } 14 | ] -------------------------------------------------------------------------------- /test/redditor/lib_redditor_friend.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/api/v1/me/friends/XtremeCheese", 6 | "{note: My best friend!}" 7 | ], 8 | "response": { 9 | "date": 1503502054.0, 10 | "note": "My best friend!", 11 | "name": "XtremeCheese", 12 | "id": "t2_6g5vc" 13 | } 14 | }, 15 | { 16 | "always": false, 17 | "request": [ 18 | "https://oauth.reddit.com/api/v1/me/friends/", 19 | "null" 20 | ], 21 | "response": { 22 | "kind": "UserList", 23 | "data": { 24 | "children": [ 25 | { 26 | "date": 1503502054.0, 27 | "note": "My best friend!", 28 | "name": "XtremeCheese", 29 | "id": "t2_6g5vc" 30 | } 31 | ] 32 | } 33 | } 34 | }, 35 | { 36 | "always": false, 37 | "request": [ 38 | "https://oauth.reddit.com/user/XtremeCheese/about/", 39 | "null" 40 | ], 41 | "response": { 42 | "kind": "t2", 43 | "data": { 44 | "is_employee": false, 45 | "verified": false, 46 | "name": "XtremeCheese", 47 | "is_friend": true, 48 | "created": 1324019663.0, 49 | "has_subscribed": true, 50 | "hide_from_robots": true, 51 | "created_utc": 1323990863.0, 52 | "subreddit": null, 53 | "comment_karma": 1793, 54 | "is_gold": false, 55 | "is_mod": true, 56 | "pref_show_snoovatar": false, 57 | "link_karma": 4772, 58 | "has_verified_email": true, 59 | "id": "6g5vc" 60 | } 61 | } 62 | }, 63 | { 64 | "always": false, 65 | "request": [ 66 | "https://oauth.reddit.com/api/v1/me/friends/XtremeCheese", 67 | "null" 68 | ], 69 | "response": "" 70 | }, 71 | { 72 | "always": false, 73 | "request": [ 74 | "https://oauth.reddit.com/api/v1/me/friends/", 75 | "null" 76 | ], 77 | "response": { 78 | "kind": "UserList", 79 | "data": { 80 | "children": [] 81 | } 82 | } 83 | } 84 | ] 85 | -------------------------------------------------------------------------------- /test/redditor/lib_redditor_gildings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/user/DRAWApiOfficial/gilded/given", 6 | "{limit: 2}" 7 | ], 8 | "response": { 9 | "kind": "Listing", 10 | "data": { 11 | "modhash": null, 12 | "dist": 1, 13 | "children": [ 14 | { 15 | "kind": "t3", 16 | "data": { 17 | "approved_at_utc": null, 18 | "subreddit": "drawapitesting", 19 | "selftext": "", 20 | "user_reports": [], 21 | "saved": false, 22 | "mod_reason_title": null, 23 | "gilded": 1, 24 | "clicked": false, 25 | "title": "Gilded post!", 26 | "link_flair_richtext": [], 27 | "subreddit_name_prefixed": "r/drawapitesting", 28 | "hidden": false, 29 | "pwls": null, 30 | "link_flair_css_class": null, 31 | "downs": 0, 32 | "thumbnail_height": null, 33 | "parent_whitelist_status": null, 34 | "hide_score": false, 35 | "name": "t3_8j4lbd", 36 | "quarantine": false, 37 | "link_flair_text_color": "dark", 38 | "ignore_reports": false, 39 | "subreddit_type": "restricted", 40 | "ups": 1, 41 | "domain": "self.drawapitesting", 42 | "media_embed": {}, 43 | "thumbnail_width": null, 44 | "author_flair_template_id": null, 45 | "is_original_content": false, 46 | "secure_media": null, 47 | "is_reddit_media_domain": false, 48 | "category": null, 49 | "secure_media_embed": {}, 50 | "link_flair_text": null, 51 | "can_mod_post": true, 52 | "score": 1, 53 | "approved_by": null, 54 | "author_flair_background_color": null, 55 | "thumbnail": "self", 56 | "edited": false, 57 | "author_flair_css_class": null, 58 | "author_flair_richtext": [], 59 | "is_self": true, 60 | "mod_note": null, 61 | "created": 1526254602.0, 62 | "link_flair_type": "text", 63 | "wls": null, 64 | "post_categories": null, 65 | "banned_by": null, 66 | "author_flair_type": "text", 67 | "contest_mode": false, 68 | "selftext_html": null, 69 | "likes": null, 70 | "suggested_sort": null, 71 | "banned_at_utc": null, 72 | "view_count": 1, 73 | "archived": false, 74 | "no_follow": true, 75 | "spam": false, 76 | "is_crosspostable": true, 77 | "pinned": false, 78 | "over_18": false, 79 | "can_gild": false, 80 | "removed": false, 81 | "spoiler": false, 82 | "locked": false, 83 | "author_flair_text": null, 84 | "rte_mode": "markdown", 85 | "visited": false, 86 | "num_reports": 0, 87 | "distinguished": null, 88 | "subreddit_id": "t5_3mqw1", 89 | "mod_reason_by": null, 90 | "removal_reason": null, 91 | "id": "8j4lbd", 92 | "report_reasons": [], 93 | "author": "XtremeCheese", 94 | "num_crossposts": 0, 95 | "num_comments": 0, 96 | "send_replies": true, 97 | "mod_reports": [], 98 | "approved": false, 99 | "author_flair_text_color": null, 100 | "permalink": "/r/drawapitesting/comments/8j4lbd/gilded_post/", 101 | "whitelist_status": null, 102 | "stickied": false, 103 | "url": "https://www.reddit.com/r/drawapitesting/comments/8j4lbd/gilded_post/", 104 | "subreddit_subscribers": 2, 105 | "created_utc": 1526225802.0, 106 | "media": null, 107 | "is_video": false 108 | } 109 | } 110 | ], 111 | "after": null, 112 | "before": null 113 | } 114 | } 115 | } 116 | ] -------------------------------------------------------------------------------- /test/redditor/lib_redditor_invalid.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/user/drawapiofficial2/about/", 6 | "null" 7 | ], 8 | "response": [ 9 | "DRAWNotFoundException", 10 | null, 11 | "Not Found" 12 | ] 13 | } 14 | ] -------------------------------------------------------------------------------- /test/redditor/lib_redditor_multireddits_bad_redditor.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/api/multi/user/drawapiofficial2/", 6 | "null" 7 | ], 8 | "response": [ 9 | "DRAWNotFoundException", 10 | "USER_DOESNT_EXIST", 11 | "Bad Request" 12 | ] 13 | } 14 | ] -------------------------------------------------------------------------------- /test/rising_listing_mixin/rising_listing_mixin_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | 8 | import 'package:test/test.dart'; 9 | import 'package:draw/draw.dart'; 10 | 11 | import '../test_utils.dart'; 12 | 13 | Future main() async { 14 | test('lib/rising_listing_mixin/random_rising_sanity', () async { 15 | final reddit = await createRedditTestInstance( 16 | 'test/rising_listing_mixin/lib_rising_listing_mixin_random_rising.json'); 17 | await for (final rand 18 | in reddit.front.randomRising(params: {'limit': '10'})) { 19 | expect(rand is Submission, isTrue); 20 | } 21 | }); 22 | 23 | test('lib/rising_listing_mixin/rising_sanity', () async { 24 | final reddit = await createRedditTestInstance( 25 | 'test/rising_listing_mixin/lib_rising_listing_mixin_rising.json'); 26 | await for (final rise in reddit.front.rising(params: {'limit': '10'})) { 27 | expect(rise is Submission, isTrue); 28 | } 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /test/submission/lib_submission_invalid.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/comments/abcdef/", 6 | "null" 7 | ], 8 | "response": [ 9 | "DRAWNotFoundException", 10 | null, 11 | "Not Found" 12 | ] 13 | } 14 | ] -------------------------------------------------------------------------------- /test/subreddit/lib_subreddit_flair_call.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/r/drawapitesting/api/flairlist/", 6 | "{limit: 100}" 7 | ], 8 | "response": { 9 | "users": [ 10 | { 11 | "flair_css_class": "", 12 | "user": "DRAWApiOfficial", 13 | "flair_text": "Test Flair" 14 | } 15 | ] 16 | } 17 | } 18 | ] -------------------------------------------------------------------------------- /test/subreddit/lib_subreddit_flair_configure.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/r/drawapitesting/about/", 6 | "null" 7 | ], 8 | "response": { 9 | "kind": "t5", 10 | "data": { 11 | "notification_level": "low", 12 | "user_flair_background_color": "#ea0027", 13 | "wls": null, 14 | "banner_img": "", 15 | "user_sr_theme_enabled": true, 16 | "user_sr_flair_enabled": true, 17 | "user_flair_text": "Test Flair", 18 | "submit_text_html": null, 19 | "user_flair_css_class": "", 20 | "user_flair_template_id": "3efba730-8c63-11e8-8e62-0e430fa0e508", 21 | "user_is_banned": false, 22 | "subreddit_type": "restricted", 23 | "community_icon": "", 24 | "banner_background_image": "", 25 | "header_title": null, 26 | "wiki_enabled": true, 27 | "over18": false, 28 | "show_media": true, 29 | "banner_background_color": "", 30 | "description": "", 31 | "user_is_muted": false, 32 | "user_flair_type": "text", 33 | "user_can_flair_in_sr": true, 34 | "display_name": "drawapitesting", 35 | "header_img": null, 36 | "description_html": null, 37 | "title": "DRAW API Testing", 38 | "collapse_deleted_comments": true, 39 | "user_has_favorited": false, 40 | "emojis_custom_size": null, 41 | "id": "3mqw1", 42 | "emojis_enabled": false, 43 | "public_description_html": "<!-- SC_OFF --><div class=\"md\"><p>A subreddit used for testing the DRAW API: <a href=\"https://github.com/draw-dev/DRAW\">https://github.com/draw-dev/DRAW</a></p>\n</div><!-- SC_ON -->", 44 | "can_assign_user_flair": true, 45 | "allow_videos": true, 46 | "spoilers_enabled": true, 47 | "icon_size": null, 48 | "primary_color": "", 49 | "user_is_contributor": true, 50 | "audience_target": "", 51 | "suggested_comment_sort": null, 52 | "active_user_count": 6, 53 | "icon_img": "", 54 | "original_content_tag_enabled": false, 55 | "display_name_prefixed": "r/drawapitesting", 56 | "can_assign_link_flair": true, 57 | "submit_text": "", 58 | "allow_videogifs": true, 59 | "user_flair_text_color": "light", 60 | "accounts_active": 6, 61 | "public_traffic": false, 62 | "header_size": null, 63 | "subscribers": 2, 64 | "user_flair_position": "right", 65 | "submit_text_label": "null", 66 | "key_color": "#24a0ed", 67 | "link_flair_position": "right", 68 | "user_flair_richtext": [], 69 | "all_original_content": false, 70 | "lang": "en", 71 | "has_menu_widget": false, 72 | "is_enrolled_in_new_modmail": true, 73 | "whitelist_status": null, 74 | "name": "t5_3mqw1", 75 | "user_flair_enabled_in_sr": true, 76 | "created": 1500005383.0, 77 | "url": "/r/drawapitesting/", 78 | "quarantine": false, 79 | "hide_ads": false, 80 | "created_utc": 1499976583.0, 81 | "banner_size": null, 82 | "user_is_moderator": true, 83 | "submit_link_label": "null", 84 | "allow_discovery": true, 85 | "accounts_active_is_fuzzed": false, 86 | "advertiser_category": null, 87 | "public_description": "A subreddit used for testing the DRAW API: https://github.com/draw-dev/DRAW", 88 | "link_flair_enabled": true, 89 | "allow_images": true, 90 | "videostream_links_count": 0, 91 | "comment_score_hide_mins": 0, 92 | "show_media_preview": true, 93 | "submission_type": "any", 94 | "user_is_subscriber": true 95 | } 96 | } 97 | }, 98 | { 99 | "always": false, 100 | "request": [ 101 | "https://oauth.reddit.com/r/drawapitesting/api/flairtemplate/", 102 | "{api_type: json, flair_enabled: false, flair_position: right, flair_self_assign_enabled: true, link_flair_position: left, link_flair_self_assign_enabled: true}" 103 | ], 104 | "response": { 105 | "json": { 106 | "errors": [] 107 | } 108 | } 109 | }, 110 | { 111 | "always": false, 112 | "request": [ 113 | "https://oauth.reddit.com/r/drawapitesting/api/flairtemplate/", 114 | "{api_type: json, flair_enabled: false, flair_position: right, flair_self_assign_enabled: false, link_flair_position: left, link_flair_self_assign_enabled: false}" 115 | ], 116 | "response": { 117 | "json": { 118 | "errors": [] 119 | } 120 | } 121 | } 122 | ] -------------------------------------------------------------------------------- /test/subreddit/lib_subreddit_flair_delete.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/r/drawapitesting/api/flairlist/", 6 | "{limit: 100}" 7 | ], 8 | "response": { 9 | "users": [ 10 | { 11 | "flair_css_class": "redandblue", 12 | "user": "DRAWApiOfficial", 13 | "flair_text": "Test Flair" 14 | } 15 | ] 16 | } 17 | }, 18 | { 19 | "always": false, 20 | "request": [ 21 | "https://oauth.reddit.com/r/drawapitesting/api/deleteflair", 22 | "{api_type: json, name: DRAWApiOfficial}" 23 | ], 24 | "response": { 25 | "json": { 26 | "errors": [] 27 | } 28 | } 29 | }, 30 | { 31 | "always": false, 32 | "request": [ 33 | "https://oauth.reddit.com/r/drawapitesting/api/flairlist/", 34 | "{limit: 100}" 35 | ], 36 | "response": { 37 | "users": [] 38 | } 39 | } 40 | ] -------------------------------------------------------------------------------- /test/subreddit/lib_subreddit_flair_delete_all.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/r/drawapitesting/api/flairlist/", 6 | "{limit: 100}" 7 | ], 8 | "response": { 9 | "users": [ 10 | { 11 | "flair_css_class": "", 12 | "user": "DRAWApiOfficial", 13 | "flair_text": "Test Flair" 14 | } 15 | ] 16 | } 17 | }, 18 | { 19 | "always": false, 20 | "request": [ 21 | "https://oauth.reddit.com/r/drawapitesting/api/flairlist/", 22 | "{limit: 100}" 23 | ], 24 | "response": { 25 | "users": [ 26 | { 27 | "flair_css_class": "", 28 | "user": "DRAWApiOfficial", 29 | "flair_text": "Test Flair" 30 | } 31 | ] 32 | } 33 | }, 34 | { 35 | "always": false, 36 | "request": [ 37 | "https://oauth.reddit.com/r/drawapitesting/api/flaircsv/", 38 | "{flair_csv: \"DRAWApiOfficial\",\"\",\"\"}" 39 | ], 40 | "response": [ 41 | { 42 | "status": "removed flair for user DRAWApiOfficial", 43 | "errors": {}, 44 | "ok": true, 45 | "warnings": {} 46 | } 47 | ] 48 | }, 49 | { 50 | "always": false, 51 | "request": [ 52 | "https://oauth.reddit.com/r/drawapitesting/api/flairlist/", 53 | "{limit: 100}" 54 | ], 55 | "response": { 56 | "users": [] 57 | } 58 | } 59 | ] -------------------------------------------------------------------------------- /test/subreddit/lib_subreddit_flair_link_templates.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/r/drawapitesting/api/flairtemplate/", 6 | "{api_type: json, css_class: , flair_type: LINK_FLAIR, text: Foobazbar, text_editable: true}" 7 | ], 8 | "response": { 9 | "json": { 10 | "errors": [] 11 | } 12 | } 13 | }, 14 | { 15 | "always": false, 16 | "request": [ 17 | "https://oauth.reddit.com/r/drawapitesting/api/flairtemplate/", 18 | "{api_type: json, css_class: , flair_type: LINK_FLAIR, text: Foobarz, text_editable: false}" 19 | ], 20 | "response": { 21 | "json": { 22 | "errors": [] 23 | } 24 | } 25 | }, 26 | { 27 | "always": false, 28 | "request": [ 29 | "https://oauth.reddit.com/r/drawapitesting/api/link_flair", 30 | "null" 31 | ], 32 | "response": [ 33 | { 34 | "text": "Foobazbar", 35 | "text_editable": true, 36 | "id": "43c8b170-b3c5-11e8-90f7-0e3384945fba", 37 | "css_class": "" 38 | }, 39 | { 40 | "text": "Foobarz", 41 | "text_editable": false, 42 | "id": "43f97d8c-b3c5-11e8-a8c0-0e306e3ddd3c", 43 | "css_class": "" 44 | } 45 | ] 46 | }, 47 | { 48 | "always": false, 49 | "request": [ 50 | "https://oauth.reddit.com/r/drawapitesting/api/deleteflairtemplate/", 51 | "{api_type: json, flair_template_id: 43c8b170-b3c5-11e8-90f7-0e3384945fba}" 52 | ], 53 | "response": { 54 | "json": { 55 | "errors": [] 56 | } 57 | } 58 | }, 59 | { 60 | "always": false, 61 | "request": [ 62 | "https://oauth.reddit.com/r/drawapitesting/api/link_flair", 63 | "null" 64 | ], 65 | "response": [ 66 | { 67 | "text": "Foobarz", 68 | "text_editable": false, 69 | "id": "43f97d8c-b3c5-11e8-a8c0-0e306e3ddd3c", 70 | "css_class": "" 71 | } 72 | ] 73 | }, 74 | { 75 | "always": false, 76 | "request": [ 77 | "https://oauth.reddit.com/r/drawapitesting/api/flairtemplate/", 78 | "{api_type: json, css_class: , flair_template_id: 43f97d8c-b3c5-11e8-a8c0-0e306e3ddd3c, text: Foo, text_editable: false}" 79 | ], 80 | "response": { 81 | "json": { 82 | "errors": [] 83 | } 84 | } 85 | }, 86 | { 87 | "always": false, 88 | "request": [ 89 | "https://oauth.reddit.com/r/drawapitesting/api/clearflairtemplates/", 90 | "{api_type: json, flair_type: LINK_FLAIR}" 91 | ], 92 | "response": { 93 | "json": { 94 | "errors": [] 95 | } 96 | } 97 | }, 98 | { 99 | "always": false, 100 | "request": [ 101 | "https://oauth.reddit.com/r/drawapitesting/api/link_flair", 102 | "null" 103 | ], 104 | "response": [] 105 | } 106 | ] -------------------------------------------------------------------------------- /test/subreddit/lib_subreddit_flair_redditor_templates.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/r/drawapitesting/api/flairtemplate/", 6 | "{api_type: json, css_class: , flair_type: USER_FLAIR, text: Foobazbar, text_editable: true}" 7 | ], 8 | "response": { 9 | "json": { 10 | "errors": [] 11 | } 12 | } 13 | }, 14 | { 15 | "always": false, 16 | "request": [ 17 | "https://oauth.reddit.com/r/drawapitesting/api/flairtemplate/", 18 | "{api_type: json, css_class: , flair_type: USER_FLAIR, text: Foobarz, text_editable: false}" 19 | ], 20 | "response": { 21 | "json": { 22 | "errors": [] 23 | } 24 | } 25 | }, 26 | { 27 | "always": false, 28 | "request": [ 29 | "https://oauth.reddit.com/r/drawapitesting/api/flairselector/", 30 | "{}" 31 | ], 32 | "response": { 33 | "current": { 34 | "flair_css_class": null, 35 | "flair_template_id": null, 36 | "flair_text": null, 37 | "flair_position": "right" 38 | }, 39 | "choices": [ 40 | { 41 | "flair_css_class": "", 42 | "flair_template_id": "279096fc-b3c6-11e8-be4c-0eb4305c796a", 43 | "flair_text_editable": true, 44 | "flair_position": "right", 45 | "flair_text": "Foobazbar" 46 | }, 47 | { 48 | "flair_css_class": "", 49 | "flair_template_id": "27b23cf8-b3c6-11e8-9e40-0ea414e16e42", 50 | "flair_text_editable": true, 51 | "flair_position": "right", 52 | "flair_text": "Foobarz" 53 | } 54 | ] 55 | } 56 | }, 57 | { 58 | "always": false, 59 | "request": [ 60 | "https://oauth.reddit.com/r/drawapitesting/api/deleteflairtemplate/", 61 | "{api_type: json, flair_template_id: 279096fc-b3c6-11e8-be4c-0eb4305c796a}" 62 | ], 63 | "response": { 64 | "json": { 65 | "errors": [] 66 | } 67 | } 68 | }, 69 | { 70 | "always": false, 71 | "request": [ 72 | "https://oauth.reddit.com/r/drawapitesting/api/flairselector/", 73 | "{}" 74 | ], 75 | "response": { 76 | "current": { 77 | "flair_css_class": null, 78 | "flair_template_id": null, 79 | "flair_text": null, 80 | "flair_position": "right" 81 | }, 82 | "choices": [ 83 | { 84 | "flair_css_class": "", 85 | "flair_template_id": "27b23cf8-b3c6-11e8-9e40-0ea414e16e42", 86 | "flair_text_editable": true, 87 | "flair_position": "right", 88 | "flair_text": "Foobarz" 89 | } 90 | ] 91 | } 92 | }, 93 | { 94 | "always": false, 95 | "request": [ 96 | "https://oauth.reddit.com/r/drawapitesting/api/flairtemplate/", 97 | "{api_type: json, css_class: , flair_template_id: 27b23cf8-b3c6-11e8-9e40-0ea414e16e42, text: Foo, text_editable: false}" 98 | ], 99 | "response": { 100 | "json": { 101 | "errors": [] 102 | } 103 | } 104 | }, 105 | { 106 | "always": false, 107 | "request": [ 108 | "https://oauth.reddit.com/r/drawapitesting/api/clearflairtemplates/", 109 | "{api_type: json, flair_type: USER_FLAIR}" 110 | ], 111 | "response": { 112 | "json": { 113 | "errors": [] 114 | } 115 | } 116 | }, 117 | { 118 | "always": false, 119 | "request": [ 120 | "https://oauth.reddit.com/r/drawapitesting/api/flairselector/", 121 | "{}" 122 | ], 123 | "response": { 124 | "current": { 125 | "flair_css_class": null, 126 | "flair_template_id": null, 127 | "flair_text": null, 128 | "flair_position": "right" 129 | }, 130 | "choices": [] 131 | } 132 | } 133 | ] -------------------------------------------------------------------------------- /test/subreddit/lib_subreddit_flair_set_flair.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/r/drawapitesting/api/flairlist/", 6 | "{limit: 100}" 7 | ], 8 | "response": { 9 | "users": [ 10 | { 11 | "flair_css_class": "", 12 | "user": "DRAWApiOfficial", 13 | "flair_text": "Test Flair" 14 | } 15 | ] 16 | } 17 | }, 18 | { 19 | "always": false, 20 | "request": [ 21 | "https://oauth.reddit.com/r/drawapitesting/api/flair/", 22 | "{api_type: json, css_class: , name: XtremeCheese, text: Test flair 2}" 23 | ], 24 | "response": { 25 | "json": { 26 | "errors": [] 27 | } 28 | } 29 | }, 30 | { 31 | "always": false, 32 | "request": [ 33 | "https://oauth.reddit.com/r/drawapitesting/api/flairlist/", 34 | "{limit: 100}" 35 | ], 36 | "response": { 37 | "users": [ 38 | { 39 | "flair_css_class": null, 40 | "user": "XtremeCheese", 41 | "flair_text": "Test flair 2" 42 | }, 43 | { 44 | "flair_css_class": "", 45 | "user": "DRAWApiOfficial", 46 | "flair_text": "Test Flair" 47 | } 48 | ] 49 | } 50 | } 51 | ] -------------------------------------------------------------------------------- /test/subreddit/lib_subreddit_flair_update.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/r/drawapitesting/api/flaircsv/", 6 | "{flair_csv: \"DRAWApiOfficial\",\"Flair Test\",\"\"\n\"XtremeCheese\",\"Flair Test\",\"\"}" 7 | ], 8 | "response": [ 9 | { 10 | "status": "added flair for user DRAWApiOfficial", 11 | "errors": {}, 12 | "ok": true, 13 | "warnings": {} 14 | }, 15 | { 16 | "status": "added flair for user XtremeCheese", 17 | "errors": {}, 18 | "ok": true, 19 | "warnings": {} 20 | } 21 | ] 22 | }, 23 | { 24 | "always": false, 25 | "request": [ 26 | "https://oauth.reddit.com/r/drawapitesting/api/flairlist/", 27 | "{limit: 100}" 28 | ], 29 | "response": { 30 | "users": [ 31 | { 32 | "flair_css_class": "", 33 | "user": "XtremeCheese", 34 | "flair_text": "Flair Test" 35 | }, 36 | { 37 | "flair_css_class": "", 38 | "user": "DRAWApiOfficial", 39 | "flair_text": "Flair Test" 40 | } 41 | ] 42 | } 43 | }, 44 | { 45 | "always": false, 46 | "request": [ 47 | "https://oauth.reddit.com/r/drawapitesting/api/flaircsv/", 48 | "{flair_csv: \"DRAWApiOfficial\",\"\",\"\"\n\"XtremeCheese\",\"\",\"\"}" 49 | ], 50 | "response": [ 51 | { 52 | "status": "removed flair for user DRAWApiOfficial", 53 | "errors": {}, 54 | "ok": true, 55 | "warnings": {} 56 | }, 57 | { 58 | "status": "removed flair for user XtremeCheese", 59 | "errors": {}, 60 | "ok": true, 61 | "warnings": {} 62 | } 63 | ] 64 | }, 65 | { 66 | "always": false, 67 | "request": [ 68 | "https://oauth.reddit.com/r/drawapitesting/api/flairlist/", 69 | "{limit: 100}" 70 | ], 71 | "response": { 72 | "users": [] 73 | } 74 | } 75 | ] -------------------------------------------------------------------------------- /test/subreddit/lib_subreddit_invalid.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/r/drawapitesting2/about/", 6 | "null" 7 | ], 8 | "response": [ 9 | "DRAWRedirectResponse", 10 | "/subreddits/search" 11 | ] 12 | } 13 | ] -------------------------------------------------------------------------------- /test/subreddit/lib_subreddit_moderator_relationship_mods.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/r/drawapitesting/about/moderators/", 6 | "{limit: 100}" 7 | ], 8 | "response": { 9 | "kind": "UserList", 10 | "data": { 11 | "children": [ 12 | { 13 | "author_flair_css_class": null, 14 | "author_flair_text": null, 15 | "mod_permissions": [ 16 | "all" 17 | ], 18 | "date": 1499976584.0, 19 | "id": "t2_6g5vc", 20 | "name": "XtremeCheese" 21 | }, 22 | { 23 | "author_flair_css_class": null, 24 | "author_flair_text": null, 25 | "mod_permissions": [ 26 | "all" 27 | ], 28 | "date": 1501802131.0, 29 | "id": "t2_9b8dzfo", 30 | "name": "DRAWApiOfficial" 31 | } 32 | ] 33 | } 34 | } 35 | }, 36 | { 37 | "always": false, 38 | "request": [ 39 | "https://oauth.reddit.com/r/drawapitesting/about/moderators/", 40 | "{user: XtremeCheese, limit: 100}" 41 | ], 42 | "response": { 43 | "kind": "UserList", 44 | "data": { 45 | "children": [ 46 | { 47 | "author_flair_css_class": null, 48 | "author_flair_text": null, 49 | "mod_permissions": [ 50 | "all" 51 | ], 52 | "date": 1499976584.0, 53 | "id": "t2_6g5vc", 54 | "name": "XtremeCheese" 55 | } 56 | ] 57 | } 58 | } 59 | } 60 | ] -------------------------------------------------------------------------------- /test/subreddit/lib_subreddit_moderator_relationship_remove.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/r/drawapitesting/about/moderators/", 6 | "{limit: 100}" 7 | ], 8 | "response": { 9 | "kind": "UserList", 10 | "data": { 11 | "children": [ 12 | { 13 | "author_flair_css_class": null, 14 | "author_flair_text": null, 15 | "mod_permissions": [ 16 | "all" 17 | ], 18 | "date": 1540078242.0, 19 | "id": "t2_9b8dzfo", 20 | "name": "DRAWApiOfficial" 21 | }, 22 | { 23 | "author_flair_css_class": null, 24 | "author_flair_text": null, 25 | "mod_permissions": [ 26 | "all" 27 | ], 28 | "date": 1540078278.0, 29 | "id": "t2_6g5vc", 30 | "name": "XtremeCheese" 31 | } 32 | ] 33 | } 34 | } 35 | }, 36 | { 37 | "always": false, 38 | "request": [ 39 | "https://oauth.reddit.com/r/drawapitesting/api/unfriend/", 40 | "{name: XtremeCheese, type: moderator}" 41 | ], 42 | "response": {} 43 | }, 44 | { 45 | "always": false, 46 | "request": [ 47 | "https://oauth.reddit.com/r/drawapitesting/about/moderators/", 48 | "{limit: 100}" 49 | ], 50 | "response": { 51 | "kind": "UserList", 52 | "data": { 53 | "children": [ 54 | { 55 | "author_flair_css_class": null, 56 | "author_flair_text": null, 57 | "mod_permissions": [ 58 | "all" 59 | ], 60 | "date": 1540078242.0, 61 | "id": "t2_9b8dzfo", 62 | "name": "DRAWApiOfficial" 63 | } 64 | ] 65 | } 66 | } 67 | } 68 | ] -------------------------------------------------------------------------------- /test/subreddit/lib_subreddit_quarantine.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/api/quarantine_optin", 6 | "{sr_name: whiteswinfights}" 7 | ], 8 | "response": {} 9 | }, 10 | { 11 | "always": false, 12 | "request": [ 13 | "https://oauth.reddit.com/r/whiteswinfights/top", 14 | "{t: all, limit: 100}" 15 | ], 16 | "response": { 17 | "kind": "Listing", 18 | "data": { 19 | "after": null, 20 | "dist": 27, 21 | "modhash": null, 22 | "whitelist_status": null, 23 | "children": [ 24 | { 25 | "kind": "t3", 26 | "data": { 27 | "subreddit_id": "t5_38958", 28 | "approved_at_utc": null, 29 | "send_replies": true, 30 | "mod_reason_by": null, 31 | "banned_by": null, 32 | "num_reports": null, 33 | "removal_reason": null, 34 | "subreddit": "WhitesWinFights", 35 | "selftext_html": null, 36 | "selftext": "", 37 | "likes": null, 38 | "suggested_sort": null, 39 | "user_reports": [], 40 | "secure_media": null, 41 | "is_reddit_media_domain": false, 42 | "saved": false, 43 | "id": "39kkfr", 44 | "banned_at_utc": null, 45 | "mod_reason_title": null, 46 | "view_count": null, 47 | "archived": true, 48 | "clicked": false, 49 | "no_follow": false, 50 | "title": "", 51 | "num_crossposts": 0, 52 | "link_flair_text": null, 53 | "mod_reports": [], 54 | "can_mod_post": false, 55 | "is_crosspostable": false, 56 | "pinned": false, 57 | "score": 100, 58 | "approved_by": null, 59 | "over_18": true, 60 | "report_reasons": null, 61 | "domain": "hw-mobile.worldstarhiphop.com", 62 | "hidden": false, 63 | "thumbnail": "", 64 | "edited": false, 65 | "link_flair_css_class": null, 66 | "author_flair_css_class": null, 67 | "contest_mode": false, 68 | "gilded": 0, 69 | "downs": 0, 70 | "brand_safe": false, 71 | "secure_media_embed": {}, 72 | "media_embed": {}, 73 | "author_flair_text": null, 74 | "stickied": false, 75 | "can_gild": false, 76 | "is_self": false, 77 | "parent_whitelist_status": null, 78 | "name": "t3_39kkfr", 79 | "spoiler": false, 80 | "permalink": "/r/WhitesWinFights/comments/39kkfr/", 81 | "subreddit_type": "public", 82 | "locked": false, 83 | "hide_score": false, 84 | "created": 1434136296.0, 85 | "url": "", 86 | "whitelist_status": null, 87 | "quarantine": true, 88 | "author": "Squad", 89 | "created_utc": 1434107496.0, 90 | "subreddit_name_prefixed": "r/WhitesWinFights", 91 | "ups": 100, 92 | "media": null, 93 | "num_comments": 12, 94 | "visited": false, 95 | "mod_note": null, 96 | "is_video": false, 97 | "distinguished": null 98 | } 99 | } 100 | ], 101 | "before": null 102 | } 103 | } 104 | }, 105 | { 106 | "always": false, 107 | "request": [ 108 | "https://oauth.reddit.com/api/quarantine_optout", 109 | "{sr_name: whiteswinfights}" 110 | ], 111 | "response": {} 112 | }, 113 | { 114 | "always": false, 115 | "request": [ 116 | "https://oauth.reddit.com/r/whiteswinfights/top", 117 | "{t: all, limit: 100}" 118 | ], 119 | "response": { 120 | "kind": "DRAW_TEST_THROW_AUTH_ERROR" 121 | } 122 | } 123 | ] -------------------------------------------------------------------------------- /test/subreddit/lib_subreddit_rules.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/r/whatcouldgowrong/about/rules", 6 | "null" 7 | ], 8 | "response": { 9 | "rules": [ 10 | { 11 | "kind": "link", 12 | "description": "", 13 | "short_name": "Contains stupid idea and thing going wrong", 14 | "violation_reason": "No stupid idea or Nothing went wrong", 15 | "created_utc": 1487830366.0, 16 | "priority": 0 17 | }, 18 | { 19 | "kind": "link", 20 | "description": "", 21 | "short_name": "Direct links to content only (and no compilations)", 22 | "violation_reason": "Not a direct link to content", 23 | "created_utc": 1487830387.0, 24 | "priority": 1 25 | }, 26 | { 27 | "kind": "all", 28 | "description": "", 29 | "short_name": "Tag your material NSFW or NSFL as appropriate", 30 | "violation_reason": "Non-properly labeled as NSFW or NSFL", 31 | "created_utc": 1487830414.0, 32 | "priority": 2 33 | }, 34 | { 35 | "kind": "all", 36 | "description": "", 37 | "short_name": "No visible death posts allowed", 38 | "violation_reason": "Shows death", 39 | "created_utc": 1487830438.0, 40 | "priority": 3 41 | }, 42 | { 43 | "kind": "all", 44 | "description": "Mods can remove obscene material including racism, sexism, and other posts.", 45 | "short_name": "No obscene material", 46 | "violation_reason": "Contains obscene material", 47 | "created_utc": 1487830491.0, 48 | "priority": 4, 49 | "description_html": "<!-- SC_OFF --><div class=\"md\"><p>Mods can remove obscene material including racism, sexism, and other posts.</p>\n</div><!-- SC_ON -->" 50 | } 51 | ], 52 | "site_rules": [ 53 | "Spam", 54 | "Personal and confidential information", 55 | "Threatening, harassing, or inciting violence" 56 | ] 57 | } 58 | } 59 | ] -------------------------------------------------------------------------------- /test/subreddit/lib_subreddit_stylesheet_upload.json: -------------------------------------------------------------------------------- 1 | [{"always":false,"request":["https://oauth.reddit.com/r/drawapitesting/api/upload_sr_img","{name: foobar, upload_type: img, img_type: png, api_type: json}"],"response":{"errors":[],"img_src":"https://a.thumbs.redditmedia.com/MJGdqUs7bXLgG7-pYG5zVRdI_6qUQ6svvlZXURe5K98.png","errors_values":[]}}] -------------------------------------------------------------------------------- /test/subreddit/lib_subreddit_stylesheet_upload_bytes.json: -------------------------------------------------------------------------------- 1 | [{"always":false,"request":["https://oauth.reddit.com/r/drawapitesting/api/upload_sr_img","{name: foobar, upload_type: img, img_type: png, api_type: json}"],"response":{"errors":[],"img_src":"https://a.thumbs.redditmedia.com/MJGdqUs7bXLgG7-pYG5zVRdI_6qUQ6svvlZXURe5K98.png","errors_values":[]}}] -------------------------------------------------------------------------------- /test/subreddit/lib_subreddit_stylesheet_upload_header.json: -------------------------------------------------------------------------------- 1 | [{"always":false,"request":["https://oauth.reddit.com/r/drawapitesting/api/upload_sr_img","{upload_type: header, img_type: png, api_type: json}"],"response":{"errors":[],"img_src":"https://a.thumbs.redditmedia.com/MJGdqUs7bXLgG7-pYG5zVRdI_6qUQ6svvlZXURe5K98.png","errors_values":[]}}] -------------------------------------------------------------------------------- /test/subreddit/lib_subreddit_stylesheet_upload_invalid_cases.json: -------------------------------------------------------------------------------- 1 | [{"always":false,"request":["https://oauth.reddit.com/r/drawapitesting/api/upload_sr_img","{name: foobar, upload_type: img, img_type: png, api_type: json}"],"response":{"errors":["IMAGE_ERROR"],"img_src":"","errors_values":["Invalid image or general image error"]}}] -------------------------------------------------------------------------------- /test/subreddit/lib_subreddit_stylesheet_upload_mobile_header.json: -------------------------------------------------------------------------------- 1 | [{"always":false,"request":["https://oauth.reddit.com/r/drawapitesting/api/upload_sr_img","{upload_type: banner, img_type: jpeg, api_type: json}"],"response":{"errors":[],"img_src":"https://a.thumbs.redditmedia.com/MkErrkhg6-Iou7zdTRxnpwOSNK4DPWXZ3xI35LiKTU0.png","errors_values":[]}}] -------------------------------------------------------------------------------- /test/subreddit/lib_subreddit_stylesheet_upload_mobile_icon.json: -------------------------------------------------------------------------------- 1 | [{"always":false,"request":["https://oauth.reddit.com/r/drawapitesting/api/upload_sr_img","{upload_type: icon, img_type: jpeg, api_type: json}"],"response":{"errors":[],"img_src":"https://b.thumbs.redditmedia.com/GJSQRXiRY-2CH4PTgrrPNXqSPaQSKJYUikUr15m2n3Y.png","errors_values":[]}}] -------------------------------------------------------------------------------- /test/subreddit/lib_subreddit_wiki_invalid_wiki_page.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/r/drawapitesting/wiki/invalid", 6 | "{api_type: json, page: invalid}" 7 | ], 8 | "response": [ 9 | "DRAWNotFoundException", 10 | "PAGE_NOT_CREATED", 11 | "Not Found" 12 | ] 13 | } 14 | ] -------------------------------------------------------------------------------- /test/subreddit/lib_subreddit_wiki_page_moderation_add_remove.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/r/drawapitesting/wiki/settings/test_page", 6 | "null" 7 | ], 8 | "response": { 9 | "kind": "wikipagesettings", 10 | "data": { 11 | "permlevel": 1, 12 | "editors": [], 13 | "listed": false 14 | } 15 | } 16 | }, 17 | { 18 | "always": false, 19 | "request": [ 20 | "https://oauth.reddit.com/r/drawapitesting/api/wiki/alloweditor/add", 21 | "{page: test_page, username: Toxicity-Moderator}" 22 | ], 23 | "response": {} 24 | }, 25 | { 26 | "always": false, 27 | "request": [ 28 | "https://oauth.reddit.com/r/drawapitesting/wiki/settings/test_page", 29 | "null" 30 | ], 31 | "response": { 32 | "kind": "wikipagesettings", 33 | "data": { 34 | "permlevel": 1, 35 | "editors": [ 36 | { 37 | "kind": "t2", 38 | "data": { 39 | "is_employee": false, 40 | "icon_img": "https://www.redditstatic.com/avatars/avatar_default_13_008985.png", 41 | "pref_show_snoovatar": false, 42 | "name": "Toxicity-Moderator", 43 | "is_friend": false, 44 | "created": 1490608615.0, 45 | "has_subscribed": false, 46 | "hide_from_robots": false, 47 | "created_utc": 1490579815.0, 48 | "link_karma": 1, 49 | "comment_karma": 0, 50 | "is_gold": false, 51 | "is_mod": false, 52 | "verified": false, 53 | "subreddit": null, 54 | "has_verified_email": true, 55 | "id": "16ivow" 56 | } 57 | } 58 | ], 59 | "listed": false 60 | } 61 | } 62 | }, 63 | { 64 | "always": false, 65 | "request": [ 66 | "https://oauth.reddit.com/r/drawapitesting/api/wiki/alloweditor/del", 67 | "{page: test_page, username: Toxicity-Moderator}" 68 | ], 69 | "response": {} 70 | }, 71 | { 72 | "always": false, 73 | "request": [ 74 | "https://oauth.reddit.com/r/drawapitesting/wiki/settings/test_page", 75 | "null" 76 | ], 77 | "response": { 78 | "kind": "wikipagesettings", 79 | "data": { 80 | "permlevel": 1, 81 | "editors": [], 82 | "listed": false 83 | } 84 | } 85 | } 86 | ] -------------------------------------------------------------------------------- /test/subreddit/lib_subreddit_wiki_page_moderation_invalid_add_remove.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/r/drawapitesting/api/wiki/alloweditor/add", 6 | "{page: test_page, username: DrAwApIoFfIcIaL2}" 7 | ], 8 | "response": [ 9 | "DRAWNotFoundException", 10 | "UNKNOWN_USER", 11 | "Not Found" 12 | ] 13 | }, 14 | { 15 | "always": false, 16 | "request": [ 17 | "https://oauth.reddit.com/r/drawapitesting/api/wiki/alloweditor/del", 18 | "{page: test_page, username: DrAwApIoFfIcIaL2}" 19 | ], 20 | "response": [ 21 | "DRAWNotFoundException", 22 | "UNKNOWN_USER", 23 | "Not Found" 24 | ] 25 | } 26 | ] -------------------------------------------------------------------------------- /test/subreddit/lib_subreddit_wiki_page_moderation_settings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/r/drawapitesting/wiki/settings/test_page", 6 | "null" 7 | ], 8 | "response": { 9 | "kind": "wikipagesettings", 10 | "data": { 11 | "permlevel": 1, 12 | "editors": [], 13 | "listed": true 14 | } 15 | } 16 | }, 17 | { 18 | "always": false, 19 | "request": [ 20 | "https://oauth.reddit.com/r/drawapitesting/wiki/settings/test_page", 21 | "{listed: false, permlevel: 2}" 22 | ], 23 | "response": { 24 | "kind": "wikipagesettings", 25 | "data": { 26 | "permlevel": 2, 27 | "editors": [], 28 | "listed": false 29 | } 30 | } 31 | } 32 | ] -------------------------------------------------------------------------------- /test/subreddit/lib_subreddits_gold.json: -------------------------------------------------------------------------------- 1 | [{"always":false,"request":["https://oauth.reddit.com/subreddits/premium/","{limit: 5}"],"response":{"kind":"Listing","data":{"modhash":null,"dist":0,"children":[],"after":null,"before":null}}}] -------------------------------------------------------------------------------- /test/subreddit/lib_subreddits_recommended.json: -------------------------------------------------------------------------------- 1 | [{"always":false,"request":["https://oauth.reddit.com/api/recommend/sr/politics,MorbidReality","{omit: hillaryclinton}"],"response":[{"sr_name":"KasichForPresident"},{"sr_name":"PoliticalDiscussion"},{"sr_name":"TimCanova"},{"sr_name":"RenewableEnergy"},{"sr_name":"democrats"},{"sr_name":"LabourUK"},{"sr_name":"Liberal"},{"sr_name":"robintracking"},{"sr_name":"GrassrootsSelect"},{"sr_name":"enoughsandersspam"}]}] -------------------------------------------------------------------------------- /test/subreddit/lib_subreddits_search.json: -------------------------------------------------------------------------------- 1 | [{"always":false,"request":["https://oauth.reddit.com/subreddits/search/","{q: drawapitesting, limit: 100}"],"response":{"kind":"Listing","data":{"modhash":null,"dist":1,"children":[{"kind":"t5","data":{"user_flair_background_color":null,"submit_text_html":null,"restrict_posting":true,"user_is_banned":false,"free_form_reports":true,"wiki_enabled":true,"user_is_muted":false,"user_can_flair_in_sr":null,"display_name":"drawapitesting","header_img":null,"title":"DRAW API Testing","icon_size":null,"primary_color":"","active_user_count":null,"icon_img":null,"display_name_prefixed":"r/drawapitesting","accounts_active":null,"public_traffic":false,"subscribers":2,"user_flair_richtext":[],"videostream_links_count":0,"name":"t5_3mqw1","quarantine":false,"hide_ads":false,"emojis_enabled":false,"advertiser_category":"","public_description":"A subreddit used for testing the DRAW API: https://github.com/draw-dev/DRAW","comment_score_hide_mins":0,"user_has_favorited":false,"user_flair_template_id":null,"community_icon":"https://styles.redditmedia.com/t5_3mqw1/styles/communityIcon_aep5wbw2yzi21.png","banner_background_image":"https://styles.redditmedia.com/t5_3mqw1/styles/bannerBackgroundImage_dbkinvub40j21.png","original_content_tag_enabled":false,"submit_text":"","description_html":null,"spoilers_enabled":true,"header_title":null,"header_size":null,"user_flair_position":"right","all_original_content":false,"has_menu_widget":false,"is_enrolled_in_new_modmail":true,"key_color":"#24a0ed","event_posts_enabled":true,"can_assign_user_flair":true,"created":1500005383.0,"wls":null,"show_media_preview":true,"submission_type":"any","user_is_subscriber":true,"disable_contributor_requests":false,"allow_videogifs":true,"user_flair_type":"text","collapse_deleted_comments":true,"emojis_custom_size":null,"public_description_html":"<!-- SC_OFF --><div class=\"md\"><p>A subreddit used for testing the DRAW API: <a href=\"https://github.com/draw-dev/DRAW\">https://github.com/draw-dev/DRAW</a></p>\n</div><!-- SC_ON -->","allow_videos":true,"suggested_comment_sort":null,"can_assign_link_flair":true,"collections_enabled":true,"accounts_active_is_fuzzed":false,"submit_text_label":"null","link_flair_position":"right","user_sr_flair_enabled":null,"user_flair_enabled_in_sr":false,"allow_discovery":true,"user_sr_theme_enabled":true,"link_flair_enabled":true,"subreddit_type":"restricted","notification_level":"low","banner_img":null,"user_flair_text":null,"banner_background_color":"","show_media":true,"id":"3mqw1","user_is_contributor":true,"over18":false,"description":"","submit_link_label":"null","user_flair_text_color":null,"restrict_commenting":false,"user_flair_css_class":null,"allow_images":true,"lang":"en","whitelist_status":null,"url":"/r/drawapitesting/","created_utc":1499976583.0,"banner_size":null,"mobile_banner_image":"","user_is_moderator":true}}],"after":null,"before":null}}}] -------------------------------------------------------------------------------- /test/subreddit/lib_subreddits_stream.json: -------------------------------------------------------------------------------- 1 | [{"always":false,"request":["https://oauth.reddit.com/subreddits/new/","{limit: 1}"],"response":{"kind":"Listing","data":{"modhash":null,"dist":1,"children":[{"kind":"t5","data":{"user_flair_background_color":null,"submit_text_html":null,"restrict_posting":true,"user_is_banned":false,"free_form_reports":true,"wiki_enabled":null,"user_is_muted":false,"user_can_flair_in_sr":null,"display_name":"Taylor_Rain","header_img":null,"title":"Taylor_Rain","icon_size":null,"primary_color":"","active_user_count":null,"icon_img":"","display_name_prefixed":"r/Taylor_Rain","accounts_active":null,"public_traffic":false,"subscribers":1,"user_flair_richtext":[],"name":"t5_1rhn7r","quarantine":false,"hide_ads":false,"emojis_enabled":false,"advertiser_category":"","public_description":"Taylor_Rain","comment_score_hide_mins":0,"user_has_favorited":false,"user_flair_template_id":null,"community_icon":"","banner_background_image":"","original_content_tag_enabled":false,"submit_text":"","description_html":"<!-- SC_OFF --><div class=\"md\"><p>Taylor_Rain</p>\n</div><!-- SC_ON -->","spoilers_enabled":true,"header_title":"","header_size":null,"user_flair_position":"right","all_original_content":false,"has_menu_widget":false,"is_enrolled_in_new_modmail":null,"key_color":"","can_assign_user_flair":false,"created":1563001557.0,"wls":null,"show_media_preview":true,"submission_type":"any","user_is_subscriber":false,"disable_contributor_requests":false,"allow_videogifs":true,"user_flair_type":"text","collapse_deleted_comments":false,"emojis_custom_size":null,"public_description_html":"<!-- SC_OFF --><div class=\"md\"><p>Taylor_Rain</p>\n</div><!-- SC_ON -->","allow_videos":false,"suggested_comment_sort":null,"can_assign_link_flair":false,"accounts_active_is_fuzzed":false,"submit_text_label":"","link_flair_position":"","user_sr_flair_enabled":null,"user_flair_enabled_in_sr":false,"allow_discovery":true,"user_sr_theme_enabled":true,"link_flair_enabled":false,"subreddit_type":"public","notification_level":null,"banner_img":"","user_flair_text":null,"banner_background_color":"","show_media":true,"id":"1rhn7r","user_is_contributor":false,"over18":true,"description":"Taylor_Rain","submit_link_label":"","user_flair_text_color":null,"restrict_commenting":false,"user_flair_css_class":null,"allow_images":true,"lang":"en","whitelist_status":null,"url":"/r/Taylor_Rain/","created_utc":1562972757.0,"banner_size":null,"mobile_banner_image":"","user_is_moderator":false}}],"after":"t5_1rhn7r"}}}] 2 | -------------------------------------------------------------------------------- /test/subreddit/subreddit_listing_mixin_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | 8 | import 'package:test/test.dart'; 9 | import 'package:draw/draw.dart'; 10 | 11 | import '../test_utils.dart'; 12 | 13 | Future main() async { 14 | test('lib/subreddit/subreddit_listing_mixin_sanity', () async { 15 | final reddit = await createRedditTestInstance( 16 | 'test/subreddit/lib_subreddit_listing_mixin_sanity.json'); 17 | final subreddit = reddit.subreddit('funny'); 18 | await for (final content in subreddit.comments(params: {'limit': '10'})) { 19 | expect(content is Comment, isTrue); 20 | expect(content.body, isNotNull); 21 | } 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /test/subreddit/subreddit_moderation_unread.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/r/MorbidReality/message/moderator/unread/", 6 | "{limit: 100}" 7 | ], 8 | "response": { 9 | "kind": "Listing", 10 | "data": { 11 | "after": null, 12 | "dist": 1, 13 | "modhash": null, 14 | "whitelist_status": "promo_adult_nsfw", 15 | "children": [ 16 | { 17 | "kind": "t4", 18 | "data": { 19 | "first_message": null, 20 | "first_message_name": null, 21 | "subreddit": "MorbidReality", 22 | "likes": null, 23 | "replies": "", 24 | "id": "b9p3ar", 25 | "subject": "Doxxing Alert!", 26 | "was_comment": false, 27 | "score": 0, 28 | "author": "AutoModerator", 29 | "num_comments": null, 30 | "parent_id": null, 31 | "subreddit_name_prefixed": "r/MorbidReality", 32 | "new": true, 33 | "body": "\nhttps://www.reddit.com/r/MorbidReality/comments/86mnqm/woman_carjacked_raped_forced_to_jump_naked_into/dw6jw01/?context=3\n\nThe above comment by /u/keytud was removed because it contained a possible phone number. Please investigate immediately.\n\nIf the user is doxxing, [ban them](/r/MorbidReality/about/banned) and [report them to the reddit admins](http://www.reddit.com/message/compose?to=%2Fr%2Freddit.com&subject=Doxxing%20Report:%20%2Fu%2Fkeytud&message=%2Fu%2Fkeytud%20posted%20a%20phone%20number:%20https://www.reddit.com/r/MorbidReality/comments/86mnqm/woman_carjacked_raped_forced_to_jump_naked_into/dw6jw01/?context=3) immediately.", 34 | "dest": "#MorbidReality", 35 | "body_html": "<!-- SC_OFF --><div class=\"md\"><p><a href=\"https://www.reddit.com/r/MorbidReality/comments/86mnqm/woman_carjacked_raped_forced_to_jump_naked_into/dw6jw01/?context=3\">https://www.reddit.com/r/MorbidReality/comments/86mnqm/woman_carjacked_raped_forced_to_jump_naked_into/dw6jw01/?context=3</a></p>\n\n<p>The above comment by <a href=\"/u/keytud\">/u/keytud</a> was removed because it contained a possible phone number. Please investigate immediately.</p>\n\n<p>If the user is doxxing, <a href=\"/r/MorbidReality/about/banned\">ban them</a> and <a href=\"http://www.reddit.com/message/compose?to=%2Fr%2Freddit.com&amp;subject=Doxxing%20Report:%20%2Fu%2Fkeytud&amp;message=%2Fu%2Fkeytud%20posted%20a%20phone%20number:%20https://www.reddit.com/r/MorbidReality/comments/86mnqm/woman_carjacked_raped_forced_to_jump_naked_into/dw6jw01/?context=3\">report them to the reddit admins</a> immediately.</p>\n</div><!-- SC_ON -->", 36 | "name": "t4_b9p3ar", 37 | "created": 1521868316.0, 38 | "created_utc": 1521839516.0, 39 | "context": "", 40 | "distinguished": "moderator" 41 | } 42 | } 43 | ], 44 | "before": null 45 | } 46 | } 47 | } 48 | ] -------------------------------------------------------------------------------- /test/subreddit/subreddit_modmail_create.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/api/mod/conversations/", 6 | "{body: Hello Reddit!, isAuthorHidden: true, srName: drawapitesting, subject: Test Message!, to: DRAWApiOfficial}" 7 | ], 8 | "response": { 9 | "conversation": { 10 | "isAuto": false, 11 | "objIds": [ 12 | { 13 | "id": "8ghgw", 14 | "key": "messages" 15 | } 16 | ], 17 | "isRepliable": true, 18 | "lastUserUpdate": null, 19 | "isInternal": true, 20 | "lastModUpdate": "2018-10-05T02:09:46.607780+00:00", 21 | "lastUpdated": "2018-10-05T02:09:46.607780+00:00", 22 | "authors": [ 23 | { 24 | "isMod": true, 25 | "isAdmin": false, 26 | "name": "DRAWApiOfficial", 27 | "isOp": true, 28 | "isParticipant": false, 29 | "isHidden": true, 30 | "id": 20270258340, 31 | "isDeleted": false 32 | } 33 | ], 34 | "owner": { 35 | "displayName": "drawapitesting", 36 | "type": "subreddit", 37 | "id": "t5_3mqw1" 38 | }, 39 | "id": "5959g", 40 | "isHighlighted": false, 41 | "subject": "Test Message!", 42 | "participant": {}, 43 | "state": 0, 44 | "lastUnread": null, 45 | "numMessages": 1 46 | }, 47 | "messages": { 48 | "8ghgw": { 49 | "body": "

Hello Reddit!

\n
", 50 | "author": { 51 | "isMod": true, 52 | "isAdmin": false, 53 | "name": "DRAWApiOfficial", 54 | "isOp": true, 55 | "isParticipant": false, 56 | "isHidden": true, 57 | "id": 20270258340, 58 | "isDeleted": false 59 | }, 60 | "isInternal": true, 61 | "date": "2018-10-05T02:09:46.607780+00:00", 62 | "bodyMarkdown": "Hello Reddit!", 63 | "id": "8ghgw" 64 | } 65 | }, 66 | "modActions": {} 67 | } 68 | } 69 | ] -------------------------------------------------------------------------------- /test/subreddit/subreddit_modmail_subreddits.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/api/mod/conversations/subreddits", 6 | "null" 7 | ], 8 | "response": { 9 | "subreddits": { 10 | "t5_3mqw1": { 11 | "display_name": "drawapitesting", 12 | "name": "drawapitesting", 13 | "lastUpdated": "2018-09-30T23:00:15.552509+00:00", 14 | "keyColor": "#24a0ed", 15 | "subscribers": 2, 16 | "id": "t5_3mqw1", 17 | "icon": "" 18 | } 19 | } 20 | } 21 | } 22 | ] -------------------------------------------------------------------------------- /test/subreddit/subreddits_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | 8 | import 'package:draw/draw.dart'; 9 | import 'package:draw/src/models/subreddit.dart'; 10 | import 'package:test/test.dart'; 11 | 12 | import '../test_utils.dart'; 13 | 14 | Future main() async { 15 | test('lib/subreddits/default', () async { 16 | final reddit = await createRedditTestInstance( 17 | 'test/subreddit/lib_subreddits_default.json'); 18 | final stream = reddit.subreddits.defaults(limit: 5); 19 | final result = await stream.toList(); 20 | expect(result.length, 5); 21 | }); 22 | 23 | test('lib/subreddits/gold', () async { 24 | final reddit = await createRedditTestInstance( 25 | 'test/subreddit/lib_subreddits_gold.json'); 26 | final stream = reddit.subreddits.gold(limit: 5); 27 | final result = await stream.toList(); 28 | // Requires Reddit gold to get results. 29 | expect(result.length, 0); 30 | }); 31 | 32 | test('lib/subreddits/newest', () async { 33 | final reddit = await createRedditTestInstance( 34 | 'test/subreddit/lib_subreddits_newest.json'); 35 | final stream = reddit.subreddits.newest(limit: 5); 36 | final result = await stream.toList(); 37 | expect(result.length, 5); 38 | }); 39 | 40 | test('lib/subreddits/popular', () async { 41 | final reddit = await createRedditTestInstance( 42 | 'test/subreddit/lib_subreddits_popular.json'); 43 | final stream = reddit.subreddits.popular(limit: 5); 44 | final result = await stream.toList(); 45 | expect(result.length, 5); 46 | }); 47 | 48 | test('lib/subreddits/recommended', () async { 49 | final reddit = await createRedditTestInstance( 50 | 'test/subreddit/lib_subreddits_recommended.json'); 51 | 52 | final results = await reddit.subreddits.recommended( 53 | ['politics', SubredditRef.name(reddit, 'MorbidReality')], 54 | omitSubreddits: ['hillaryclinton']); 55 | expect(results.length, 10); 56 | 57 | // ignore: unawaited_futures 58 | expectLater(reddit.subreddits.recommended([2]), 59 | throwsA(TypeMatcher())); 60 | }); 61 | 62 | test('lib/subreddits/search', () async { 63 | final reddit = await createRedditTestInstance( 64 | 'test/subreddit/lib_subreddits_search.json'); 65 | final results = await reddit.subreddits.search('drawapitesting').toList(); 66 | expect(results.length, 1); 67 | }); 68 | 69 | test('lib/subreddits/search_by_name', () async { 70 | final reddit = await createRedditTestInstance( 71 | 'test/subreddit/lib_subreddits_search_by_name.json'); 72 | final names = (await reddit.subreddits.searchByName('Morbid')) 73 | .map((s) => s.displayName); 74 | final pattern = RegExp(r'[Mm]orbid'); 75 | for (final name in names) { 76 | expect(name.startsWith(pattern), isTrue); 77 | } 78 | 79 | final sfw = 80 | (await reddit.subreddits.searchByName('Morbid', includeNsfw: false)); 81 | 82 | for (final s in sfw) { 83 | final populated = await s.populate(); 84 | expect(populated.over18, isFalse); 85 | } 86 | }); 87 | 88 | test('lib/subreddits/stream', () async { 89 | final reddit = await createRedditTestInstance( 90 | 'test/subreddit/lib_subreddits_stream.json'); 91 | final result = await reddit.subreddits.stream(limit: 1).toList(); 92 | expect(result.length, 1); 93 | }); 94 | } 95 | -------------------------------------------------------------------------------- /test/test_all.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'auth/run_live_auth_tests.dart' as live_auth_test; 7 | import 'comment/comment_test.dart' as comment_test; 8 | import 'frontpage/frontpage_test.dart' as frontpage_test; 9 | import 'gilded_listing_mixin/gilded_listing_mixin_test.dart' 10 | as gilded_listing_mixin_test; 11 | import 'inbox/inbox_test.dart' as inbox_test; 12 | import 'messageable_mixin/messageable_mixin_test.dart' 13 | as messageable_mixin_test; 14 | import 'multireddit/multireddit_test.dart' as multireddit_test; 15 | import 'redditor/redditor_test.dart' as redditor_test; 16 | import 'rising_listing_mixin/rising_listing_mixin_test.dart' 17 | as rising_listing_mixin_test; 18 | import 'submission/submission_test.dart' as submission_test; 19 | import 'subreddit/subreddit_listing_mixin_test.dart' 20 | as subreddit_listing_mixin_test; 21 | import 'subreddit/subreddit_moderation_test.dart' as subreddit_mod_test; 22 | import 'subreddit/subreddits_test.dart' as subreddits_test; 23 | import 'subreddit/subreddit_test.dart' as subreddit_test; 24 | import 'unit_tests/enum_stringify_test.dart' as enum_stringify_test; 25 | import 'unit_tests/test_draw_config_context.dart' as draw_config_test; 26 | import 'unit_tests/utils_test.dart' as utils_test; 27 | import 'user_content_moderation/user_content_moderation_test.dart' 28 | as user_content_moderation_test; 29 | import 'user_content/user_content_test.dart' as user_content_test; 30 | import 'user/user_test.dart' as user_test; 31 | 32 | void main() { 33 | live_auth_test.main(); 34 | comment_test.main(); 35 | draw_config_test.main(); 36 | enum_stringify_test.main(); 37 | frontpage_test.main(); 38 | gilded_listing_mixin_test.main(); 39 | inbox_test.main(); 40 | messageable_mixin_test.main(); 41 | multireddit_test.main(); 42 | redditor_test.main(); 43 | rising_listing_mixin_test.main(); 44 | submission_test.main(); 45 | subreddits_test.main(); 46 | subreddit_listing_mixin_test.main(); 47 | subreddit_mod_test.main(); 48 | subreddit_test.main(); 49 | user_content_moderation_test.main(); 50 | user_content_test.main(); 51 | user_test.main(); 52 | utils_test.main(); 53 | } 54 | -------------------------------------------------------------------------------- /test/test_config.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:draw/src/draw_config_context.dart'; 3 | 4 | void main() { 5 | test('Simple tests for default section of local file.', () { 6 | final configContext = DRAWConfigContext(); 7 | expect(configContext.clientId, equals('Y4PJOclpDQy3xZ')); 8 | expect(configContext.clientSecret, equals('UkGLTe6oqsMk5nHCJTHLrwgvHpr')); 9 | expect(configContext.password, equals('pni9ubeht4wd50gk')); 10 | expect(configContext.username, equals('fakebot1')); 11 | }); 12 | 13 | test('Testing non-default section.', () { 14 | final configContext = DRAWConfigContext(siteName: 'section'); 15 | expect(configContext.password, equals('different')); 16 | expect(configContext.oauthUrl, equals('https://oauth.reddit.com')); 17 | }); 18 | 19 | test( 20 | 'Testing non-default section for parameters not present in default site.', 21 | () { 22 | final configContext = DRAWConfigContext(siteName: 'section1'); 23 | expect(configContext.password, equals('pni9ubeht4wd50gk')); 24 | expect(configContext.username, equals('sectionbot')); 25 | }); 26 | 27 | test('Testing non-default parameters with empty strings.', () { 28 | final configContext = DRAWConfigContext(siteName: 'emptyTest'); 29 | expect(configContext.username, equals('')); 30 | }); 31 | 32 | test('Testing default values for unset parameters.', () { 33 | final configContext = DRAWConfigContext(); 34 | expect(configContext.shortUrl, equals('https://redd.it')); 35 | expect(configContext.checkForUpdates, equals(false)); 36 | expect(configContext.revokeToken, 37 | equals('https://www.reddit.com/api/v1/revoke_token')); 38 | expect(configContext.oauthUrl, equals('oauth.reddit.com')); 39 | expect(configContext.authorizeUrl, 40 | equals('https://reddit.com/api/v1/authorize')); 41 | expect(configContext.accessToken, 42 | equals('https://www.reddit.com/api/v1/access_token')); 43 | }); 44 | 45 | test('Test for CheckForUpdates Truth value check', () { 46 | final configContext = DRAWConfigContext(siteName: 'testUpdateCheck1'); 47 | expect(configContext.checkForUpdates, equals(true)); 48 | final configContext1 = DRAWConfigContext(siteName: 'testUpdateCheckOn'); 49 | expect(configContext1.checkForUpdates, equals(true)); 50 | final configContext2 = DRAWConfigContext(siteName: 'testUpdateCheckTrue'); 51 | expect(configContext2.checkForUpdates, equals(true)); 52 | final configContext3 = DRAWConfigContext(siteName: 'testUpdateCheckYes'); 53 | expect(configContext3.checkForUpdates, equals(true)); 54 | final configContext4 = DRAWConfigContext(siteName: 'testUpdateCheckFalse'); 55 | expect(configContext4.checkForUpdates, equals(false)); 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /test/test_utils.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'dart:async'; 7 | 8 | import 'package:draw/draw.dart'; 9 | import 'auth/credentials.dart'; 10 | import 'test_authenticator.dart'; 11 | 12 | Future createRedditTestInstance(String path, 13 | {bool live = false}) async { 14 | var testAuth; 15 | if (live) { 16 | final tempReddit = await Reddit.createScriptInstance( 17 | userAgent: 'foobar', 18 | username: kUsername, 19 | password: kPassword, 20 | clientId: kScriptClientID, 21 | clientSecret: kScriptClientSecret); 22 | testAuth = TestAuthenticator(path, recordAuth: tempReddit.auth); 23 | } else { 24 | testAuth = TestAuthenticator(path); 25 | } 26 | return Reddit.fromAuthenticator(testAuth); 27 | } 28 | 29 | Future writeRecording(Reddit reddit) async { 30 | assert(reddit.auth is TestAuthenticator); 31 | final auth = reddit.auth as TestAuthenticator; 32 | await auth.writeRecording(); 33 | } 34 | -------------------------------------------------------------------------------- /test/unit_tests/test.ini: -------------------------------------------------------------------------------- 1 | default=default 2 | reddit_url=https://www.reddit_test.com 3 | client_id=Y4PJOclpDQy3xZ 4 | client_secret=UkGLTe6oqsMk5nHCJTHLrwgvHpr 5 | password=pni9ubeht4wd50gk 6 | username=fakebot1 7 | redirect_uri=www.google.com 8 | access_token=abc123 9 | http_proxy=http://proxy1.com 10 | https_proxy=https://proxy1-secure.com 11 | message_kind=t_10 12 | refresh_token=refresh123 13 | revoke_token=revoke123 14 | user_agent=foobar 15 | 16 | [section] 17 | password=different 18 | oauth_url=https://oauth.reddit.com 19 | 20 | [section1] 21 | password=pni9ubeht4wd50gk 22 | username=sectionbot 23 | 24 | [emptyTest] 25 | username= 26 | 27 | [testUpdateCheck1] 28 | check_for_updates=1 29 | 30 | [testUpdateCheckOn] 31 | check_for_updates=on 32 | 33 | [testUpdateCheckTrue] 34 | check_for_updates=true 35 | 36 | [testUpdateCheckYes] 37 | check_for_updates=yes 38 | 39 | [testUpdateCheckFalse] 40 | check_for_updates=false 41 | -------------------------------------------------------------------------------- /test/unit_tests/utils_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, the Dart Reddit API Wrapper project authors. 2 | // Please see the AUTHORS file for details. All rights reserved. 3 | // Use of this source code is governed by a BSD-style license that 4 | // can be found in the LICENSE file. 5 | 6 | import 'package:test/test.dart'; 7 | 8 | import 'package:draw/src/util.dart'; 9 | 10 | void main() { 11 | test('BoundedSet', () { 12 | final bounded = BoundedSet(5); 13 | for (var i = 0; i < 5; ++i) { 14 | bounded.add(i); 15 | expect(bounded.contains(i), isTrue); 16 | } 17 | expect(bounded.contains(6), isFalse); 18 | bounded.add(6); 19 | expect(bounded.contains(6), isTrue); 20 | expect(bounded.contains(0), isFalse); 21 | }); 22 | 23 | test('ExponentialCounter', () { 24 | final counter = ExponentialCounter(5); 25 | for (var i = 0; i < 25; ++i) { 26 | expect(counter.counter() <= 5.0, isTrue); 27 | } 28 | counter.reset(); 29 | expect(counter.counter() <= 2.0, isTrue); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /test/user/lib_user_contributorSubreddits.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/subreddits/mine/contributor/", 6 | "{limit: 101}" 7 | ], 8 | "response": { 9 | "kind": "Listing", 10 | "data": { 11 | "modhash": null, 12 | "children": [ 13 | { 14 | "kind": "t5", 15 | "data": { 16 | "user_is_contributor": true, 17 | "banner_img": "", 18 | "user_flair_text": null, 19 | "submit_text_html": null, 20 | "user_is_banned": false, 21 | "wiki_enabled": true, 22 | "show_media": true, 23 | "id": "3mqw1", 24 | "description": "", 25 | "submit_text": "", 26 | "user_can_flair_in_sr": null, 27 | "display_name": "drawapitesting", 28 | "header_img": null, 29 | "description_html": null, 30 | "title": "DRAW API Testing", 31 | "collapse_deleted_comments": true, 32 | "user_has_favorited": false, 33 | "public_description": "A subreddit used for testing the DRAW API: https://github.com/draw-dev/DRAW", 34 | "over18": false, 35 | "public_description_html": "<!-- SC_OFF --><div class=\"md\"><p>A subreddit used for testing the DRAW API: <a href=\"https://github.com/draw-dev/DRAW\">https://github.com/draw-dev/DRAW</a></p>\n</div><!-- SC_ON -->", 36 | "spoilers_enabled": true, 37 | "icon_size": null, 38 | "audience_target": "", 39 | "suggested_comment_sort": null, 40 | "active_user_count": null, 41 | "icon_img": "", 42 | "header_title": null, 43 | "display_name_prefixed": "r/drawapitesting", 44 | "user_is_muted": false, 45 | "submit_link_label": null, 46 | "accounts_active": null, 47 | "public_traffic": false, 48 | "header_size": null, 49 | "subscribers": 2, 50 | "user_flair_css_class": null, 51 | "submit_text_label": null, 52 | "key_color": "#24a0ed", 53 | "user_sr_flair_enabled": null, 54 | "lang": "en", 55 | "is_enrolled_in_new_modmail": true, 56 | "whitelist_status": null, 57 | "name": "t5_3mqw1", 58 | "user_flair_enabled_in_sr": false, 59 | "created": 1500005383.0, 60 | "url": "/r/drawapitesting/", 61 | "quarantine": false, 62 | "hide_ads": false, 63 | "created_utc": 1499976583.0, 64 | "banner_size": null, 65 | "user_is_moderator": true, 66 | "accounts_active_is_fuzzed": false, 67 | "advertiser_category": null, 68 | "user_sr_theme_enabled": true, 69 | "link_flair_enabled": false, 70 | "allow_images": true, 71 | "show_media_preview": true, 72 | "comment_score_hide_mins": 0, 73 | "subreddit_type": "restricted", 74 | "submission_type": "any", 75 | "user_is_subscriber": true 76 | } 77 | } 78 | ], 79 | "after": null, 80 | "before": null 81 | } 82 | } 83 | } 84 | ] -------------------------------------------------------------------------------- /test/user/lib_user_friends.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": true, 4 | "request": [ 5 | "https://oauth.reddit.com/api/v1/me/friends/", 6 | "null" 7 | ], 8 | "response": { 9 | "kind": "UserList", 10 | "data": { 11 | "children": [ 12 | { 13 | "date": 1501884713.0, 14 | "name": "XtremeCheese", 15 | "id": "t2_6g5vc" 16 | } 17 | ] 18 | } 19 | } 20 | } 21 | ] -------------------------------------------------------------------------------- /test/user/lib_user_karma.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": true, 4 | "request": [ 5 | "https://oauth.reddit.com/api/v1/me/karma", 6 | "null" 7 | ], 8 | "response": { 9 | "kind": "KarmaList", 10 | "data": [] 11 | } 12 | } 13 | ] -------------------------------------------------------------------------------- /test/user/lib_user_me.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": true, 4 | "request": [ 5 | "https://oauth.reddit.com/api/v1/me", 6 | "null" 7 | ], 8 | "response": { 9 | "is_employee": false, 10 | "features": { 11 | "do_not_track": true, 12 | "show_amp_link": true, 13 | "live_happening_now": true, 14 | "adserver_reporting": true, 15 | "give_hsts_grants": true, 16 | "legacy_search_pref": true, 17 | "listing_service_rampup": true, 18 | "mobile_web_targeting": true, 19 | "default_srs_holdout": { 20 | "owner": "relevance", 21 | "variant": "control_2", 22 | "experiment_id": 171 23 | }, 24 | "adzerk_do_not_track": true, 25 | "users_listing": true, 26 | "show_user_sr_name": true, 27 | "whitelisted_pms": true, 28 | "sticky_comments": true, 29 | "personalization_prefs": true, 30 | "upgrade_cookies": true, 31 | "interest_targeting": true, 32 | "new_report_flow": true, 33 | "block_user_by_report": true, 34 | "post_embed": true, 35 | "ads_auto_refund": true, 36 | "orangereds_as_emails": true, 37 | "mweb_xpromo_modal_listing_click_daily_dismissible_ios": true, 38 | "mweb_xpromo_ad_feed_android": { 39 | "owner": "channels", 40 | "variant": "control_2", 41 | "experiment_id": 195 42 | }, 43 | "expando_events": true, 44 | "eu_cookie_policy": true, 45 | "utm_comment_links": true, 46 | "force_https": true, 47 | "activity_service_write": true, 48 | "post_to_profile_beta": true, 49 | "reddituploads_redirect": true, 50 | "outbound_clicktracking": true, 51 | "new_loggedin_cache_policy": true, 52 | "inbox_push": true, 53 | "https_redirect": true, 54 | "search_dark_traffic": true, 55 | "mweb_xpromo_interstitial_comments_ios": true, 56 | "pause_ads": true, 57 | "programmatic_ads": true, 58 | "geopopular": true, 59 | "show_recommended_link": true, 60 | "mweb_xpromo_interstitial_comments_android": true, 61 | "ads_auction": true, 62 | "screenview_events": true, 63 | "new_report_dialog": true, 64 | "moat_tracking": true, 65 | "subreddit_rules": true, 66 | "mobile_settings": true, 67 | "adzerk_reporting_2": true, 68 | "mobile_native_banner": true, 69 | "ads_auto_extend": true, 70 | "geopopular_au_holdout": { 71 | "owner": "relevance", 72 | "variant": "control_2", 73 | "experiment_id": 206 74 | }, 75 | "scroll_events": true, 76 | "mweb_xpromo_modal_listing_click_daily_dismissible_android": true, 77 | "adblock_test": true, 78 | "activity_service_read": true 79 | }, 80 | "pref_no_profanity": true, 81 | "is_suspended": false, 82 | "pref_geopopular": "", 83 | "subreddit": null, 84 | "is_sponsor": false, 85 | "gold_expiration": null, 86 | "id": "9b8dzfo", 87 | "suspension_expiration_utc": null, 88 | "verified": false, 89 | "new_modmail_exists": true, 90 | "over_18": true, 91 | "is_gold": false, 92 | "is_mod": true, 93 | "has_verified_email": true, 94 | "has_mod_mail": false, 95 | "oauth_client_id": "Db_4C6XcNCNqow", 96 | "hide_from_robots": false, 97 | "link_karma": 1, 98 | "inbox_count": 0, 99 | "pref_top_karma_subreddits": null, 100 | "has_mail": false, 101 | "pref_show_snoovatar": false, 102 | "name": "DRAWApiOfficial", 103 | "created": 1501830779.0, 104 | "gold_creddits": 0, 105 | "created_utc": 1501801979.0, 106 | "in_beta": false, 107 | "comment_karma": 0, 108 | "has_subscribed": false 109 | } 110 | } 111 | ] -------------------------------------------------------------------------------- /test/user/lib_user_moderatorSubreddits.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/subreddits/mine/moderator/", 6 | "{limit: 100}" 7 | ], 8 | "response": { 9 | "kind": "Listing", 10 | "data": { 11 | "modhash": null, 12 | "children": [ 13 | { 14 | "kind": "t5", 15 | "data": { 16 | "user_is_contributor": true, 17 | "banner_img": "", 18 | "user_flair_text": null, 19 | "submit_text_html": null, 20 | "user_is_banned": false, 21 | "wiki_enabled": true, 22 | "show_media": true, 23 | "id": "3mqw1", 24 | "description": "", 25 | "submit_text": "", 26 | "user_can_flair_in_sr": null, 27 | "display_name": "drawapitesting", 28 | "header_img": null, 29 | "description_html": null, 30 | "title": "DRAW API Testing", 31 | "collapse_deleted_comments": true, 32 | "user_has_favorited": false, 33 | "public_description": "A subreddit used for testing the DRAW API: https://github.com/draw-dev/DRAW", 34 | "over18": false, 35 | "public_description_html": "<!-- SC_OFF --><div class=\"md\"><p>A subreddit used for testing the DRAW API: <a href=\"https://github.com/draw-dev/DRAW\">https://github.com/draw-dev/DRAW</a></p>\n</div><!-- SC_ON -->", 36 | "spoilers_enabled": true, 37 | "icon_size": null, 38 | "audience_target": "", 39 | "suggested_comment_sort": null, 40 | "active_user_count": null, 41 | "icon_img": "", 42 | "header_title": null, 43 | "display_name_prefixed": "r/drawapitesting", 44 | "user_is_muted": false, 45 | "submit_link_label": null, 46 | "accounts_active": null, 47 | "public_traffic": false, 48 | "header_size": null, 49 | "subscribers": 2, 50 | "user_flair_css_class": null, 51 | "submit_text_label": null, 52 | "key_color": "#24a0ed", 53 | "user_sr_flair_enabled": null, 54 | "lang": "en", 55 | "is_enrolled_in_new_modmail": true, 56 | "whitelist_status": null, 57 | "name": "t5_3mqw1", 58 | "user_flair_enabled_in_sr": false, 59 | "created": 1500005383.0, 60 | "url": "/r/drawapitesting/", 61 | "quarantine": false, 62 | "hide_ads": false, 63 | "created_utc": 1499976583.0, 64 | "banner_size": null, 65 | "user_is_moderator": true, 66 | "mod_permissions": [ 67 | "all" 68 | ], 69 | "accounts_active_is_fuzzed": false, 70 | "advertiser_category": null, 71 | "user_sr_theme_enabled": true, 72 | "link_flair_enabled": false, 73 | "allow_images": true, 74 | "show_media_preview": true, 75 | "comment_score_hide_mins": 0, 76 | "subreddit_type": "restricted", 77 | "submission_type": "any", 78 | "user_is_subscriber": true 79 | } 80 | } 81 | ], 82 | "after": null, 83 | "before": null 84 | } 85 | } 86 | } 87 | ] -------------------------------------------------------------------------------- /test/user_content/lib_user_content_replyable.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/api/submit/", 6 | "{api_type: json, sr: drawapitesting, resubmit: true, sendreplies: true, title: Replyable submission, nsfw: false, spoiler: false, kind: self, text: Testing!}" 7 | ], 8 | "response": { 9 | "json": { 10 | "errors": [], 11 | "data": { 12 | "url": "https://www.reddit.com/r/drawapitesting/comments/9f9j0y/replyable_submission/", 13 | "drafts_count": 0, 14 | "id": "9f9j0y", 15 | "name": "t3_9f9j0y" 16 | } 17 | } 18 | } 19 | }, 20 | { 21 | "always": false, 22 | "request": [ 23 | "https://oauth.reddit.com/api/comment/", 24 | "{text: Test comment!, thing_id: t3_9f9j0y, api_type: json}" 25 | ], 26 | "response": { 27 | "json": { 28 | "errors": [], 29 | "data": { 30 | "things": [ 31 | { 32 | "kind": "t1", 33 | "data": { 34 | "author_flair_background_color": null, 35 | "subreddit_id": "t5_3mqw1", 36 | "approved_at_utc": null, 37 | "distinguished": null, 38 | "mod_reason_by": null, 39 | "banned_by": null, 40 | "author_flair_type": "text", 41 | "removal_reason": null, 42 | "link_id": "t3_9f9j0y", 43 | "author_flair_template_id": null, 44 | "likes": true, 45 | "replies": "", 46 | "user_reports": [], 47 | "saved": false, 48 | "id": "e5uqrtw", 49 | "banned_at_utc": null, 50 | "mod_reason_title": null, 51 | "gilded": 0, 52 | "archived": false, 53 | "report_reasons": [], 54 | "author": "DRAWApiOfficial", 55 | "rte_mode": "markdown", 56 | "can_mod_post": true, 57 | "send_replies": true, 58 | "parent_id": "t3_9f9j0y", 59 | "score": 1, 60 | "author_fullname": "t2_9b8dzfo", 61 | "approved_by": null, 62 | "mod_note": null, 63 | "collapsed": false, 64 | "body": "Test comment!", 65 | "edited": false, 66 | "author_flair_css_class": null, 67 | "name": "t1_e5uqrtw", 68 | "downs": 0, 69 | "author_flair_richtext": [], 70 | "is_submitter": true, 71 | "collapsed_reason": null, 72 | "body_html": "<div class=\"md\"><p>Test comment!</p>\n</div>", 73 | "stickied": false, 74 | "can_gild": false, 75 | "removed": false, 76 | "approved": false, 77 | "author_flair_text_color": null, 78 | "score_hidden": false, 79 | "permalink": "/r/drawapitesting/comments/9f9j0y/replyable_submission/e5uqrtw/", 80 | "num_reports": 0, 81 | "no_follow": false, 82 | "created": 1536800228.0, 83 | "subreddit": "drawapitesting", 84 | "author_flair_text": null, 85 | "spam": false, 86 | "created_utc": 1536771428.0, 87 | "subreddit_name_prefixed": "r/drawapitesting", 88 | "controversiality": 0, 89 | "ignore_reports": false, 90 | "mod_reports": [], 91 | "subreddit_type": "restricted", 92 | "ups": 1 93 | } 94 | } 95 | ] 96 | } 97 | } 98 | } 99 | }, 100 | { 101 | "always": false, 102 | "request": [ 103 | "https://oauth.reddit.com/api/del/", 104 | "{id: t3_9f9j0y}" 105 | ], 106 | "response": {} 107 | }, 108 | { 109 | "always": false, 110 | "request": [ 111 | "https://oauth.reddit.com/api/del/", 112 | "{id: t1_e5uqrtw}" 113 | ], 114 | "response": {} 115 | } 116 | ] -------------------------------------------------------------------------------- /test/user_content/lib_user_content_toggleable.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "always": false, 4 | "request": [ 5 | "https://oauth.reddit.com/api/submit/", 6 | "{api_type: json, sr: drawapitesting, resubmit: true, sendreplies: true, title: Editable submission, nsfw: false, spoiler: false, kind: self, text: Testing!}" 7 | ], 8 | "response": { 9 | "json": { 10 | "errors": [], 11 | "data": { 12 | "url": "https://www.reddit.com/r/drawapitesting/comments/9f9j7y/editable_submission/", 13 | "drafts_count": 0, 14 | "id": "9f9j7y", 15 | "name": "t3_9f9j7y" 16 | } 17 | } 18 | } 19 | }, 20 | { 21 | "always": false, 22 | "request": [ 23 | "https://oauth.reddit.com/api/sendreplies/", 24 | "{id: t3_9f9j7y, state: false}" 25 | ], 26 | "response": {} 27 | }, 28 | { 29 | "always": false, 30 | "request": [ 31 | "https://oauth.reddit.com/api/sendreplies/", 32 | "{id: t3_9f9j7y, state: true}" 33 | ], 34 | "response": {} 35 | } 36 | ] -------------------------------------------------------------------------------- /third_party/reply/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.2.0-dev 4 | 5 | - Migrate to null safety 6 | 7 | ## 0.1.2-dev 8 | 9 | - Add support for JSON encoding 10 | - Enable creating a `Recording` object from a set of records 11 | 12 | 13 | ## 0.1.1-dev 14 | 15 | - Add support for a custom `Equality` object for matching requests 16 | 17 | ## 0.1.0-dev 18 | 19 | - Initial commit; supports basic record/reply functionality 20 | -------------------------------------------------------------------------------- /third_party/reply/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, reply.dart authors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /third_party/reply/README.md: -------------------------------------------------------------------------------- 1 | Utilities for recording and replaying API interactions 2 | 3 | *This package is a fork of [reply](https://github.com/matanlurey/reply.dart) that has been migrated to Dart 2 with null safety* -------------------------------------------------------------------------------- /third_party/reply/lib/src/conclusion_builder.dart: -------------------------------------------------------------------------------- 1 | part of reply; 2 | 3 | class _DefaultConclusionBuilder> 4 | implements ConclusionBuilder { 5 | final T _recorder; 6 | final Q _request; 7 | final R _response; 8 | 9 | _DefaultConclusionBuilder( 10 | this._recorder, 11 | this._request, 12 | this._response, 13 | ); 14 | 15 | @override 16 | T always() { 17 | return _recorder 18 | ..addRecord(_DefaultRecord(_request, _response, always: true)); 19 | } 20 | 21 | @override 22 | T once() { 23 | return _recorder..addRecord(_DefaultRecord(_request, _response)); 24 | } 25 | 26 | @override 27 | T times(int times) { 28 | for (var i = 0; i < times; i++) { 29 | _recorder.addRecord(_DefaultRecord(_request, _response)); 30 | } 31 | return _recorder; 32 | } 33 | } 34 | 35 | class _DefaultRecord implements Record { 36 | @override 37 | final bool always; 38 | 39 | @override 40 | final Q request; 41 | 42 | @override 43 | final R response; 44 | 45 | const _DefaultRecord(this.request, this.response, {this.always = false}); 46 | } 47 | -------------------------------------------------------------------------------- /third_party/reply/lib/src/recorder.dart: -------------------------------------------------------------------------------- 1 | part of reply; 2 | 3 | class _DefaultRecorder implements Recorder { 4 | final Equality _requestEquality; 5 | final List> _records = >[]; 6 | 7 | _DefaultRecorder({Equality? requestEquality}) 8 | : _requestEquality = requestEquality ?? IdentityEquality(); 9 | 10 | @override 11 | void addRecord(Record record) { 12 | _records.add(record); 13 | } 14 | 15 | @override 16 | ResponseBuilder given(Q request) { 17 | if (request == null) { 18 | throw ArgumentError.notNull('request'); 19 | } 20 | return _DefaultResponseBuilder(this, request); 21 | } 22 | 23 | @override 24 | Recording toRecording() { 25 | return Recording(_records, requestEquality: _requestEquality); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /third_party/reply/lib/src/recording.dart: -------------------------------------------------------------------------------- 1 | part of reply; 2 | 3 | class _DefaultRecording implements Recording { 4 | final List> _records; 5 | final Equality _requestEquality; 6 | 7 | _DefaultRecording( 8 | Iterable> records, { 9 | Equality? requestEquality, 10 | }) : _records = records.toList(), 11 | _requestEquality = requestEquality ?? IdentityEquality(); 12 | 13 | @override 14 | bool hasRecord(Q request) { 15 | return _records.any((r) => _requestEquality.equals(request, r.request)); 16 | } 17 | 18 | @override 19 | R reply(Q request) { 20 | for (var i = 0; i < _records.length; i++) { 21 | if (_requestEquality.equals(_records[i].request, request)) { 22 | return _replyAt(i); 23 | } 24 | } 25 | throw StateError('No record found for $request.'); 26 | } 27 | 28 | R _replyAt(int index) { 29 | final record = _records[index]; 30 | if (!record.always) { 31 | _records.removeAt(index); 32 | } 33 | return record.response; 34 | } 35 | 36 | @override 37 | dynamic toJsonEncodable({ 38 | required Function(Q request) encodeRequest, 39 | required Function(R response) encodeResponse, 40 | }) => 41 | _records.map((record) { 42 | return { 43 | 'always': record.always, 44 | 'request': encodeRequest(record.request), 45 | 'response': encodeResponse(record.response), 46 | }; 47 | }).toList(); 48 | } 49 | -------------------------------------------------------------------------------- /third_party/reply/lib/src/response_builder.dart: -------------------------------------------------------------------------------- 1 | part of reply; 2 | 3 | class _DefaultResponseBuilder implements ResponseBuilder { 4 | final Recorder _recorder; 5 | final Q _request; 6 | 7 | _DefaultResponseBuilder(this._recorder, this._request); 8 | 9 | @override 10 | ConclusionBuilder> reply( 11 | R response, { 12 | void Function(Branch branch)? andBranch, 13 | }) { 14 | if (andBranch != null) { 15 | throw UnimplementedError(); 16 | } 17 | if (response == null) { 18 | throw ArgumentError.notNull('response'); 19 | } 20 | return _DefaultConclusionBuilder( 21 | _recorder, 22 | _request, 23 | response, 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /third_party/reply/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: reply 2 | version: 0.2.0-dev 3 | description: Utilities for recording and replaying API interactions 4 | homepage: https://github.com/matanlurey/reply.dart 5 | authors: 6 | - Matan Lurey 7 | - Brian Robles 8 | 9 | environment: 10 | sdk: '>=2.12.0 <3.0.0' 11 | 12 | dependencies: 13 | collection: '^1.15.0' 14 | 15 | dev_dependencies: 16 | test: '^1.17.10' 17 | -------------------------------------------------------------------------------- /third_party/reply/test/reply_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:collection/collection.dart'; 4 | import 'package:reply/reply.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | group('Simple request/response', () { 9 | late Recorder recorder; 10 | const notInfiniteButALot = 100; 11 | setUp(() => recorder = Recorder()); 12 | 13 | test('should support responding once', () { 14 | recorder.given('Hello').reply('Hi there!').once(); 15 | final recording = recorder.toRecording(); 16 | expect(recording.hasRecord('Hello'), isTrue); 17 | expect(recording.reply('Hello'), 'Hi there!'); 18 | expect(recording.hasRecord('Hello'), isFalse); 19 | expect(() => recording.reply('Hello'), throwsStateError); 20 | }); 21 | 22 | test('should support emitting a response n times', () { 23 | recorder.given('Hello').reply('Hi there!').times(2); 24 | final recording = recorder.toRecording(); 25 | expect(recording.reply('Hello'), 'Hi there!'); 26 | expect(recording.reply('Hello'), 'Hi there!'); 27 | expect(() => recording.reply('Hello'), throwsStateError); 28 | }); 29 | 30 | test('should support emitting a response ∞ times', () { 31 | recorder.given('Hello').reply('Hi there!').always(); 32 | final recording = recorder.toRecording(); 33 | for (var i = 0; i < notInfiniteButALot; i++) { 34 | expect(recording.reply('Hello'), 'Hi there!'); 35 | } 36 | expect(recording.hasRecord('Hello'), isTrue); 37 | }); 38 | 39 | test('should encode as a valid JSON', () { 40 | recorder 41 | .given('Hello') 42 | .reply('Hi there!') 43 | .times(2) 44 | .given('Thanks') 45 | .reply('You are welcome!') 46 | .always(); 47 | final json = recorder.toRecording().toJsonEncodable( 48 | encodeRequest: (q) => q, 49 | encodeResponse: (r) => r, 50 | ); 51 | expect(json, [ 52 | { 53 | 'always': false, 54 | 'request': 'Hello', 55 | 'response': 'Hi there!', 56 | }, 57 | { 58 | 'always': false, 59 | 'request': 'Hello', 60 | 'response': 'Hi there!', 61 | }, 62 | { 63 | 'always': true, 64 | 'request': 'Thanks', 65 | 'response': 'You are welcome!', 66 | }, 67 | ]); 68 | expect(jsonDecode(jsonEncode(json)), json); 69 | final copy = Recording.fromJson( 70 | json, 71 | toRequest: (q) => q, 72 | toResponse: (r) => r, 73 | ); 74 | expect(copy.hasRecord('Hello'), isTrue); 75 | expect(copy.hasRecord('Thanks'), isTrue); 76 | }); 77 | }); 78 | 79 | group('Custom equality', () { 80 | late Recorder<_CustomRequest, String> recorder; 81 | 82 | setUp(() { 83 | recorder = Recorder<_CustomRequest, String>( 84 | requestEquality: const _CustomRequestEquality(), 85 | ); 86 | }); 87 | 88 | test('should be used to determine request matches', () { 89 | recorder.given(_CustomRequest(123, 'A')).reply('Hello').once(); 90 | final recording = recorder.toRecording(); 91 | expect( 92 | recording.hasRecord(_CustomRequest(123, 'B')), 93 | isTrue, 94 | ); 95 | }); 96 | }); 97 | } 98 | 99 | class _CustomRequest { 100 | final int idNumber; 101 | final String name; 102 | 103 | _CustomRequest(this.idNumber, this.name); 104 | } 105 | 106 | class _CustomRequestEquality implements Equality<_CustomRequest> { 107 | const _CustomRequestEquality(); 108 | 109 | @override 110 | bool equals(_CustomRequest e1, _CustomRequest e2) { 111 | return e1.idNumber == e2.idNumber; 112 | } 113 | 114 | @override 115 | int hash(_CustomRequest e) => e.idNumber; 116 | 117 | @override 118 | bool isValidKey(Object? o) => o is _CustomRequest; 119 | } 120 | -------------------------------------------------------------------------------- /tool/travis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Fast fail the script on failures. 4 | set -e 5 | 6 | # Install dart_coveralls; gather and send coverage data. 7 | if [ "$COVERALLS_TOKEN" ] && [ "$TRAVIS_DART_VERSION" = "stable" ]; then 8 | pub global activate dart_coveralls 9 | #git clone https://github.com/bkonyi/dart-coveralls.git 10 | #cd dart-coveralls 11 | #pub get 12 | #cd .. 13 | echo "Running coverage..." 14 | #dart dart-coveralls/bin/dart_coveralls.dart report \ 15 | dart_coveralls report \ 16 | --retry 2 \ 17 | --exclude-test-files \ 18 | --throw-on-error \ 19 | --throw-on-connectivity-error \ 20 | test/test_all.dart 21 | echo "Coverage complete." 22 | rm -rf dart-coveralls 23 | else 24 | if [ -z ${COVERALLS_TOKEN+x} ]; then echo "COVERALLS_TOKEN is unset"; fi 25 | if [ -z ${TRAVIS_DART_VERSION+x} ]; then 26 | echo "TRAVIS_DART_VERSION is unset"; 27 | else 28 | echo "TRAVIS_DART_VERSION is $TRAVIS_DART_VERSION"; 29 | fi 30 | 31 | echo "Skipping coverage for this configuration." 32 | fi 33 | --------------------------------------------------------------------------------