Twitter-API-1.0006/000755 000765 000024 00000000000 14031400662 013732 5ustar00marcstaff000000 000000 Twitter-API-1.0006/Build.PL000644 000765 000024 00000000260 14031400662 015224 0ustar00marcstaff000000 000000 # This Build.PL for Twitter-API was generated by Dist::Zilla::Plugin::ModuleBuildTiny 0.015. use strict; use warnings; use v5.14.1; use Module::Build::Tiny 0.034; Build_PL(); Twitter-API-1.0006/LICENSE000644 000765 000024 00000043663 14031400662 014753 0ustar00marcstaff000000 000000 This software is copyright (c) 2015-2021 by Marc Mims. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. Terms of the Perl programming language system itself a) the GNU General Public License as published by the Free Software Foundation; either version 1, or (at your option) any later version, or b) the "Artistic License" --- The GNU General Public License, Version 1, February 1989 --- This software is Copyright (c) 2015-2021 by Marc Mims. This is free software, licensed under: The GNU General Public License, Version 1, February 1989 GNU GENERAL PUBLIC LICENSE Version 1, February 1989 Copyright (C) 1989 Free Software Foundation, Inc. 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The license agreements of most software companies try to keep users at the mercy of those companies. By contrast, our General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. The General Public License applies to the Free Software Foundation's software and to any other program whose authors commit to using it. You can use it for your programs, too. When we speak of free software, we are referring to freedom, not price. Specifically, the General Public License is designed to make sure that you have the freedom to give away or sell copies of free software, that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of a such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must tell them their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any work containing the Program or a portion of it, either verbatim or with modifications. Each licensee is addressed as "you". 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this General Public License and to the absence of any warranty; and give any other recipients of the Program a copy of this General Public License along with the Program. You may charge a fee for the physical act of transferring a copy. 2. You may modify your copy or copies of the Program or any portion of it, and copy and distribute such modifications under the terms of Paragraph 1 above, provided that you also do the following: a) cause the modified files to carry prominent notices stating that you changed the files and the date of any change; and b) cause the whole of any work that you distribute or publish, that in whole or in part contains the Program or any part thereof, either with or without modifications, to be licensed at no charge to all third parties under the terms of this General Public License (except that you may choose to grant warranty protection to some or all third parties, at your option). c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the simplest and most usual way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this General Public License. d) You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. Mere aggregation of another independent work with the Program (or its derivative) on a volume of a storage or distribution medium does not bring the other work under the scope of these terms. 3. You may copy and distribute the Program (or a portion or derivative of it, under Paragraph 2) in object code or executable form under the terms of Paragraphs 1 and 2 above provided that you also do one of the following: a) accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Paragraphs 1 and 2 above; or, b) accompany it with a written offer, valid for at least three years, to give any third party free (except for a nominal charge for the cost of distribution) a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Paragraphs 1 and 2 above; or, c) accompany it with the information you received as to where the corresponding source code may be obtained. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form alone.) Source code for a work means the preferred form of the work for making modifications to it. For an executable file, complete source code means all the source code for all modules it contains; but, as a special exception, it need not include source code for modules which are standard libraries that accompany the operating system on which the executable file runs, or for standard header files or definitions files that accompany that operating system. 4. You may not copy, modify, sublicense, distribute or transfer the Program except as expressly provided under this General Public License. Any attempt otherwise to copy, modify, sublicense, distribute or transfer the Program is void, and will automatically terminate your rights to use the Program under this License. However, parties who have received copies, or rights to use copies, from you under this General Public License will not have their licenses terminated so long as such parties remain in full compliance. 5. By copying, distributing or modifying the Program (or any work based on the Program) you indicate your acceptance of this license to do so, and all its terms and conditions. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. 7. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of the license which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the license, you may choose any version ever published by the Free Software Foundation. 8. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 9. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 10. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS Appendix: How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to humanity, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 19yy This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 1, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) 19xx name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (a program to direct compilers to make passes at assemblers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice That's all there is to it! --- The Artistic License 1.0 --- This software is Copyright (c) 2015-2021 by Marc Mims. This is free software, licensed under: The Artistic License 1.0 The Artistic License Preamble The intent of this document is to state the conditions under which a Package may be copied, such that the Copyright Holder maintains some semblance of artistic control over the development of the package, while giving the users of the package the right to use and distribute the Package in a more-or-less customary fashion, plus the right to make reasonable modifications. Definitions: - "Package" refers to the collection of files distributed by the Copyright Holder, and derivatives of that collection of files created through textual modification. - "Standard Version" refers to such a Package if it has not been modified, or has been modified in accordance with the wishes of the Copyright Holder. - "Copyright Holder" is whoever is named in the copyright or copyrights for the package. - "You" is you, if you're thinking about copying or distributing this Package. - "Reasonable copying fee" is whatever you can justify on the basis of media cost, duplication charges, time of people involved, and so on. (You will not be required to justify it to the Copyright Holder, but only to the computing community at large as a market that must bear the fee.) - "Freely Available" means that no fee is charged for the item itself, though there may be fees involved in handling the item. It also means that recipients of the item may redistribute it under the same conditions they received it. 1. You may make and give away verbatim copies of the source form of the Standard Version of this Package without restriction, provided that you duplicate all of the original copyright notices and associated disclaimers. 2. You may apply bug fixes, portability fixes and other modifications derived from the Public Domain or from the Copyright Holder. A Package modified in such a way shall still be considered the Standard Version. 3. You may otherwise modify your copy of this Package in any way, provided that you insert a prominent notice in each changed file stating how and when you changed that file, and provided that you do at least ONE of the following: a) place your modifications in the Public Domain or otherwise make them Freely Available, such as by posting said modifications to Usenet or an equivalent medium, or placing the modifications on a major archive site such as ftp.uu.net, or by allowing the Copyright Holder to include your modifications in the Standard Version of the Package. b) use the modified Package only within your corporation or organization. c) rename any non-standard executables so the names do not conflict with standard executables, which must also be provided, and provide a separate manual page for each non-standard executable that clearly documents how it differs from the Standard Version. d) make other distribution arrangements with the Copyright Holder. 4. You may distribute the programs of this Package in object code or executable form, provided that you do at least ONE of the following: a) distribute a Standard Version of the executables and library files, together with instructions (in the manual page or equivalent) on where to get the Standard Version. b) accompany the distribution with the machine-readable source of the Package with your modifications. c) accompany any non-standard executables with their corresponding Standard Version executables, giving the non-standard executables non-standard names, and clearly documenting the differences in manual pages (or equivalent), together with instructions on where to get the Standard Version. d) make other distribution arrangements with the Copyright Holder. 5. You may charge a reasonable copying fee for any distribution of this Package. You may charge any fee you choose for support of this Package. You may not charge a fee for this Package itself. However, you may distribute this Package in aggregate with other (possibly commercial) programs as part of a larger (possibly commercial) software distribution provided that you do not advertise this Package as a product of your own. 6. The scripts and library files supplied as input to or produced as output from the programs of this Package do not automatically fall under the copyright of this Package, but belong to whomever generated them, and may be sold commercially, and may be aggregated with this Package. 7. C or perl subroutines supplied by you and linked into this Package shall not be considered part of this Package. 8. The name of the Copyright Holder may not be used to endorse or promote products derived from this software without specific prior written permission. 9. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. The End Twitter-API-1.0006/cpanfile000644 000765 000024 00000001765 14031400662 015447 0ustar00marcstaff000000 000000 requires perl => '5.14.1'; requires 'Carp'; requires 'Digest::SHA'; requires 'Encode'; requires 'HTML::Entities'; requires 'HTTP::Request::Common'; requires 'HTTP::Thin'; requires 'IO::Socket::SSL'; requires 'JSON::MaybeXS'; requires 'Module::Runtime'; requires 'Moo'; requires 'Moo::Role'; requires 'MooX::Aliases'; requires 'MooX::Traits'; requires 'namespace::clean'; requires 'Ref::Util'; requires 'Scalar::Util'; requires 'StackTrace::Auto'; requires 'Sub::Exporter::Progressive'; requires 'Throwable'; requires 'Time::HiRes'; requires 'Time::Local'; requires 'Try::Tiny'; requires 'URI'; requires 'URL::Encode'; requires 'WWW::OAuth' => '0.006'; recommends 'Cpanel::JSON::XS'; recommends 'WWW::Form::UrlEncoded::XS'; on test => sub { requires 'HTTP::Response'; requires 'HTTP::Status'; requires 'List::Util', '1.35'; # for function all added in 1.33 requires 'Test::Pod'; requires 'Test::Fatal'; requires 'Test::More'; requires 'Test::Spec'; requires 'Test::Warnings'; }; Twitter-API-1.0006/Changes000644 000765 000024 00000005664 14031400662 015240 0ustar00marcstaff000000 000000 Revision history for Twitter::API 1.0006 2021-04-01 10:23:58 PDT - rudimentary support for Twitter API V2 early access / beta 1.0005 2018-10-02 10:45:08 PDT - force fallback to WWW::Form::UrlEncoded::PP for tests - recommend appropriate XS modules 1.0004 2018-10-01 07:58:26 PDT - added invalidate_access_token method - added undeclared test dependencies - new_direct_messages_event now takes an optional event structure - updated links to Twitter documentation (thanks Rob Hoelz) 1.0003 2018-08-04 10:37:24 PDT - add support for new Direct Messages API endpoints (drumsoft) 1.0002 2018-05-25 18:07:26 PDT - fixed authentication error in RetryOnError (thanks Rob Hoelz) 1.0001 2018-03-30 20:19:15 PDT - added RateLimiting trait (thanks Rob Hoelz) - removed the alpha designation - it's production ready 0.0113 2017-07-18 16:20:14 PDT - make Dist::Zilla happy again - minor POD fixes 0.0112 2017-04-18 15:45:24 PDT - fixed typo: Undefined subroutine &Twitter::API::Error::is_hash - fixed documentation for OAuth methods, s/get_/oauth_/ 0.0111 2017-03-18 18:43:33 PDT - allow array ref for media_ids parameter to update 0.0110 2017-02-08 15:59:31 PST - remove experimental status; ready for production - drop dependency on Data::Visitor::Lite and Class::Load - Fix create_mute and destroy_mute required parameters - Add Twitter error codes 99, 135, and 136 for is_token_error 0.0109 2016-12-17 07:29:53 PST - Added Twitter API error code 215 to is_token_error 0.0108 2016-12-12 10:53:34 PST - make Migration trait optional - use WWW::Form::UrlEncoded::PP in tests to avoid a fall back warning 0.0107 2016-12-11 06:37:10 PST - moved and renamed API endpoint helper methods into Twitter::API::Role::RequestArgs for other implementers 0.0106 2016-12-10 17:31:25 PST - improved tests and fixes for oauth management methods - renamed s/Transition/Migration - fixed some POD links 0.0105 2016-12-09 22:16:05 PST - alpha release - updated examples for changes semantics in 0.0104 - added example/oauth_webapp.psgi - dependecy diet: replaced Net::OAuth with WWW::OAuth 0.0104 2016-12-08 15:10:34 PST (TRIAL RELEASE) - added Net::Twitter/::Lite migration support - renamed methods that exist in Net::Twitter with different semantics, primarily OAuth management methods - more documentation and tests 0.0103 2016-12-06 07:43:10 PST (TRIAL RELEASE) - fixed: RetryOnError failed to load - upload_media accepts positional params media or media_data 0.0102 2016-12-05 08:44:19 PST (TRIAL RELEASE) - fix invalidate_token - more documentation - reworked Twitter::API::Utils - timestamp methods now return time (epoch seconds), gmtime, and localtime, respectively 0.0101 2016-12-03 20:13:21 PST (TRIAL RELEASE) - documented trait ApiMethods - fixed broken ApiMethods methods and the test that wasn't catching them 0.0100 2016-12-02 13:32:27 PST (TRIAL RELEASE) - initial release Twitter-API-1.0006/MANIFEST000644 000765 000024 00000002304 14031400662 015062 0ustar00marcstaff000000 000000 # This file was automatically generated by Dist::Zilla::Plugin::Manifest v6.010. Build.PL Changes LICENSE MANIFEST META.json META.yml README cpanfile dist.ini examples/api.pl examples/app-auth.pl examples/decode-html-entities.pl examples/oauth_desktop.pl examples/oauth_web.psgi examples/retry-on-error.pl examples/upload.pl lib/Twitter/API.pm lib/Twitter/API/Context.pm lib/Twitter/API/Error.pm lib/Twitter/API/Role/RequestArgs.pm lib/Twitter/API/Trait/ApiMethods.pm lib/Twitter/API/Trait/AppAuth.pm lib/Twitter/API/Trait/DecodeHtmlEntities.pm lib/Twitter/API/Trait/Enchilada.pm lib/Twitter/API/Trait/Migration.pm lib/Twitter/API/Trait/NormalizeBooleans.pm lib/Twitter/API/Trait/RateLimiting.pm lib/Twitter/API/Trait/RetryOnError.pm lib/Twitter/API/Util.pm t/000_load.t t/author-pod-syntax.t t/base.t t/error.t t/oauth.t t/role/request-args.t t/trait/api-methods/direct-messages.t t/trait/api-methods/invalidate-access-token.t t/trait/api-methods/net-twitter-compatibility.t t/trait/api-methods/update.t t/trait/api-methods/upload-media.t t/trait/app-auth.t t/trait/decode-html-entities.t t/trait/migration.t t/trait/normalize-booleans.t t/trait/rate-limiting.t t/trait/retry-on-error.t t/url-for.t t/util.t weaver.ini Twitter-API-1.0006/t/000755 000765 000024 00000000000 14031400662 014175 5ustar00marcstaff000000 000000 Twitter-API-1.0006/README000644 000765 000024 00000021413 14031400662 014613 0ustar00marcstaff000000 000000 NAME Twitter::API - A Twitter REST API library for Perl VERSION version 1.0006 SYNOPSIS ### Common usage ### use Twitter::API; my $client = Twitter::API->new_with_traits( traits => 'Enchilada', consumer_key => $YOUR_CONSUMER_KEY, consumer_secret => $YOUR_CONSUMER_SECRET, access_token => $YOUR_ACCESS_TOKEN, access_token_secret => $YOUR_ACCESS_TOKEN_SECRET, ); my $me = $client->verify_credentials; my $user = $client->show_user('twitter'); # In list context, both the Twitter API result and a Twitter::API::Context # object are returned. my ($r, $context) = $client->home_timeline({ count => 200, trim_user => 1 }); my $remaning = $context->rate_limit_remaining; my $until = $context->rate_limit_reset; ### No frills ### my $client = Twitter::API->new( consumer_key => $YOUR_CONSUMER_KEY, consumer_secret => $YOUR_CONSUMER_SECRET, ); my $r = $client->get('account/verify_credentials', { -token => $an_access_token, -token_secret => $an_access_token_secret, }); ### Error handling ### use Twitter::API::Util 'is_twitter_api_error'; use Try::Tiny; try { my $r = $client->verify_credentials; } catch { die $_ unless is_twitter_api_error($_); # The error object includes plenty of information say $_->http_request->as_string; say $_->http_response->as_string; say 'No use retrying right away' if $_->is_permanent_error; if ( $_->is_token_error ) { say "There's something wrong with this token." } if ( $_->twitter_error_code == 326 ) { say "Oops! Twitter thinks you're spam bot!"; } }; DESCRIPTION Twitter::API provides an interface to the Twitter REST API for perl. Features: * full support for all Twitter REST API endpoints * not dependent on a new distribution for new endpoint support * optionally specify access tokens per API call * error handling via an exception object that captures the full request/response context * full support for OAuth handshake and Xauth authentication Additional features are available via optional traits: * convenient methods for API endpoints with simplified argument handling via ApiMethods * normalized booleans (Twitter likes 'true' and 'false', except when it doesn't) via NormalizeBooleans * automatic decoding of HTML entities via DecodeHtmlEntities * automatic retry on transient errors via RetryOnError * "the whole enchilada" combines all the above traits via Enchilada * app-only (OAuth2) support via AppAuth * automatic rate limiting via RateLimiting Some features are provided by separate distributions to avoid additional dependencies most users won't want or need: * async support via subclass Twitter::API::AnyEvent * inflate API call results to objects via Twitter::API::Trait::InflateObjects OVERVIEW Migration from Net::Twitter and Net::Twitter::Lite Migration support is included to assist users migrating from Net::Twitter and Net::Twitter::Lite. It will be removed from a future release. See Migration for details about migrating your existing Net::Twitter/::Lite applications. Normal usage Normally, you will construct a Twitter::API client with some traits, primarily ApiMethods. It provides methods for each known Twitter API endpoint. Documentation is provided for each of those methods in ApiMethods. See the list of traits in the "DESCRIPTION" and refer to the documentation for each. Minimalist usage Without any traits, Twitter::API provides access to API endpoints with the get and post methods described below, as well as methods for managing OAuth authentication. API results are simply perl data structures decoded from the JSON responses. Refer to the Twitter API Documentation for available endpoints, parameters, and responses. Twitter API V2 Beta Support Twitter intends to replace the current public API, version 1.1, with version 2. See https://developer.twitter.com/en/docs/twitter-api/early-access. You can use Twitter::API for the V2 beta with the minimalist usage described just above by passing values in the constructor for api_version and api_ext. my $client = Twitter::API->new_with_traits( api_version => '2', api_ext => '', %oauth_credentials, ); my $user = $client->get("users/by/username/$username"); More complete V2 support is anticipated in a future release. ATTRIBUTES consumer_key, consumer_secret Required. Every application has it's own application credentials. access_token, access_token_secret Optional. If provided, every API call will be authenticated with these user credentials. See AppAuth for app-only (OAuth2) support, which does not require user credentials. You can also pass options -token and -token_secret to specify user credentials on each API call. api_url Optional. Defaults to https://api.twitter.com. upload_url Optional. Defaults to https://upload.twitter.com. api_version Optional. Defaults to 1.1. api_ext Optional. Defaults to .json. agent Optional. Used for both the User-Agent and X-Twitter-Client identifiers. Defaults to Twitter-API-$VERSION (Perl). timeout Optional. Request timeout in seconds. Defaults to 10. METHODS get($url, [ \%args ]) Issues an HTTP GET request to Twitter. If $url is just a path part, e.g., account/verify_credentials, it will be expanded to a full URL by prepending the api_url, api_version and appending .json. A full URL can also be specified, e.g. https://api.twitter.com/1.1/account/verify_credentials.json. This should accommodate any new API endpoints Twitter adds without requiring an update to this module. post($url, [ \%args ]) See get above, for a discussion $url. For file upload, pass an array reference as described in https://metacpan.org/pod/distribution/HTTP-Message/lib/HTTP/Request/Common.pm#POST-url-Header-Value-...-Content-content. oauth_request_token([ \%args ]) This is the first step in the OAuth handshake. The only argument expected is callback, which defaults to oob for PIN based verification. Web applications will pass a callback URL. Returns a hashref that includes oauth_token and oauth_token_secret. See https://developer.twitter.com/en/docs/basics/authentication/api-reference/request_token. oauth_authentication_url(\%args) This is the second step in the OAuth handshake. The only required argument is oauth_token. Use the value returned by get_request_token. Optional arguments: force_login and screen_name to pre-fill Twitter's authentication form. See https://developer.twitter.com/en/docs/basics/authentication/api-reference/authenticate. oauth_authorization_url(\%args) Identical to oauth_authentication_url, but uses authorization flow, rather than authentication flow. See https://developer.twitter.com/en/docs/basics/authentication/api-reference/authorize. oauth_access_token(\%ags) This is the third and final step in the OAuth handshake. Pass the request token, request token_secret obtained in the get_request_token call, and either the PIN number if you used oob for the callback value in get_request_token or the verifier parameter returned in the web callback, as verfier. See https://developer.twitter.com/en/docs/basics/authentication/api-reference/access_token. xauth(\%args) Requires per application approval from Twitter. Pass username and password. SEE ALSO * API::Twitter - Twitter.com API Client * AnyEvent::Twitter::Stream - Receive Twitter streaming API in an event loop * AnyEvent::Twitter - A thin wrapper for Twitter API using OAuth * Mojo::WebService::Twitter - Simple Twitter API client * Net::Twitter - Twitter::API's predecessor (also Net::Twitter::Lite) AUTHOR Marc Mims COPYRIGHT AND LICENSE This software is copyright (c) 2015-2021 by Marc Mims. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. Twitter-API-1.0006/examples/000755 000765 000024 00000000000 14031400662 015550 5ustar00marcstaff000000 000000 Twitter-API-1.0006/META.yml000644 000765 000024 00000006173 14031400662 015212 0ustar00marcstaff000000 000000 --- abstract: 'A Twitter REST API library for Perl' author: - 'Marc Mims ' build_requires: HTTP::Response: '0' HTTP::Status: '0' List::Util: '1.35' Test::Fatal: '0' Test::More: '0' Test::Pod: '0' Test::Spec: '0' Test::Warnings: '0' configure_requires: Module::Build::Tiny: '0.034' dynamic_config: 0 generated_by: 'Dist::Zilla version 6.010, CPAN::Meta::Converter version 2.150001' license: perl meta-spec: url: http://module-build.sourceforge.net/META-spec-v1.4.html version: '1.4' name: Twitter-API no_index: directory: - eg - examples - inc - share - t - xt provides: Twitter::API: file: lib/Twitter/API.pm version: '1.0006' Twitter::API::Context: file: lib/Twitter/API/Context.pm version: '1.0006' Twitter::API::Error: file: lib/Twitter/API/Error.pm version: '1.0006' Twitter::API::Role::RequestArgs: file: lib/Twitter/API/Role/RequestArgs.pm version: '1.0006' Twitter::API::Trait::ApiMethods: file: lib/Twitter/API/Trait/ApiMethods.pm version: '1.0006' Twitter::API::Trait::AppAuth: file: lib/Twitter/API/Trait/AppAuth.pm version: '1.0006' Twitter::API::Trait::DecodeHtmlEntities: file: lib/Twitter/API/Trait/DecodeHtmlEntities.pm version: '1.0006' Twitter::API::Trait::Enchilada: file: lib/Twitter/API/Trait/Enchilada.pm version: '1.0006' Twitter::API::Trait::Migration: file: lib/Twitter/API/Trait/Migration.pm version: '1.0006' Twitter::API::Trait::NormalizeBooleans: file: lib/Twitter/API/Trait/NormalizeBooleans.pm version: '1.0006' Twitter::API::Trait::RateLimiting: file: lib/Twitter/API/Trait/RateLimiting.pm version: '1.0006' Twitter::API::Trait::RetryOnError: file: lib/Twitter/API/Trait/RetryOnError.pm version: '1.0006' Twitter::API::Util: file: lib/Twitter/API/Util.pm version: '1.0006' recommends: Cpanel::JSON::XS: '0' WWW::Form::UrlEncoded::XS: '0' requires: Carp: '0' Digest::SHA: '0' Encode: '0' HTML::Entities: '0' HTTP::Request::Common: '0' HTTP::Thin: '0' IO::Socket::SSL: '0' JSON::MaybeXS: '0' Module::Runtime: '0' Moo: '0' Moo::Role: '0' MooX::Aliases: '0' MooX::Traits: '0' Ref::Util: '0' Scalar::Util: '0' StackTrace::Auto: '0' Sub::Exporter::Progressive: '0' Throwable: '0' Time::HiRes: '0' Time::Local: '0' Try::Tiny: '0' URI: '0' URL::Encode: '0' WWW::OAuth: '0.006' namespace::clean: '0' perl: v5.14.1 resources: IRC: irc://irc.perl.org/#net-twitter bugtracker: https://github.com/semifor/Twitter-API/issues homepage: https://github.com/semifor/Twitter-API repository: https://github.com/semifor/Twitter-API.git version: '1.0006' x_contributors: - 'Andrew Grangaard ' - 'Dave Jacoby ' - 'Desmond Daignault ' - 'gregor herrmann ' - 'Haruka Kataoka ' - 'Karen Etheridge ' - 'kjt299 ' - 'Marc Mims ' - 'Mohammad S Anwar ' - 'Rob Hoelz ' x_serialization_backend: 'YAML::Tiny version 1.69' Twitter-API-1.0006/lib/000755 000765 000024 00000000000 14031400662 014500 5ustar00marcstaff000000 000000 Twitter-API-1.0006/weaver.ini000644 000765 000024 00000000547 14031400662 015732 0ustar00marcstaff000000 000000 [@CorePrep] [-SingleEncoding] [-Transformer] transformer = List [Name] [Region / buttons] [Version] [Region / prelude] [Generic / SYNOPSIS] [Generic / DESCRIPTION] [Generic / OVERVIEW] [Collect / ATTRIBUTES] command = attr [Collect / METHODS] command = method [Collect / FUNCTIONS] command = func [Leftovers] [Region / postlude] [Authors] [Legal] Twitter-API-1.0006/META.json000644 000765 000024 00000011514 14031400662 015355 0ustar00marcstaff000000 000000 { "abstract" : "A Twitter REST API library for Perl", "author" : [ "Marc Mims " ], "dynamic_config" : 0, "generated_by" : "Dist::Zilla version 6.010, CPAN::Meta::Converter version 2.150001", "license" : [ "perl_5" ], "meta-spec" : { "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", "version" : 2 }, "name" : "Twitter-API", "no_index" : { "directory" : [ "eg", "examples", "inc", "share", "t", "xt" ] }, "prereqs" : { "configure" : { "requires" : { "Module::Build::Tiny" : "0.034" } }, "develop" : { "requires" : { "Test::Pod" : "1.41" } }, "runtime" : { "recommends" : { "Cpanel::JSON::XS" : "0", "WWW::Form::UrlEncoded::XS" : "0" }, "requires" : { "Carp" : "0", "Digest::SHA" : "0", "Encode" : "0", "HTML::Entities" : "0", "HTTP::Request::Common" : "0", "HTTP::Thin" : "0", "IO::Socket::SSL" : "0", "JSON::MaybeXS" : "0", "Module::Runtime" : "0", "Moo" : "0", "Moo::Role" : "0", "MooX::Aliases" : "0", "MooX::Traits" : "0", "Ref::Util" : "0", "Scalar::Util" : "0", "StackTrace::Auto" : "0", "Sub::Exporter::Progressive" : "0", "Throwable" : "0", "Time::HiRes" : "0", "Time::Local" : "0", "Try::Tiny" : "0", "URI" : "0", "URL::Encode" : "0", "WWW::OAuth" : "0.006", "namespace::clean" : "0", "perl" : "v5.14.1" } }, "test" : { "requires" : { "HTTP::Response" : "0", "HTTP::Status" : "0", "List::Util" : "1.35", "Test::Fatal" : "0", "Test::More" : "0", "Test::Pod" : "0", "Test::Spec" : "0", "Test::Warnings" : "0" } } }, "provides" : { "Twitter::API" : { "file" : "lib/Twitter/API.pm", "version" : "1.0006" }, "Twitter::API::Context" : { "file" : "lib/Twitter/API/Context.pm", "version" : "1.0006" }, "Twitter::API::Error" : { "file" : "lib/Twitter/API/Error.pm", "version" : "1.0006" }, "Twitter::API::Role::RequestArgs" : { "file" : "lib/Twitter/API/Role/RequestArgs.pm", "version" : "1.0006" }, "Twitter::API::Trait::ApiMethods" : { "file" : "lib/Twitter/API/Trait/ApiMethods.pm", "version" : "1.0006" }, "Twitter::API::Trait::AppAuth" : { "file" : "lib/Twitter/API/Trait/AppAuth.pm", "version" : "1.0006" }, "Twitter::API::Trait::DecodeHtmlEntities" : { "file" : "lib/Twitter/API/Trait/DecodeHtmlEntities.pm", "version" : "1.0006" }, "Twitter::API::Trait::Enchilada" : { "file" : "lib/Twitter/API/Trait/Enchilada.pm", "version" : "1.0006" }, "Twitter::API::Trait::Migration" : { "file" : "lib/Twitter/API/Trait/Migration.pm", "version" : "1.0006" }, "Twitter::API::Trait::NormalizeBooleans" : { "file" : "lib/Twitter/API/Trait/NormalizeBooleans.pm", "version" : "1.0006" }, "Twitter::API::Trait::RateLimiting" : { "file" : "lib/Twitter/API/Trait/RateLimiting.pm", "version" : "1.0006" }, "Twitter::API::Trait::RetryOnError" : { "file" : "lib/Twitter/API/Trait/RetryOnError.pm", "version" : "1.0006" }, "Twitter::API::Util" : { "file" : "lib/Twitter/API/Util.pm", "version" : "1.0006" } }, "release_status" : "stable", "resources" : { "bugtracker" : { "web" : "https://github.com/semifor/Twitter-API/issues" }, "homepage" : "https://github.com/semifor/Twitter-API", "repository" : { "type" : "git", "url" : "https://github.com/semifor/Twitter-API.git", "web" : "https://github.com/semifor/Twitter-API" }, "x_IRC" : "irc://irc.perl.org/#net-twitter" }, "version" : "1.0006", "x_contributors" : [ "Andrew Grangaard ", "Dave Jacoby ", "Desmond Daignault ", "gregor herrmann ", "Haruka Kataoka ", "Karen Etheridge ", "kjt299 ", "Marc Mims ", "Mohammad S Anwar ", "Rob Hoelz " ], "x_serialization_backend" : "Cpanel::JSON::XS version 3.0213" } Twitter-API-1.0006/dist.ini000644 000765 000024 00000004215 14031400662 015400 0ustar00marcstaff000000 000000 author = Marc Mims license = Perl_5 copyright_year = 2015-2021 copyright_holder = Marc Mims ;; copied from @Milla and modified [NameFromDirectory] ; Make the git repo installable [Git::GatherDir] exclude_filename = Build.PL exclude_filename = META.json exclude_filename = LICENSE exclude_filename = README.md [CopyFilesFromBuild] copy = META.json copy = LICENSE copy = Build.PL ; should be after GatherDir ; Equivalent to Module::Install's version_from, license_from and author_from [VersionFromMainModule] [ReversionOnRelease] prompt = 1 ; after ReversionOnRelease for munge_files, before Git::Commit for after_release [NextRelease] format = %-6v %{yyyy-MM-dd HH:mm:ss VVV}d%{ (TRIAL RELEASE)}T [Git::Check] allow_dirty = dist.ini allow_dirty = Changes allow_dirty = META.json allow_dirty = README.md allow_dirty = Build.PL ; for $VERSION allow_dirty = lib/Twitter/API.pm ; Make Github center and front [GithubMeta] issues = 1 [MetaResources] x_IRC = irc://irc.perl.org/#net-twitter ; Set no_index to sensible directories [MetaNoIndex] directory = t directory = xt directory = inc directory = share directory = eg directory = examples [PkgVersion] [SurgicalPodWeaver] replacer = replace_with_comment post_code_replacer = replace_with_nothing [MetaProvides::Package] ; cpanfile -> META.json [Prereqs::FromCPANfile] [ModuleBuildTiny] [MetaJSON] ; x_contributors for MetaCPAN [Git::Contributors] ; standard stuff [PodSyntaxTests] [MetaYAML] [License] [ReadmeAnyFromPod] [ReadmeAnyFromPod/ReadmeTextInBuild] [ExtraTests] [ExecDir] dir = script [ShareDir] [Manifest] [ManifestSkip] [CheckChangesHasContent] [TestRelease] [ConfirmRelease] ;[ $ENV{FAKE_RELEASE} ? 'FakeRelease' : 'UploadToCPAN' ], [UploadToCPAN] [CopyFilesFromRelease] ;match = \.pm$ [Git::Commit] commit_msg = Release %v allow_dirty = dist.ini allow_dirty = Changes allow_dirty = META.json allow_dirty = README.md allow_dirty = Build.PL ; for $VERSION allow_dirty = lib/Twitter/API.pm ; .pm files copied back from Release ;allow_dirty_match = \.pm$ ; .pm files copied back from Release [Git::Tag] tag_format = %v tag_message = [Git::Push] remotes_must_exist = 0 Twitter-API-1.0006/lib/Twitter/000755 000765 000024 00000000000 14031400662 016142 5ustar00marcstaff000000 000000 Twitter-API-1.0006/lib/Twitter/API.pm000644 000765 000024 00000050505 14031400662 017116 0ustar00marcstaff000000 000000 package Twitter::API; # ABSTRACT: A Twitter REST API library for Perl our $VERSION = '1.0006'; use 5.14.1; use Moo; use Carp; use Digest::SHA; use Encode qw/encode_utf8/; use HTTP::Request; use HTTP::Request::Common qw/POST/; use JSON::MaybeXS (); use Module::Runtime qw/use_module/; use Ref::Util qw/is_arrayref is_ref/; use Try::Tiny; use Twitter::API::Context; use Twitter::API::Error; use URI; use URL::Encode (); use WWW::OAuth; use namespace::clean; with qw/MooX::Traits/; sub _trait_namespace { 'Twitter::API::Trait' } has api_version => ( is => 'ro', default => sub { '1.1' }, ); has api_ext => ( is => 'ro', default => sub { '.json' }, ); has [ qw/consumer_key consumer_secret/ ] => ( is => 'ro', required => 1, ); has [ qw/access_token access_token_secret/ ] => ( is => 'rw', predicate => 1, clearer => 1, ); # The secret is no good without the token. after clear_access_token => sub { shift->clear_access_token_secret; }; has api_url => ( is => 'ro', default => sub { 'https://api.twitter.com' }, ); has upload_url => ( is => 'ro', default => sub { 'https://upload.twitter.com' }, ); has agent => ( is => 'ro', default => sub { (join('/', __PACKAGE__, $VERSION) =~ s/::/-/gr) . ' (Perl)'; }, ); has timeout => ( is => 'ro', default => sub { 10 }, ); has default_headers => ( is => 'ro', default => sub { my $agent = shift->agent; { user_agent => $agent, x_twitter_client => $agent, x_twitter_client_version => $VERSION, x_twitter_client_url => 'https://github.com/semifor/Twitter-API', }; }, ); has user_agent => ( is => 'ro', lazy => 1, default => sub { my $self = shift; use_module 'HTTP::Thin'; HTTP::Thin->new( timeout => $self->timeout, agent => $self->agent, ); }, handles => { send_request => 'request', }, ); has json_parser => ( is => 'ro', lazy => 1, default => sub { JSON::MaybeXS->new(utf8 => 1) }, handles => { from_json => 'decode', to_json => 'encode', }, ); around BUILDARGS => sub { my ( $next, $class ) = splice @_, 0, 2; my $args = $class->$next(@_); croak 'use new_with_traits' if exists $args->{traits}; return $args; }; sub get { shift->request( get => @_ ) } sub post { shift->request( post => @_ ) } sub request { my $self = shift; my $c = Twitter::API::Context->new({ http_method => uc shift, url => shift, args => shift || {}, # shallow copy so we don't spoil the defaults headers => { %{ $self->default_headers }, accept => 'application/json', content_type => 'application/json;charset=utf8', }, extra_args => \@_, }); $self->extract_options($c); $self->preprocess_args($c); $self->preprocess_url($c); $self->prepare_request($c); $self->add_authorization($c); # Allow early exit for things like Twitter::API::AnyEvent $c->set_http_response($self->send_request($c) // return $c); $self->inflate_response($c); return wantarray ? ( $c->result, $c ) : $c->result; } sub extract_options { my ( $self, $c ) = @_; my $args = $c->args; for ( keys %$args ) { $c->set_option($1, delete $$args{$_}) if /^-(.+)/; } } sub preprocess_args { my ( $self, $c ) = @_; if ( $c->http_method eq 'GET' ) { $self->flatten_array_args($c->args); } # If any of the args are arrayrefs, we'll infer it's multipart/form-data $c->set_option(multipart_form_data => 1) if $c->http_method eq 'POST' && !!grep is_ref($_), values %{ $c->args }; } sub preprocess_url { my ( $self, $c ) = @_; my $url = $c->url; my $args = $c->args; $url =~ s[:(\w+)][delete $$args{$1} // croak "missing arg $1"]eg; $c->set_url($self->api_url_for($url)); } sub prepare_request { my ( $self, $c ) = @_; # possibly override Accept header $c->set_header(accept => $c->get_option('accept')) if $c->has_option('accept'); # dispatch on HTTP method my $http_method = $c->http_method; my $prepare_method = join '_', 'mk', lc($http_method), 'request'; my $dispatch = $self->can($prepare_method) || croak "unexpected HTTP method: $http_method"; my $req = $self->$dispatch($c); $c->set_http_request($req); } sub mk_get_request { shift->mk_simple_request(GET => @_); } sub mk_delete_request { shift->mk_simple_request(DELETE => @_); } sub mk_post_request { my ( $self, $c ) = @_; if ( $c->get_option('multipart_form_data') ) { return $self->mk_multipart_post($c); } if ( $c->has_option('to_json') ) { return $self->mk_json_post($c); } return $self->mk_form_urlencoded_post($c); } sub mk_multipart_post { my ( $self, $c ) = @_; $c->set_header(content_type => 'multipart/form-data;charset=utf-8'); POST $c->url, %{ $c->headers }, Content => [ map { is_ref($_) ? $_ : encode_utf8 $_ } %{ $c->args }, ]; } sub mk_json_post { my ( $self, $c ) = @_; POST $c->url, %{ $c->headers }, Content => $self->to_json($c->get_option('to_json')); } sub mk_form_urlencoded_post { my ( $self, $c ) = @_; $c->set_header( content_type => 'application/x-www-form-urlencoded;charset=utf-8'); POST $c->url, %{ $c->headers }, Content => $self->encode_args_string($c->args); } sub mk_simple_request { my ( $self, $http_method, $c ) = @_; my $uri = URI->new($c->url); if ( my $encoded = $self->encode_args_string($c->args) ) { $uri->query($encoded); } # HTTP::Message expects an arrayref, so transform my $headers = [ %{ $c->headers } ]; return HTTP::Request->new($http_method, $uri, $headers); } sub add_authorization { my ( $self, $c ) = @_; my $req = $c->http_request; my %cred = ( client_id => $self->consumer_key, client_secret => $self->consumer_secret, ); my %oauth; # only the token management methods set 'oauth_args' if ( my $opt = $c->get_option('oauth_args') ) { %oauth = %$opt; $cred{token} = delete $oauth{oauth_token}; $cred{token_secret} = delete $oauth{oauth_token_secret}; } else { # protected request; requires tokens $cred{token} = $c->get_option('token') // $self->access_token // croak 'requires an oauth token'; $cred{token_secret} = $c->get_option('token_secret') // $self->access_token_secret // croak 'requires an oauth token secret'; } WWW::OAuth->new(%cred)->authenticate($req, \%oauth); } around send_request => sub { my ( $orig, $self, $c ) = @_; $self->$orig($c->http_request); }; sub inflate_response { my ( $self, $c ) = @_; my $res = $c->http_response; my $data; try { if ( $res->content_type eq 'application/json' ) { $data = $self->from_json($res->content); } elsif ( ( $res->content_length // 0 ) == 0 ) { # E.g., 200 OK from media/metadata/create $data = ''; } elsif ( ($c->get_option('accept') // '') eq 'application/x-www-form-urlencoded' ) { # Twitter sets Content-Type: text/html for /oauth/request_token and # /oauth/access_token even though they return url encoded form # data. So we'll decode based on what we expected when we set the # Accept header. We don't want to assume form data when we didn't # request it, because sometimes twitter returns 200 OK with actual # HTML content. We don't want to decode and return that. It's an # error. We'll just leave $data unset if we don't have a reasonable # expectation of the content type. $data = URL::Encode::url_params_mixed($res->content, 1); } } catch { # Failed to decode the response body, synthesize an error response s/ at .* line \d+.*//s; # remove file/line number $res->code(500); $res->status($_); }; $c->set_result($data); return if defined($data) && $res->is_success; $self->process_error_response($c); } sub flatten_array_args { my ( $self, $args ) = @_; # transform arrays to comma delimited strings for my $k ( keys %$args ) { my $v = $$args{$k}; $$args{$k} = join ',' => @$v if is_arrayref($v); } } sub encode_args_string { my ( $self, $args ) = @_; my @pairs; for my $k ( sort keys %$args ) { push @pairs, join '=', map $self->uri_escape($_), $k, $$args{$k}; } join '&', @pairs; } sub uri_escape { URL::Encode::url_encode_utf8($_[1]) } sub process_error_response { Twitter::API::Error->throw({ context => $_[1] }); } sub api_url_for { my $self = shift; $self->_url_for($self->api_ext, $self->api_url, $self->api_version, @_); } sub upload_url_for { my $self = shift; $self->_url_for($self->api_ext, $self->upload_url, $self->api_version, @_); } sub oauth_url_for { my $self = shift; $self->_url_for('', $self->api_url, 'oauth', @_); } sub _url_for { my ( $self, $ext, @parts ) = @_; # If we already have a fully qualified URL, just return it return $_[-1] if $_[-1] =~ m(^https?://); my $url = join('/', @parts); $url .= $ext if $ext; return $url; } # OAuth handshake sub oauth_request_token { my $self = shift; my %args = @_ == 1 && is_ref($_[0]) ? %{ $_[0] } : @_; my %oauth_args; $oauth_args{oauth_callback} = delete $args{callback} // 'oob'; return $self->request(post => $self->oauth_url_for('request_token'), { -accept => 'application/x-www-form-urlencoded', -oauth_args => \%oauth_args, %args, # i.e. ( x_auth_access_type => 'read' ) }); } sub _auth_url { my ( $self, $endpoint ) = splice @_, 0, 2; my %args = @_ == 1 && is_ref($_[0]) ? %{ $_[0] } : @_; my $uri = URI->new($self->oauth_url_for($endpoint)); $uri->query_form(%args); return $uri; }; sub oauth_authentication_url { shift->_auth_url(authenticate => @_) } sub oauth_authorization_url { shift->_auth_url(authorize => @_) } sub oauth_access_token { my $self = shift; my %args = @_ == 1 && is_ref($_[0]) ? %{ $_[0] } : @_; # We'll take 'em with or without the oauth_ prefix :) my %oauth_args; @oauth_args{map s/^(?!oauth_)/oauth_/r, keys %args} = values %args; $self->request(post => $self->oauth_url_for('access_token'), { -accept => 'application/x-www-form-urlencoded', -oauth_args => \%oauth_args, }); } sub xauth { my ( $self, $username, $password ) = splice @_, 0, 3; my %extra_args = @_ == 1 && is_ref($_[0]) ? %{ $_[0] } : @_; $self->request(post => $self->oauth_url_for('access_token'), { -accept => 'application/x-www-form-urlencoded', -oauth_args => {}, x_auth_mode => 'client_auth', x_auth_password => $password, x_auth_username => $username, %extra_args, }); } 1; __END__ =pod =encoding UTF-8 =head1 NAME Twitter::API - A Twitter REST API library for Perl =for html Build Status =head1 VERSION version 1.0006 =head1 SYNOPSIS ### Common usage ### use Twitter::API; my $client = Twitter::API->new_with_traits( traits => 'Enchilada', consumer_key => $YOUR_CONSUMER_KEY, consumer_secret => $YOUR_CONSUMER_SECRET, access_token => $YOUR_ACCESS_TOKEN, access_token_secret => $YOUR_ACCESS_TOKEN_SECRET, ); my $me = $client->verify_credentials; my $user = $client->show_user('twitter'); # In list context, both the Twitter API result and a Twitter::API::Context # object are returned. my ($r, $context) = $client->home_timeline({ count => 200, trim_user => 1 }); my $remaning = $context->rate_limit_remaining; my $until = $context->rate_limit_reset; ### No frills ### my $client = Twitter::API->new( consumer_key => $YOUR_CONSUMER_KEY, consumer_secret => $YOUR_CONSUMER_SECRET, ); my $r = $client->get('account/verify_credentials', { -token => $an_access_token, -token_secret => $an_access_token_secret, }); ### Error handling ### use Twitter::API::Util 'is_twitter_api_error'; use Try::Tiny; try { my $r = $client->verify_credentials; } catch { die $_ unless is_twitter_api_error($_); # The error object includes plenty of information say $_->http_request->as_string; say $_->http_response->as_string; say 'No use retrying right away' if $_->is_permanent_error; if ( $_->is_token_error ) { say "There's something wrong with this token." } if ( $_->twitter_error_code == 326 ) { say "Oops! Twitter thinks you're spam bot!"; } }; =head1 DESCRIPTION Twitter::API provides an interface to the Twitter REST API for perl. Features: =over 4 =item * full support for all Twitter REST API endpoints =item * not dependent on a new distribution for new endpoint support =item * optionally specify access tokens per API call =item * error handling via an exception object that captures the full request/response context =item * full support for OAuth handshake and Xauth authentication =back Additional features are available via optional traits: =over 4 =item * convenient methods for API endpoints with simplified argument handling via L =item * normalized booleans (Twitter likes 'true' and 'false', except when it doesn't) via L =item * automatic decoding of HTML entities via L =item * automatic retry on transient errors via L =item * "the whole enchilada" combines all the above traits via L =item * app-only (OAuth2) support via L =item * automatic rate limiting via L =back Some features are provided by separate distributions to avoid additional dependencies most users won't want or need: =over 4 =item * async support via subclass L =item * inflate API call results to objects via L =back =head1 OVERVIEW =head2 Migration from Net::Twitter and Net::Twitter::Lite Migration support is included to assist users migrating from L and L. It will be removed from a future release. See L for details about migrating your existing Net::Twitter/::Lite applications. =head2 Normal usage Normally, you will construct a Twitter::API client with some traits, primarily B. It provides methods for each known Twitter API endpoint. Documentation is provided for each of those methods in L. See the list of traits in the L and refer to the documentation for each. =head2 Minimalist usage Without any traits, Twitter::API provides access to API endpoints with the L and L methods described below, as well as methods for managing OAuth authentication. API results are simply perl data structures decoded from the JSON responses. Refer to the L for available endpoints, parameters, and responses. =head2 Twitter API V2 Beta Support Twitter intends to replace the current public API, version 1.1, with version 2. See L. You can use Twitter::API for the V2 beta with the minimalist usage described just above by passing values in the constructor for C and C. my $client = Twitter::API->new_with_traits( api_version => '2', api_ext => '', %oauth_credentials, ); my $user = $client->get("users/by/username/$username"); More complete V2 support is anticipated in a future release. =head1 ATTRIBUTES =head2 consumer_key, consumer_secret Required. Every application has it's own application credentials. =head2 access_token, access_token_secret Optional. If provided, every API call will be authenticated with these user credentials. See L for app-only (OAuth2) support, which does not require user credentials. You can also pass options C<-token> and C<-token_secret> to specify user credentials on each API call. =head2 api_url Optional. Defaults to C. =head2 upload_url Optional. Defaults to C. =head2 api_version Optional. Defaults to C<1.1>. =head2 api_ext Optional. Defaults to C<.json>. =head2 agent Optional. Used for both the User-Agent and X-Twitter-Client identifiers. Defaults to C. =head2 timeout Optional. Request timeout in seconds. Defaults to C<10>. =head1 METHODS =head2 get($url, [ \%args ]) Issues an HTTP GET request to Twitter. If C<$url> is just a path part, e.g., C, it will be expanded to a full URL by prepending the C, C and appending C<.json>. A full URL can also be specified, e.g. C. This should accommodate any new API endpoints Twitter adds without requiring an update to this module. =head2 post($url, [ \%args ]) See C above, for a discussion C<$url>. For file upload, pass an array reference as described in L. =head2 oauth_request_token([ \%args ]) This is the first step in the OAuth handshake. The only argument expected is C, which defaults to C for PIN based verification. Web applications will pass a callback URL. Returns a hashref that includes C and C. See L. =head2 oauth_authentication_url(\%args) This is the second step in the OAuth handshake. The only required argument is C. Use the value returned by C. Optional arguments: C and C to pre-fill Twitter's authentication form. See L. =head2 oauth_authorization_url(\%args) Identical to C, but uses authorization flow, rather than authentication flow. See L. =head2 oauth_access_token(\%ags) This is the third and final step in the OAuth handshake. Pass the request C, request C obtained in the C call, and either the PIN number if you used C for the callback value in C or the C parameter returned in the web callback, as C. See L. =head2 xauth(\%args) Requires per application approval from Twitter. Pass C and C. =head1 SEE ALSO =over 4 =item * L - Twitter.com API Client =item * L - Receive Twitter streaming API in an event loop =item * L - A thin wrapper for Twitter API using OAuth =item * L - Simple Twitter API client =item * L - Twitter::API's predecessor (also L) =back =head1 AUTHOR Marc Mims =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2015-2021 by Marc Mims. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut Twitter-API-1.0006/lib/Twitter/API/000755 000765 000024 00000000000 14031400662 016553 5ustar00marcstaff000000 000000 Twitter-API-1.0006/lib/Twitter/API/Role/000755 000765 000024 00000000000 14031400662 017454 5ustar00marcstaff000000 000000 Twitter-API-1.0006/lib/Twitter/API/Error.pm000644 000765 000024 00000026543 14031400662 020214 0ustar00marcstaff000000 000000 package Twitter::API::Error; # ABSTRACT: Twitter API exception $Twitter::API::Error::VERSION = '1.0006'; use Moo; use Ref::Util qw/is_arrayref is_hashref/; use Try::Tiny; use namespace::clean; use overload '""' => sub { shift->error }; with qw/Throwable StackTrace::Auto/; #pod =method http_request #pod #pod Returns the L object used to make the Twitter API call. #pod #pod =method http_response #pod #pod Returns the L object for the API call. #pod #pod =method twitter_error #pod #pod Returns the inflated JSON error response from Twitter (if any). #pod #pod =cut has context => ( is => 'ro', required => 1, handles => { http_request => 'http_request', http_response => 'http_response', twitter_error => 'result', }, ); #pod =method stack_trace #pod #pod Returns a L object encapsulating the call stack so you can discover, where, in your application the error occurred. #pod #pod =method stack_frame #pod #pod Delegates to C<< stack_trace->frame >>. See L for details. #pod #pod =method next_stack_fram #pod #pod Delegates to C<< stack_trace->next_frame >>. See L for details. #pod #pod =cut has '+stack_trace' => ( handles => { stack_frame => 'frame', next_stack_frame => 'next_frame', }, ); #pod =method error #pod #pod Returns a reasonable string representation of the exception. If Twitter #pod returned error information in the form of a JSON body, it is mined for error #pod text. Otherwise, the HTTP response status line is used. The stack frame is #pod mined for the point in your application where the request initiated and #pod appended to the message. #pod #pod When used in a string context, C is called to stringify exception. #pod #pod =cut has error => ( is => 'lazy', ); sub _build_error { my $self = shift; my $error = $self->twitter_error_text || $self->http_response->status_line; my ( $location ) = $self->stack_frame(0)->as_string =~ /( at .*)/; return $error . ($location || ''); } sub twitter_error_text { my $self = shift; # Twitter does not return a consistent error structure, so we have to # try each known (or guessed) variant to find a suitable message... return '' unless $self->twitter_error; my $e = $self->twitter_error; return is_hashref($e) && ( # the newest variant: array of errors exists $e->{errors} && is_arrayref($e->{errors}) && exists $e->{errors}[0] && is_hashref($e->{errors}[0]) && exists $e->{errors}[0]{message} && $e->{errors}[0]{message} # it's single error variant || exists $e->{error} && is_hashref($e->{error}) && exists $e->{error}{message} && $e->{error}{message} # the original error structure (still applies to some endpoints) || exists $e->{error} && $e->{error} # or maybe it's not that deep (documentation would be helpful, here, # Twitter!) || exists $e->{message} && $e->{message} ) || ''; # punt } #pod =method twitter_error_code #pod #pod Returns the numeric error code returned by Twitter, or 0 if there is none. See #pod L for details. #pod #pod =cut sub twitter_error_code { my $self = shift; for ( $self->twitter_error ) { return is_hashref($_) && exists $_->{errors} && exists $_->{errors}[0] && exists $_->{errors}[0]{code} && $_->{errors}[0]{code} || 0; } } #pod =method is_token_error #pod #pod Returns true if the error represents a problem with the access token or its #pod Twitter account, rather than with the resource being accessed. #pod #pod Some Twitter error codes indicate a problem with authentication or the #pod token/secret used to make the API call. For example, the account has been #pod suspended or access to the application revoked by the user. Other error codes #pod indicate a problem with the resource requested. For example, the target account #pod no longer exists. #pod #pod is_token_error returns true for the following Twitter API errors: #pod #pod =for :list #pod * 32: Could not authenticate you #pod * 64: Your account is suspended and is not permitted to access this feature #pod * 88: Rate limit exceeded #pod * 89: Invalid or expired token #pod * 99: Unable to verify your credentials. #pod * 135: Could not authenticate you #pod * 136: You have been blocked from viewing this user's profile. #pod * 215: Bad authentication data #pod * 226: This request looks like it might be automated. To protect our users from #pod spam and other malicious activity, we can’t complete this action right now. #pod * 326: To protect our users from spam… #pod #pod For error 215, Twitter's API documentation says, "Typically sent with 1.1 #pod responses with HTTP code 400. The method requires authentication but it was not #pod presented or was wholly invalid." In practice, though, this error seems to be #pod spurious, and often succeeds if retried, even with the same tokens. #pod #pod The Twitter API documentation describes error code 226, but in practice, they #pod use code 326 instead, so we check for both. This error code means the account #pod the tokens belong to has been locked for spam like activity and can't be used #pod by the API until the user takes action to unlock their account. #pod #pod See Twitter's L documentation #pod for more information. #pod #pod =cut use constant TOKEN_ERRORS => (32, 64, 88, 89, 99, 135, 136, 215, 226, 326); my %token_errors = map +($_ => undef), TOKEN_ERRORS; sub is_token_error { exists $token_errors{shift->twitter_error_code}; } #pod =method http_response_code #pod #pod Delegates to C<< http_response->code >>. Returns the HTTP status code of the #pod response. #pod #pod =cut sub http_response_code { shift->http_response->code } #pod =method is_pemanent_error #pod #pod Returns true for HTTP status codes representing an error and with values less #pod than 500. Typically, retrying an API call with one of these statuses right away #pod will simply result in the same error, again. #pod #pod =cut sub is_permanent_error { shift->http_response_code < 500 } #pod =method is_temporary_error #pod #pod Returns true or HTTP status codes of 500 or greater. Often, these errors #pod indicate a transient condition. Retrying the API call right away may result in #pod success. See the L for #pod automatically retrying temporary errors. #pod #pod =cut sub is_temporary_error { !shift->is_permanent_error } 1; __END__ =pod =encoding UTF-8 =head1 NAME Twitter::API::Error - Twitter API exception =head1 VERSION version 1.0006 =head1 SYNOPSIS use Try::Tiny; use Twitter::API; use Twitter::API::Util 'is_twitter_api_error'; my $client = Twitter::API->new(%options); try { my $r = $client->get('account/verify_credentials'); } catch { die $_ unless is_twitter_api_error($_); warn "Twitter says: ", $_->twitter_error_text; }; =head1 DESCRIPTION Twitter::API dies, throwing a Twitter::API::Error exception when it receives an error. The error object contains information about the error so your code can decide how to respond to various error conditions. =head1 METHODS =head2 http_request Returns the L object used to make the Twitter API call. =head2 http_response Returns the L object for the API call. =head2 twitter_error Returns the inflated JSON error response from Twitter (if any). =head2 stack_trace Returns a L object encapsulating the call stack so you can discover, where, in your application the error occurred. =head2 stack_frame Delegates to C<< stack_trace->frame >>. See L for details. =head2 next_stack_fram Delegates to C<< stack_trace->next_frame >>. See L for details. =head2 error Returns a reasonable string representation of the exception. If Twitter returned error information in the form of a JSON body, it is mined for error text. Otherwise, the HTTP response status line is used. The stack frame is mined for the point in your application where the request initiated and appended to the message. When used in a string context, C is called to stringify exception. =head2 twitter_error_code Returns the numeric error code returned by Twitter, or 0 if there is none. See L for details. =head2 is_token_error Returns true if the error represents a problem with the access token or its Twitter account, rather than with the resource being accessed. Some Twitter error codes indicate a problem with authentication or the token/secret used to make the API call. For example, the account has been suspended or access to the application revoked by the user. Other error codes indicate a problem with the resource requested. For example, the target account no longer exists. is_token_error returns true for the following Twitter API errors: =over 4 =item * 32: Could not authenticate you =item * 64: Your account is suspended and is not permitted to access this feature =item * 88: Rate limit exceeded =item * 89: Invalid or expired token =item * 99: Unable to verify your credentials. =item * 135: Could not authenticate you =item * 136: You have been blocked from viewing this user's profile. =item * 215: Bad authentication data =item * 226: This request looks like it might be automated. To protect our users from spam and other malicious activity, we can’t complete this action right now. =item * 326: To protect our users from spam… =back For error 215, Twitter's API documentation says, "Typically sent with 1.1 responses with HTTP code 400. The method requires authentication but it was not presented or was wholly invalid." In practice, though, this error seems to be spurious, and often succeeds if retried, even with the same tokens. The Twitter API documentation describes error code 226, but in practice, they use code 326 instead, so we check for both. This error code means the account the tokens belong to has been locked for spam like activity and can't be used by the API until the user takes action to unlock their account. See Twitter's L documentation for more information. =head2 http_response_code Delegates to C<< http_response->code >>. Returns the HTTP status code of the response. =head2 is_pemanent_error Returns true for HTTP status codes representing an error and with values less than 500. Typically, retrying an API call with one of these statuses right away will simply result in the same error, again. =head2 is_temporary_error Returns true or HTTP status codes of 500 or greater. Often, these errors indicate a transient condition. Retrying the API call right away may result in success. See the L for automatically retrying temporary errors. =head1 AUTHOR Marc Mims =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2015-2021 by Marc Mims. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut Twitter-API-1.0006/lib/Twitter/API/Util.pm000644 000765 000024 00000005510 14031400662 020027 0ustar00marcstaff000000 000000 package Twitter::API::Util; # ABSTRACT: Utilities for working with the Twitter API $Twitter::API::Util::VERSION = '1.0006'; use 5.14.1; use warnings; use Carp qw/croak/; use Scalar::Util qw/blessed/; use Time::Local qw/timegm/; use Try::Tiny; use namespace::clean; use Sub::Exporter::Progressive -setup => { exports => [ qw/ is_twitter_api_error timestamp_to_gmtime timestamp_to_localtime timestamp_to_time /], }; sub is_twitter_api_error { blessed($_[0]) && $_[0]->isa('Twitter::API::Error'); } my %month; @month{qw/Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec/} = 0..11; sub _parse_ts { local $_ = shift() // return; # "Wed Jun 06 20:07:10 +0000 2012" my ( $M, $d, $h, $m, $s, $y ) = / ^(?:Sun|Mon|Tue|Wed|Thu|Fri|Sat) \ (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \ (\d\d)\ (\d\d):(\d\d):(\d\d) \ \+0000\ (\d{4})$ /x or return; return ( $s, $m, $h, $d, $month{$M}, $y - 1900 ); }; sub timestamp_to_gmtime { gmtime timestamp_to_time($_[0]) } sub timestamp_to_localtime { localtime timestamp_to_time($_[0]) } sub timestamp_to_time { my $ts = shift // return undef; my @t = _parse_ts($ts) or croak "invalid timestamp: $ts"; timegm @t; } 1; __END__ =pod =encoding UTF-8 =head1 NAME Twitter::API::Util - Utilities for working with the Twitter API =head1 VERSION version 1.0006 =head1 SYNOPSIS use Twitter::API::Util ':all'; # Given a timestamp in Twitter's text format: my $ts = $status->{created_at}; # "Wed Jun 06 20:07:10 +0000 2012" # Convert it UNIX epoch seconds (a Perl "time" value): my $time = timestamp_to_time($status->{created_at}); # Or a Perl localtime: my $utc = timestamp_to_timepiece($status->{created_at}); # Or a Perl gmtime: my $utc = timestamp_to_gmtime($status->{created_at}); # Check to see if an exception is a Twitter::API::Error if ( is_twitter_api_error($@) ) { warn "Twitter API error: " . $@->twitter_error_text; } =head1 DESCRIPTION Exports helpful utility functions. =head1 METHODS =head2 timestamp_to_gmtime Returns C from a Twitter timestamp string. See L for details. =head2 timestamp_to_localtime Returns C for a Twitter timestamp string. See L for details. =head2 timestamp_to_time Returns a UNIX epoch time for a Twitter timestamp string. See L for details. =head2 is_twitter_api_error Returns true if the scalar passed to it is a L. Otherwise, it returns false. =head1 AUTHOR Marc Mims =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2015-2021 by Marc Mims. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut Twitter-API-1.0006/lib/Twitter/API/Context.pm000644 000765 000024 00000005500 14031400662 020535 0ustar00marcstaff000000 000000 package Twitter::API::Context; # ABSTRACT: Encapsulated state for a request/response $Twitter::API::Context::VERSION = '1.0006'; use Moo; use namespace::clean; has [ qw/http_method args headers extra_args/ ] => ( is => 'ro', ); for my $attr ( qw/url result http_response http_request/ ) { has $attr => ( writer => "set_$attr", is => 'ro', ); } has options => ( is => 'ro', default => sub { {} }, ); sub get_option { $_[0]->options->{$_[1]} } sub has_option { exists $_[0]->options->{$_[1]} } sub set_option { $_[0]->options->{$_[1]} = $_[2] } sub delete_option { delete $_[0]->options->{$_[1]} } # private method my $limit = sub { my ( $self, $which ) = @_; my $res = $self->http_response; $res->header("X-Rate-Limit-$which"); }; sub rate_limit { shift->$limit('Limit') } sub rate_limit_remaining { shift->$limit('Remaining') } sub rate_limit_reset { shift->$limit('Reset') } sub set_header { my ( $self, $header, $value ) = @_; $self->headers->{$header} = $value; } 1; __END__ =pod =encoding UTF-8 =head1 NAME Twitter::API::Context - Encapsulated state for a request/response =head1 VERSION version 1.0006 =head1 SYNOPSIS my ( $r, $c ) = $client->verify_credentials; say sprintf '%d verify_credentials calls remaining unitl %s', $c->rate_limit_remaining, scalar localtime $c->rate_limit_reset; # output: 74 verify_credentials_calls remaining until Sat Dec 3 22:05:41 2016 =head1 DESCRIPTION The state for every API call is stored in a context object. It is automatically created when a request is initiated and is returned to the caller as the second value in list context. The context includes the L and L objects, a reference to the API return data, and accessor for rate limit information. A reference to the context is also included in a L exception. =head1 METHODS =head2 http_request Returns the L object for the API call. =head2 http_response Returns the L object for the API call. =head2 result Returns the result data for the API call. =head2 rate_limit Every API endpoint has a rate limit. This method returns the rate limit for the endpoint of the API call. See L for details. =head2 rate_limit_remaining Returns the number of API calls remaining for the endpoint in the current rate limit window. =head2 rate_limit_reset Returns the time of the next rate limit window in UTC epoch seconds. =head1 AUTHOR Marc Mims =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2015-2021 by Marc Mims. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut Twitter-API-1.0006/lib/Twitter/API/Trait/000755 000765 000024 00000000000 14031400662 017636 5ustar00marcstaff000000 000000 Twitter-API-1.0006/lib/Twitter/API/Trait/RetryOnError.pm000644 000765 000024 00000011140 14031400662 022605 0ustar00marcstaff000000 000000 package Twitter::API::Trait::RetryOnError; # ABSTRACT: Automatically retry API calls on error $Twitter::API::Trait::RetryOnError::VERSION = '1.0006'; use Moo::Role; use Time::HiRes; use namespace::clean; #pod =attr initial_retry_delay #pod #pod Amount of time to delay before the initial retry. Specified in fractional #pod seconds. Default: 0.25 (250ms). #pod #pod =cut has initial_retry_delay => ( is => 'rw', default => sub { 0.250 }, # 250 milliseconds ); #pod =attr max_retry_delay #pod #pod Maximum delay between retries, specified in fractional seconds. Default: 4.0. #pod #pod =cut has max_retry_delay => ( is => 'rw', default => sub { 4.0 }, # 4 seconds ); #pod =attr retry_delay_multiplier #pod #pod After the initial delay, the delay time is multiplied by this factor to #pod progressively back off allowing more time for the transient condition to #pod resolve. However, the delay never exceeds C. Default: 2.0. #pod #pod =cut has retry_delay_multiplier => ( is => 'rw', default => sub { 2 }, # double the prior delay ); #pod =attr max_retries #pod #pod Maximum number of times to retry on error. Default: 5. #pod #pod =cut has max_retries => ( is => 'rw', default => sub { 5 }, # 0 = try forever ); #pod =attr retry_delay_code #pod #pod A coderef, called to implement a delay. It takes a single parameter, the number #pod of seconds to delay. E.g., 0.25. The default implementation is simply: #pod #pod sub { Time::HiRes::sleep(shift) } #pod #pod =cut has retry_delay_code => ( is => 'rw', default => sub { sub { Time::HiRes::sleep(shift) }; }, ); around send_request => sub { my $orig = shift; my $self = shift; my ( $c ) = @_; my $msg = $c->http_request; my $is_oauth = ( $msg->header('authorization') // '' ) =~ /^OAuth /; my $delay = $self->initial_retry_delay; my $retries = $self->max_retries; my $res; while () { $res = $self->$orig(@_); # return on success or permanent error return $res if $res->code < 500 || $retries-- == 0; $self->retry_delay_code->($delay); $delay *= $self->retry_delay_multiplier; $delay = $self->max_retry_delay if $delay > $self->max_retry_delay; # If this is an OAuth request, we need a new Authorization header # (the nonce may be invalid, now). if ( $is_oauth ) { $self->add_authorization($c); } } $res; }; 1; __END__ =pod =encoding UTF-8 =head1 NAME Twitter::API::Trait::RetryOnError - Automatically retry API calls on error =head1 VERSION version 1.0006 =head1 SYNOPSIS use Twitter::API; my $client = Twitter::API->new_with_options( traits => [ qw/ApiMethods RetryOnError/ ], %other_optons ); my $statuses = $client->home_timeline; =head1 DESCRIPTION With this trait applied, Twitter::API automatically retries API calls that result in an HTTP status code of 500 or greater. These errors often indicate a temporary problem, either on Twitter's end, locally, or somewhere in between. By default, it retries up to 5 times. The initial retry is delayed by 250ms. Additional retries double the delay time until the maximum delay is reached (default: 4 seconds). Twitter::API throws a C exception when it receives a permanent error (HTTP status code below 500), or the maximum number of retries has been reached. For non-blocking applications, set a suitable C callback. This attribute can also be used to provided retry logging. =head1 ATTRIBUTES =head2 initial_retry_delay Amount of time to delay before the initial retry. Specified in fractional seconds. Default: 0.25 (250ms). =head2 max_retry_delay Maximum delay between retries, specified in fractional seconds. Default: 4.0. =head2 retry_delay_multiplier After the initial delay, the delay time is multiplied by this factor to progressively back off allowing more time for the transient condition to resolve. However, the delay never exceeds C. Default: 2.0. =head2 max_retries Maximum number of times to retry on error. Default: 5. =head2 retry_delay_code A coderef, called to implement a delay. It takes a single parameter, the number of seconds to delay. E.g., 0.25. The default implementation is simply: sub { Time::HiRes::sleep(shift) } =head1 AUTHOR Marc Mims =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2015-2021 by Marc Mims. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut Twitter-API-1.0006/lib/Twitter/API/Trait/RateLimiting.pm000644 000765 000024 00000004236 14031400662 022571 0ustar00marcstaff000000 000000 package Twitter::API::Trait::RateLimiting; # ABSTRACT: Automatically sleep as needed to handle rate limiting $Twitter::API::Trait::RateLimiting::VERSION = '1.0006'; use Moo::Role; use HTTP::Status qw(HTTP_TOO_MANY_REQUESTS); use namespace::clean; #pod =attr rate_limit_sleep_code #pod #pod A coderef, called to implement sleeping. It takes a single parameter - #pod the number of seconds to sleep. The default implementation is: #pod #pod sub { sleep shift } #pod #pod =cut has rate_limit_sleep_code => ( is => 'rw', default => sub { sub { sleep shift }; }, ); around send_request => sub { my $orig = shift; my $self = shift; my $res = $self->$orig(@_); while($res->code == HTTP_TOO_MANY_REQUESTS) { my $sleep_time = $res->header('x-rate-limit-reset') - time; $self->rate_limit_sleep_code->($sleep_time); $res = $self->$orig(@_); } return $res; }; 1; __END__ =pod =encoding UTF-8 =head1 NAME Twitter::API::Trait::RateLimiting - Automatically sleep as needed to handle rate limiting =head1 VERSION version 1.0006 =head1 SYNOPSIS use Twitter::API; my $client = Twitter::API->new_with_options( traits => [ qw/ApiMethods RateLimiting/ ], %other_options, ); # Use $client as normal =head1 DESCRIPTION Twitter's API implements rate limiting in a 15-minute window, and will serve up an HTTP 429 error if the rate limit is exceeded for a window. Applying this trait will give L the ability to automatically sleep as much as is needed and then retry a request instead of simply throwing an exception. =head1 ATTRIBUTES =head2 rate_limit_sleep_code A coderef, called to implement sleeping. It takes a single parameter - the number of seconds to sleep. The default implementation is: sub { sleep shift } =head1 SEE ALSO L =head1 AUTHOR Marc Mims =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2015-2021 by Marc Mims. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut Twitter-API-1.0006/lib/Twitter/API/Trait/ApiMethods.pm000644 000765 000024 00000161177 14031400662 022246 0ustar00marcstaff000000 000000 package Twitter::API::Trait::ApiMethods; # ABSTRACT: Convenient API Methods $Twitter::API::Trait::ApiMethods::VERSION = '1.0006'; use 5.14.1; use Carp; use Moo::Role; use MooX::Aliases; use Ref::Util qw/is_hashref is_arrayref/; use namespace::clean; requires 'request'; with 'Twitter::API::Role::RequestArgs'; #pod =method account_settings([ \%args ]) #pod #pod L #pod #pod =cut sub account_settings { shift->request(get => 'account/settings', @_); } #pod =method blocking([ \%args ]) #pod #pod Aliases: blocks_list #pod #pod L #pod #pod =cut sub blocking { shift->request(get => 'blocks/list', @_); } alias blocks_list => 'blocking'; #pod =method blocking_ids([ \%args ]) #pod #pod Aliases: blocks_ids #pod #pod L #pod #pod =cut sub blocking_ids { shift->request(get => 'blocks/ids', @_); } alias blocks_ids => 'blocking_ids'; #pod =method collection_entries([ $id, ][ \%args ]) #pod #pod L #pod #pod =cut sub collection_entries { shift->request_with_pos_args(id => get => 'collections/entries', @_); } #pod =method collections([ $screen_name | $user_id, ][ \%args ]) #pod #pod L #pod #pod =cut sub collections { shift->request_with_pos_args(':ID', get => 'collections/list', @_); } sub direct_messages { croak 'DEPRECATED - use direct_messages_events instead' } #pod =method favorites([ \%args ]) #pod #pod L #pod #pod =cut sub favorites { shift->request(get => 'favorites/list', @_); } #pod =method followers([ \%args ]) #pod #pod Aliases: followers_list #pod #pod L #pod #pod =cut sub followers { shift->request(get => 'followers/list', @_); } alias followers_list => 'followers'; #pod =method followers_ids([ $screen_name | $user_id, ][ \%args ]) #pod #pod L #pod #pod =cut sub followers_ids { shift->request_with_pos_args(':ID', get => 'followers/ids', @_); } #pod =method friends([ \%args ]) #pod #pod Aliases: friends_list #pod #pod L #pod #pod =cut sub friends { shift->request(get => 'friends/list', @_); } alias friends_list => 'friends'; #pod =method friends_ids([ \%args ]) #pod #pod Aliases: following_ids #pod #pod L #pod #pod =cut sub friends_ids { shift->request_with_id(get => 'friends/ids', @_); } alias following_ids => 'friends_ids'; #pod =method friendships_incoming([ \%args ]) #pod #pod Aliases: incoming_friendships #pod #pod L #pod #pod =cut sub friendships_incoming { shift->request(get => 'friendships/incoming', @_); } alias incoming_friendships => 'friendships_incoming'; #pod =method friendships_outgoing([ \%args ]) #pod #pod Aliases: outgoing_friendships #pod #pod L #pod #pod =cut sub friendships_outgoing { shift->request(get => 'friendships/outgoing', @_); } alias outgoing_friendships => 'friendships_outgoing'; #pod =method geo_id([ $place_id, ][ \%args ]) #pod #pod L #pod #pod =cut # NT incompatibility sub geo_id { shift->request_with_pos_args(place_id => get => 'geo/id/:place_id', @_); } #pod =method geo_search([ \%args ]) #pod #pod L #pod #pod =cut sub geo_search { shift->request(get => 'geo/search', @_); } #pod =method get_configuration([ \%args ]) #pod #pod L #pod #pod =cut sub get_configuration { shift->request(get => 'help/configuration', @_); } #pod =method get_languages([ \%args ]) #pod #pod L #pod #pod =cut sub get_languages { shift->request(get => 'help/languages', @_); } #pod =method get_list([ \%args ]) #pod #pod Aliases: show_list #pod #pod L #pod #pod =cut sub get_list { shift->request(get => 'lists/show', @_); } alias show_list => 'get_list'; #pod =method get_lists([ \%args ]) #pod #pod Aliases: list_lists, all_subscriptions #pod #pod L #pod #pod =cut sub get_lists { shift->request(get => 'lists/list', @_); } alias $_ => 'get_lists' for qw/list_lists all_subscriptions/; #pod =method get_privacy_policy([ \%args ]) #pod #pod L #pod #pod =cut sub get_privacy_policy { shift->request(get => 'help/privacy', @_); } #pod =method get_tos([ \%args ]) #pod #pod L #pod #pod =cut sub get_tos { shift->request(get => 'help/tos', @_); } #pod =method home_timeline([ \%args ]) #pod #pod L #pod #pod =cut sub home_timeline { shift->request(get => 'statuses/home_timeline', @_); } #pod =method list_members([ \%args ]) #pod #pod L #pod #pod =cut sub list_members { shift->request(get => 'lists/members', @_); } #pod =method list_memberships([ \%args ]) #pod #pod L #pod #pod =cut sub list_memberships { shift->request(get => 'lists/memberships', @_); } #pod =method list_ownerships([ \%args ]) #pod #pod L #pod #pod =cut sub list_ownerships { shift->request(get => 'lists/ownerships', @_); } #pod =method list_statuses([ \%args ]) #pod #pod L #pod #pod =cut sub list_statuses { shift->request(get => 'lists/statuses', @_); } #pod =method list_subscribers([ \%args ]) #pod #pod L #pod #pod =cut sub list_subscribers { shift->request(get => 'lists/subscribers', @_); } #pod =method list_subscriptions([ \%args ]) #pod #pod Aliases: subscriptions #pod #pod L #pod #pod =cut sub list_subscriptions { shift->request(get => 'lists/subscriptions', @_); } alias subscriptions => 'list_subscriptions'; #pod =method lookup_friendships([ \%args ]) #pod #pod L #pod #pod =cut sub lookup_friendships { shift->request(get => 'friendships/lookup', @_); } #pod =method lookup_statuses([ $id, ][ \%args ]) #pod #pod L #pod #pod =cut sub lookup_statuses { shift->request_with_pos_args(id => get => 'statuses/lookup', @_); } #pod =method lookup_users([ \%args ]) #pod #pod L #pod #pod =cut sub lookup_users { shift->request(get => 'users/lookup', @_); } #pod =method mentions([ \%args ]) #pod #pod Aliases: replies, mentions_timeline #pod #pod L #pod #pod =cut sub mentions { shift->request(get => 'statuses/mentions_timeline', @_); } alias $_ => 'mentions' for qw/replies mentions_timeline/; #pod =method mutes([ \%args ]) #pod #pod Aliases: muting_ids, muted_ids #pod #pod L #pod #pod =cut sub mutes { shift->request(get => 'mutes/users/ids', @_); } alias $_ => 'mutes' for qw/muting_ids muted_ids/; #pod =method muting([ \%args ]) #pod #pod Aliases: mutes_list #pod #pod L #pod #pod =cut sub muting { shift->request(get => 'mutes/users/list', @_); } alias mutes_list => 'muting'; #pod =method no_retweet_ids([ \%args ]) #pod #pod Aliases: no_retweets_ids #pod #pod L #pod #pod =cut sub no_retweet_ids { shift->request(get => 'friendships/no_retweets/ids', @_); } alias no_retweets_ids => 'no_retweet_ids'; #pod =method oembed([ \%args ]) #pod #pod L #pod #pod =cut sub oembed { shift->request(get => 'statuses/oembed', @_); } #pod =method profile_banner([ \%args ]) #pod #pod L #pod #pod =cut sub profile_banner { shift->request(get => 'users/profile_banner', @_); } #pod =method rate_limit_status([ \%args ]) #pod #pod L #pod #pod =cut sub rate_limit_status { shift->request(get => 'application/rate_limit_status', @_); } #pod =method retweeters_ids([ $id, ][ \%args ]) #pod #pod L #pod #pod =cut sub retweeters_ids { shift->request_with_pos_args(id => get => 'statuses/retweeters/ids', @_); } #pod =method retweets([ $id, ][ \%args ]) #pod #pod L #pod #pod =cut sub retweets { shift->request_with_pos_args(id => get => 'statuses/retweets/:id', @_); } #pod =method retweets_of_me([ \%args ]) #pod #pod Aliases: retweeted_of_me #pod #pod L #pod #pod =cut sub retweets_of_me { shift->request(get => 'statuses/retweets_of_me', @_); } alias retweeted_of_me => 'retweets_of_me'; #pod =method reverse_geocode([ $lat, [ $long, ]][ \%args ]) #pod #pod L #pod #pod =cut sub reverse_geocode { shift->request_with_pos_args([ qw/lat long/ ], get => 'geo/reverse_geocode', @_); } #pod =method saved_searches([ \%args ]) #pod #pod L #pod #pod =cut sub saved_searches { shift->request(get => 'saved_searches/list', @_); } #pod =method search([ $q, ][ \%args ]) #pod #pod L #pod #pod =cut sub search { shift->request_with_pos_args(q => get => 'search/tweets', @_); } #pod =method sent_direct_messages([ \%args ]) #pod #pod Aliases: direct_messages_sent #pod #pod L #pod #pod =cut sub sent_direct_messages { croak 'DEPRECATED - use direct_messages_events instead' } alias direct_messages_sent => 'sent_direct_messages'; sub show_direct_message { croak 'DEPRECATED - show_direct_messages_event instead' } #pod =method show_friendship([ \%args ]) #pod #pod Aliases: show_relationship #pod #pod L #pod #pod =cut sub show_friendship { shift->request(get => 'friendships/show', @_); } alias show_relationship => 'show_friendship'; #pod =method show_list_member([ \%args ]) #pod #pod Aliases: is_list_member #pod #pod L #pod #pod =cut sub show_list_member { shift->request(get => 'lists/members/show', @_); } alias is_list_member => 'show_list_member'; #pod =method show_list_subscriber([ \%args ]) #pod #pod Aliases: is_list_subscriber, is_subscriber_lists #pod #pod L #pod #pod =cut sub show_list_subscriber { shift->request(get => 'lists/subscribers/show', @_); } alias $_ => 'show_list_subscriber' for qw/is_list_subscriber is_subscriber_lists/; #pod =method show_saved_search([ $id, ][ \%args ]) #pod #pod L #pod #pod =cut sub show_saved_search { shift->request_with_pos_args(id => get => 'saved_searches/show/:id', @_); } #pod =method show_status([ $id, ][ \%args ]) #pod #pod L #pod #pod =cut sub show_status { shift->request_with_pos_args(id => get => 'statuses/show/:id', @_); } #pod =method show_user([ $screen_name | $user_id, ][ \%args ]) #pod #pod L #pod #pod =cut sub show_user { shift->request_with_pos_args(':ID', get => 'users/show', @_); } #pod =method suggestion_categories([ \%args ]) #pod #pod L #pod #pod =cut sub suggestion_categories { shift->request(get => 'users/suggestions', @_); } #pod =method trends_available([ \%args ]) #pod #pod L #pod #pod =cut sub trends_available { my ( $self, $args ) = @_; goto &trends_closest if exists $$args{lat} || exists $$args{long}; shift->request(get => 'trends/available', @_); } #pod =method trends_closest([ \%args ]) #pod #pod L #pod #pod =cut sub trends_closest { shift->request(get => 'trends/closest', @_); } #pod =method trends_place([ $id, ][ \%args ]) #pod #pod L #pod #pod =cut sub trends_place { shift->request_with_pos_args(id => get => 'trends/place', @_); } alias trends_location => 'trends_place'; #pod =method user_suggestions([ $slug, ][ \%args ]) #pod #pod L #pod #pod =cut # Net::Twitter compatibility - rename category to slug my $rename_category = sub { my $self = shift; my $args = is_hashref($_[-1]) ? pop : {}; $args->{slug} = delete $args->{category} if exists $args->{category}; return ( @_, $args ); }; sub user_suggestions { my $self = shift; $self->request_with_pos_args(slug => get => 'users/suggestions/:slug/members', $self->$rename_category(@_)); } alias follow_suggestions => 'user_suggestions'; #pod =method user_suggestions_for([ $slug, ][ \%args ]) #pod #pod Aliases: follow_suggestions #pod #pod L #pod #pod =cut sub user_suggestions_for { my $self = shift; $self->request_with_pos_args(slug => get => 'users/suggestions/:slug', $self->$rename_category(@_)); } alias follow_suggestions_for => 'user_suggestions_for'; #pod =method user_timeline([ $screen_name | $user_id, ][ \%args ]) #pod #pod L #pod #pod =cut sub user_timeline { shift->request_with_id(get => 'statuses/user_timeline', @_); } #pod =method users_search([ $q, ][ \%args ]) #pod #pod Aliases: find_people, search_users #pod #pod L #pod #pod =cut sub users_search { shift->request_with_pos_args(q => get => 'users/search', @_); } alias $_ => 'users_search' for qw/find_people search_users/; #pod =method verify_credentials([ \%args ]) #pod #pod L #pod #pod =cut sub verify_credentials { shift->request(get => 'account/verify_credentials', @_); } #pod =method add_collection_entry([ $id, [ $tweet_id, ]][ \%args ]) #pod #pod L #pod #pod =cut sub add_collection_entry { shift->request_with_pos_args([ qw/id tweet_id /], post => 'collections/entries/add', @_); } #pod =method add_list_member([ \%args ]) #pod #pod L #pod #pod =cut sub add_list_member { shift->request(post => 'lists/members/create', @_); } # deprecated: https://dev.twitter.com/rest/reference/post/geo/place sub add_place { shift->request_with_pos_args([ qw/name contained_within token lat long/ ], post => 'geo/place', @_); } #pod =method create_block([ $screen_name | $user_id, ][ \%args ]) #pod #pod L #pod #pod =cut sub create_block { shift->request_with_pos_args(':ID', post => 'blocks/create', @_); } #pod =method create_collection([ $name, ][ \%args ]) #pod #pod L #pod #pod =cut sub create_collection { shift->request_with_pos_args(name => post => 'collections/create', @_); } #pod =method create_favorite([ $id, ][ \%args ]) #pod #pod L #pod #pod =cut sub create_favorite { shift->request_with_pos_args(id => post => 'favorites/create', @_); } #pod =method create_friend([ $screen_name | $user_id, ][ \%args ]) #pod #pod Aliases: follow, follow_new, create_friendship #pod #pod L #pod #pod =cut sub create_friend { shift->request_with_pos_args(':ID', post => 'friendships/create', @_); } alias $_ => 'create_friend' for qw/follow follow_new create_friendship/; #pod =method create_list([ $name, ][ \%args ]) #pod #pod L #pod #pod =cut sub create_list { shift->request_with_pos_args(name => post => 'lists/create', @_); } #pod =method create_media_metadata([ \%args ]) #pod #pod L #pod #pod =cut # E.g.: # create_media_metadata({ media_id => $id, alt_text => { text => $text } }) sub create_media_metadata { my ( $self, $to_json ) = @_; croak 'expected a single hashref argument' unless @_ == 2 && is_hashref($_[1]); $self->request(post => 'media/metadata/create', { -to_json => $to_json, }); } #pod =method create_mute([ $screen_name | $user_id, ][ \%args ]) #pod #pod L #pod #pod Alias: mute #pod #pod =cut sub create_mute { shift->request_with_pos_args(':ID' => post => 'mutes/users/create', @_); } alias mute => 'create_mute'; #pod =method create_saved_search([ $query, ][ \%args ]) #pod #pod L #pod #pod =cut sub create_saved_search { shift->request_with_pos_args(query => post => 'saved_searches/create', @_); } #pod =method curate_collection([ \%args ]) #pod #pod L #pod #pod =cut sub curate_collection { my ( $self, $to_json ) = @_; croak 'unexpected extra args' if @_ > 2; $self->request(post => 'collections/entries/curate', { -to_json => $to_json, }); } #pod =method delete_list([ \%args ]) #pod #pod L #pod #pod =cut sub delete_list { shift->request(post => 'lists/destroy', @_); } #pod =method delete_list_member([ \%args ]) #pod #pod L #pod #pod =cut sub delete_list_member { shift->request(post => 'lists/members/destroy', @_); } alias remove_list_member => 'delete_list_member'; #pod =method destroy_block([ $screen_name | $user_id, ][ \%args ]) #pod #pod L #pod #pod =cut sub destroy_block { shift->request_with_pos_args(':ID', post => 'blocks/destroy', @_); } #pod =method destroy_collection([ $id, ][ \%args ]) #pod #pod L #pod #pod =cut sub destroy_collection { shift->request_with_pos_args(id => post => 'collections/destroy', @_); } sub destroy_direct_message { croak 'DEPRECATED - use destroy_direct_messages_event instead' } #pod =method destroy_favorite([ $id, ][ \%args ]) #pod #pod L #pod #pod =cut sub destroy_favorite { shift->request_with_pos_args(id => post => 'favorites/destroy', @_); } #pod =method destroy_friend([ $screen_name | $user_id, ][ \%args ]) #pod #pod Aliases: unfollow, destroy_friendship #pod #pod L #pod #pod =cut sub destroy_friend { shift->request_with_pos_args(':ID', post => 'friendships/destroy', @_); } alias $_ => 'destroy_friend' for qw/unfollow destroy_friendship/; #pod =method destroy_mute([ $screen_name | $user_id, ][ \%args ]) #pod #pod L #pod #pod Alias: unmute #pod #pod =cut sub destroy_mute { shift->request_with_pos_args(':ID' => post => 'mutes/users/destroy', @_); } alias unmute => 'destroy_mute'; #pod =method destroy_saved_search([ $id, ][ \%args ]) #pod #pod Aliases: delete_saved_search #pod #pod L #pod #pod =cut sub destroy_saved_search { shift->request_with_pos_args(id => post => 'saved_searches/destroy/:id', @_); } alias delete_saved_search => 'destroy_saved_search'; #pod =method destroy_status([ $id, ][ \%args ]) #pod #pod L #pod #pod =cut sub destroy_status { shift->request_with_pos_args(id => post => 'statuses/destroy/:id', @_); } #pod =method members_create_all([ \%args ]) #pod #pod Aliases: add_list_members #pod #pod L #pod #pod =cut sub members_create_all { shift->request(post => 'lists/members/create_all', @_); } alias add_list_members => 'members_create_all'; #pod =method members_destroy_all([ \%args ]) #pod #pod Aliases: remove_list_members #pod #pod L #pod #pod =cut sub members_destroy_all { shift->request(post => 'lists/members/destroy_all', @_); } alias remove_list_members => 'members_destroy_all'; #pod =method move_collection_entry([ $id, [ $tweet_id, [ $relative_to, ]]][ \%args ]) #pod #pod L #pod #pod =cut sub move_collection_entry { shift->request_with_pos_args([ qw/id tweet_id relative_to /], post => 'collections/entries/move', @_); } sub new_direct_message { croak 'DEPRECATED - use new_direct_messages_event instead' } #pod =method remove_collection_entry([ $id, [ $tweet_id, ]][ \%args ]) #pod #pod L #pod #pod =cut sub remove_collection_entry { shift->request_with_pos_args([ qw/id tweet_id/ ], post => 'collections/entries/remove', @_); } #pod =method remove_profile_banner([ \%args ]) #pod #pod L #pod #pod =cut sub remove_profile_banner { shift->request(post => 'account/remove_profile_banner', @_); } #pod =method report_spam([ \%args ]) #pod #pod L #pod #pod =cut sub report_spam { shift->request_with_id(post => 'users/report_spam', @_); } #pod =method retweet([ $id, ][ \%args ]) #pod #pod L #pod #pod =cut sub retweet { shift->request_with_pos_args(id => post => 'statuses/retweet/:id', @_); } #pod =method subscribe_list([ \%args ]) #pod #pod L #pod #pod =cut sub subscribe_list { shift->request(post => 'lists/subscribers/create', @_); } #pod =method unretweet([ $id, ][ \%args ]) #pod #pod L #pod #pod =cut sub unretweet { shift->request_with_pos_args(id => post => 'statuses/unretweet/:id', @_); } #pod =method unsubscribe_list([ \%args ]) #pod #pod L #pod #pod =cut sub unsubscribe_list { shift->request(post => 'lists/subscribers/destroy', @_); } #pod =method update([ $status, ][ \%args ]) #pod #pod L #pod #pod =cut sub update { my $self = shift; my ( $http_method, $path, $args, @rest ) = $self->normalize_pos_args(status => post => 'statuses/update', @_); $self->flatten_list_args(media_ids => $args); return $self->request($http_method, $path, $args, @rest); } #pod =method update_account_settings([ \%args ]) #pod #pod L #pod #pod =cut sub update_account_settings { shift->request(post => 'account/settings', @_); } #pod =method update_collection([ $id, ][ \%args ]) #pod #pod L #pod #pod =cut sub update_collection { shift->request_with_pos_args(id => post => 'collections/update', @_); } #pod =method update_friendship([ \%args ]) #pod #pod L #pod #pod =cut sub update_friendship { shift->request_with_id(post => 'friendships/update', @_); } #pod =method update_list([ \%args ]) #pod #pod L #pod #pod =cut sub update_list { shift->request(post => 'lists/update', @_); } #pod =method update_profile([ \%args ]) #pod #pod L #pod #pod =cut sub update_profile { shift->request(post => 'account/update_profile', @_); } #pod =method update_profile_background_image([ \%args ]) #pod #pod L #pod #pod =cut sub update_profile_background_image { shift->request(post => 'account/update_profile_background_image', @_); } #pod =method update_profile_banner([ $banner, ][ \%args ]) #pod #pod L #pod #pod =cut sub update_profile_banner { shift->request_with_pos_args(banner => post => 'account/update_profile_banner', @_); } #pod =method update_profile_image([ $image, ][ \%args ]) #pod #pod L #pod #pod =cut sub update_profile_image { shift->request_with_pos_args(image => post => 'account/update_profile_image', @_); } #pod =method upload_media([ $media, ][ \%args ]) #pod #pod Aliases: upload #pod #pod L #pod #pod =cut sub upload_media { my $self = shift; # Used to require media. Now requires media *or* media_data. # Handle either as a positional parameter, like we do with # screen_name or user_id on other methods. if ( @_ && !is_hashref($_[0]) ) { my $media = shift; my $key = is_arrayref($media) ? 'media' : 'media_data'; my $args = @_ && is_hashref($_[0]) ? pop : {}; $args->{$key} = $media; unshift @_, $args; } my $args = shift; $args->{-multipart_form_data} = 1; $self->flatten_list_args(additional_owners => $args); $self->request(post => $self->upload_url_for('media/upload'), $args, @_); } alias upload => 'upload_media'; #pod =method direct_messages_events([ \%args ]) #pod #pod L #pod #pod =cut sub direct_messages_events { shift->request(get => 'direct_messages/events/list', @_); } #pod =method show_direct_messages_event([ $id, ][ \%args ]) #pod #pod L #pod #pod =cut sub show_direct_messages_event { shift->request_with_pos_args(id => get => 'direct_messages/events/show', @_); } #pod =method destroy_direct_messages_event([ $id, ][ \%args ]) #pod #pod L #pod #pod =cut sub destroy_direct_messages_event { shift->request_with_pos_args(id => delete => 'direct_messages/events/destroy', @_); } #pod =method new_direct_messages_event([$text, $recipient_id ] | [ \%event ], [ \%args ]) #pod #pod For simple usage, pass text and recipient ID: #pod #pod $client->new_dirrect_messages_event($text, $recipient_id) #pod #pod For more complex messages, pass a full event structure, for example: #pod #pod $client->new_direct_massages_event({ #pod type => 'message_create', #pod message_create => { #pod target => { recipient_id => $user_id }, #pod message_data => { #pod text => $text, #pod attachment => { #pod type => 'media', #pod media => { id => $media->{id} }, #pod }, #pod }, #pod }, #pod }) #pod #pod L #pod #pod =cut sub new_direct_messages_event { my $self = shift; # The first argument is either an event hashref, or we'll create one with # the first two arguments: text and recipient_id. my $event = ref $_[0] ? shift : { type => 'message_create', message_create => { message_data => { text => shift }, target => { recipient_id => shift }, }, }; # only synthetic args are appropriate, here, e.g. # { -token => '...', -token_secret => '...' } my $args = shift // {}; $self->request(post => 'direct_messages/events/new', { -to_json => { event => $event }, %$args }); } #pod =method invalidate_access_token([ \%args ]) #pod #pod Calling this method has the same effect as a user revoking access to the #pod application via Twitter settings. The access token/secret pair will no longer #pod be valid. #pod #pod This method can be called with client that has been initialized with #pod C and C attributes, by passing C<-token> and #pod C<-token_secret> parameters, or by passing C and #pod C parameters. #pod #pod $client->invalidate_access_token; #pod $client->invalidate_access_token({ -token => $token, -token_secret => $secret }); #pod $client->invalidate_access_token({ #pod access_token => $token, #pod access_token_secret => $secret, #pod }); #pod #pod Twitter added this method to the API on 2018-09-20. #pod #pod See #pod L #pod #pod =cut # We've already used invalidate_token for oauth2/invalidate_otkon in # Trait::AppAuth, so we'll name this method invalidate_acccess_token to avoid # any conflict. sub invalidate_access_token { my ( $self, $args ) = @_; $args //= {}; # For consistency with Twitter::API calling conventions: # - accept -token/-token_secret synthetic arguments # - or use access_token/access_token_secret attributes # # Or, allow passing access_token/access_token secrets parameters as # specified in Twitter's API documentation. my $access_token = $$args{'-token'} // $self->access_token // ( $$args{'-token'} = delete $$args{access_token} ) // croak 'requires an oauth token'; my $access_token_secret = $$args{'-token_secret'} // $self->access_token_secret // ( $$args{'-token_secret'} = delete $$args{access_token_secret} ) // croak 'requires an oauth token secret'; return $self->request(post => 'oauth/invalidate_token', { access_token => $access_token, access_token_secret => $access_token_secret, %$args }); } 1; __END__ =pod =encoding UTF-8 =head1 NAME Twitter::API::Trait::ApiMethods - Convenient API Methods =head1 VERSION version 1.0006 =head1 DESCRIPTION This trait provides convenient methods for calling API endpoints. They are L compatible, with the same names and calling conventions. Refer to L for details about each method's parameters. These methods are simply shorthand forms of C and C. All methods can be called with a parameters hashref. It can be omitted for endpoints that do not require any parameters, such as C. For example, all of these calls are equivalent: $client->mentions; $client->mentions({}); $client->get('statuses/mentions_timeline'); $client->get('statuses/mentions_timeline', {}); Use the parameters hashref to pass optional parameters. For example, $client->mentions({ count => 200, trim_user=>'true' }); Some methods, with required parameters, can take positional parameters. For example, C requires a C parameter. These calls are equivalent: $client->place_id($place); $client->place_id({ place_id => $place }); When positional parameters are allowed, they must be specified in the correct order, but they don't all need to be specified. Those not specified positionally can be added to the parameters hashref. For example, these calls are equivalent: $client->add_collection_entry($id, $tweet_id); $client->add_collection_entry($id, { tweet_id => $tweet_id); $client->add_collection_entry({ id => $id, tweet_id => $tweet_id }); Many calls require a C or C. Where noted, you may pass either ID as the first positional parameter. Twitter::API will inspect the value. If it contains only digits, it will be considered a C. Otherwise, it will be considered a C. Best practice is to explicitly declare the ID type by passing it in the parameters hashref, because it is possible to for users to set their screen names to a string of digits, making the inferred ID ambiguous. These calls are equivalent: $client->create_block('realDonaldTrump'); $client->create_block({ screen_name => 'realDonaldTrump' }); Since all of these methods simple resolve to a C or C call, see the L for details about return values and error handling. =head1 METHODS =head2 account_settings([ \%args ]) L =head2 blocking([ \%args ]) Aliases: blocks_list L =head2 blocking_ids([ \%args ]) Aliases: blocks_ids L =head2 collection_entries([ $id, ][ \%args ]) L =head2 collections([ $screen_name | $user_id, ][ \%args ]) L =head2 favorites([ \%args ]) L =head2 followers([ \%args ]) Aliases: followers_list L =head2 followers_ids([ $screen_name | $user_id, ][ \%args ]) L =head2 friends([ \%args ]) Aliases: friends_list L =head2 friends_ids([ \%args ]) Aliases: following_ids L =head2 friendships_incoming([ \%args ]) Aliases: incoming_friendships L =head2 friendships_outgoing([ \%args ]) Aliases: outgoing_friendships L =head2 geo_id([ $place_id, ][ \%args ]) L =head2 geo_search([ \%args ]) L =head2 get_configuration([ \%args ]) L =head2 get_languages([ \%args ]) L =head2 get_list([ \%args ]) Aliases: show_list L =head2 get_lists([ \%args ]) Aliases: list_lists, all_subscriptions L =head2 get_privacy_policy([ \%args ]) L =head2 get_tos([ \%args ]) L =head2 home_timeline([ \%args ]) L =head2 list_members([ \%args ]) L =head2 list_memberships([ \%args ]) L =head2 list_ownerships([ \%args ]) L =head2 list_statuses([ \%args ]) L =head2 list_subscribers([ \%args ]) L =head2 list_subscriptions([ \%args ]) Aliases: subscriptions L =head2 lookup_friendships([ \%args ]) L =head2 lookup_statuses([ $id, ][ \%args ]) L =head2 lookup_users([ \%args ]) L =head2 mentions([ \%args ]) Aliases: replies, mentions_timeline L =head2 mutes([ \%args ]) Aliases: muting_ids, muted_ids L =head2 muting([ \%args ]) Aliases: mutes_list L =head2 no_retweet_ids([ \%args ]) Aliases: no_retweets_ids L =head2 oembed([ \%args ]) L =head2 profile_banner([ \%args ]) L =head2 rate_limit_status([ \%args ]) L =head2 retweeters_ids([ $id, ][ \%args ]) L =head2 retweets([ $id, ][ \%args ]) L =head2 retweets_of_me([ \%args ]) Aliases: retweeted_of_me L =head2 reverse_geocode([ $lat, [ $long, ]][ \%args ]) L =head2 saved_searches([ \%args ]) L =head2 search([ $q, ][ \%args ]) L =head2 sent_direct_messages([ \%args ]) Aliases: direct_messages_sent L =head2 show_friendship([ \%args ]) Aliases: show_relationship L =head2 show_list_member([ \%args ]) Aliases: is_list_member L =head2 show_list_subscriber([ \%args ]) Aliases: is_list_subscriber, is_subscriber_lists L =head2 show_saved_search([ $id, ][ \%args ]) L =head2 show_status([ $id, ][ \%args ]) L =head2 show_user([ $screen_name | $user_id, ][ \%args ]) L =head2 suggestion_categories([ \%args ]) L =head2 trends_available([ \%args ]) L =head2 trends_closest([ \%args ]) L =head2 trends_place([ $id, ][ \%args ]) L =head2 user_suggestions([ $slug, ][ \%args ]) L =head2 user_suggestions_for([ $slug, ][ \%args ]) Aliases: follow_suggestions L =head2 user_timeline([ $screen_name | $user_id, ][ \%args ]) L =head2 users_search([ $q, ][ \%args ]) Aliases: find_people, search_users L =head2 verify_credentials([ \%args ]) L =head2 add_collection_entry([ $id, [ $tweet_id, ]][ \%args ]) L =head2 add_list_member([ \%args ]) L =head2 create_block([ $screen_name | $user_id, ][ \%args ]) L =head2 create_collection([ $name, ][ \%args ]) L =head2 create_favorite([ $id, ][ \%args ]) L =head2 create_friend([ $screen_name | $user_id, ][ \%args ]) Aliases: follow, follow_new, create_friendship L =head2 create_list([ $name, ][ \%args ]) L =head2 create_media_metadata([ \%args ]) L =head2 create_mute([ $screen_name | $user_id, ][ \%args ]) L Alias: mute =head2 create_saved_search([ $query, ][ \%args ]) L =head2 curate_collection([ \%args ]) L =head2 delete_list([ \%args ]) L =head2 delete_list_member([ \%args ]) L =head2 destroy_block([ $screen_name | $user_id, ][ \%args ]) L =head2 destroy_collection([ $id, ][ \%args ]) L =head2 destroy_favorite([ $id, ][ \%args ]) L =head2 destroy_friend([ $screen_name | $user_id, ][ \%args ]) Aliases: unfollow, destroy_friendship L =head2 destroy_mute([ $screen_name | $user_id, ][ \%args ]) L Alias: unmute =head2 destroy_saved_search([ $id, ][ \%args ]) Aliases: delete_saved_search L =head2 destroy_status([ $id, ][ \%args ]) L =head2 members_create_all([ \%args ]) Aliases: add_list_members L =head2 members_destroy_all([ \%args ]) Aliases: remove_list_members L =head2 move_collection_entry([ $id, [ $tweet_id, [ $relative_to, ]]][ \%args ]) L =head2 remove_collection_entry([ $id, [ $tweet_id, ]][ \%args ]) L =head2 remove_profile_banner([ \%args ]) L =head2 report_spam([ \%args ]) L =head2 retweet([ $id, ][ \%args ]) L =head2 subscribe_list([ \%args ]) L =head2 unretweet([ $id, ][ \%args ]) L =head2 unsubscribe_list([ \%args ]) L =head2 update([ $status, ][ \%args ]) L =head2 update_account_settings([ \%args ]) L =head2 update_collection([ $id, ][ \%args ]) L =head2 update_friendship([ \%args ]) L =head2 update_list([ \%args ]) L =head2 update_profile([ \%args ]) L =head2 update_profile_background_image([ \%args ]) L =head2 update_profile_banner([ $banner, ][ \%args ]) L =head2 update_profile_image([ $image, ][ \%args ]) L =head2 upload_media([ $media, ][ \%args ]) Aliases: upload L =head2 direct_messages_events([ \%args ]) L =head2 show_direct_messages_event([ $id, ][ \%args ]) L =head2 destroy_direct_messages_event([ $id, ][ \%args ]) L =head2 new_direct_messages_event([$text, $recipient_id ] | [ \%event ], [ \%args ]) For simple usage, pass text and recipient ID: $client->new_dirrect_messages_event($text, $recipient_id) For more complex messages, pass a full event structure, for example: $client->new_direct_massages_event({ type => 'message_create', message_create => { target => { recipient_id => $user_id }, message_data => { text => $text, attachment => { type => 'media', media => { id => $media->{id} }, }, }, }, }) L =head2 invalidate_access_token([ \%args ]) Calling this method has the same effect as a user revoking access to the application via Twitter settings. The access token/secret pair will no longer be valid. This method can be called with client that has been initialized with C and C attributes, by passing C<-token> and C<-token_secret> parameters, or by passing C and C parameters. $client->invalidate_access_token; $client->invalidate_access_token({ -token => $token, -token_secret => $secret }); $client->invalidate_access_token({ access_token => $token, access_token_secret => $secret, }); Twitter added this method to the API on 2018-09-20. See L =head1 AUTHOR Marc Mims =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2015-2021 by Marc Mims. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut Twitter-API-1.0006/lib/Twitter/API/Trait/NormalizeBooleans.pm000644 000765 000024 00000006217 14031400662 023625 0ustar00marcstaff000000 000000 package Twitter::API::Trait::NormalizeBooleans; # ABSTRACT: Normalize Booleans $Twitter::API::Trait::NormalizeBooleans::VERSION = '1.0006'; use 5.14.1; use Moo::Role; use namespace::clean; requires 'preprocess_args'; around preprocess_args => sub { my ( $next, $self, $c ) = @_; $self->$next($c); $self->normalize_bools($c->args); }; # Twitter usually accepts 1, 't', 'true', or false for booleans, but they # prefer 'true' or 'false'. In some cases, like include_email, they only accept # 'true'. So, we normalize these options. my @normal_bools = qw/ contributor_details display_coordinates exclude_replies hide_media hide_thread hide_tweet include_email include_entities include_my_tweet include_rts include_user_entities map omit_script possibly_sensitive reverse trim_user /; # Workaround Twitter bug: any value passed for these options are treated as # true. The only way to get 'false' is to not pass the skip_user at all. So, # we strip these boolean args if their values are false. my @true_only_bools = qw/skip_status skip_user/; my %is_bool = map { $_ => undef } @normal_bools, @true_only_bools; my %is_true_only_bool = map { $_ => undef } @true_only_bools; sub is_bool { exists $is_bool{$_[1]} } sub is_true_only_bool { exists $is_true_only_bool{$_[1]} } sub normalize_bools { my ( $self, $args ) = @_; # Twitter prefers 'true' or 'false' (requires it in some cases). for my $k ( keys %$args ) { next unless $self->is_bool($k); $args->{$k} = $args->{$k} ? 'true' : 'false'; delete $args->{$k} if $self->is_true_only_bool($k) && $args->{$k} eq 'false'; } } 1; __END__ =pod =encoding UTF-8 =head1 NAME Twitter::API::Trait::NormalizeBooleans - Normalize Booleans =head1 VERSION version 1.0006 =head1 SYNOPSIS use Twitter::API; my $client = Twitter::API->new_with_traits( traits => [ qw/ApiMethods NormalizeBooleans/ ], %other_new_options ); my ( $r, $c ) = $client->home_timeline({ trim_user => 1 }); say $c->http_request->uri; # output: https://api.twitter.com/1.1/statuses/home_timeline.json?trim_user=true =head1 DESCRIPTION Twitter has a strange concept of boolean values. Usually, the API accepts C, C, or C<1> for true. Sometimes it accepts C, C, or C<0> for false. But then you have strange cases like the C parameter used for authorized applications by the C endpoint. It only accepts C. Worse, for some boolean values, passing C, C, or C<0> all work as if you passed C. For those values, false means not including the parameter at all. So, this trait attempts to normalize booleans by transforming any perl truthy value to the Twitter API's preference, C. It transforms falsey values to C. And then it removes false parameters that the API always treats as true. You're welcome. =head1 AUTHOR Marc Mims =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2015-2021 by Marc Mims. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut Twitter-API-1.0006/lib/Twitter/API/Trait/AppAuth.pm000644 000765 000024 00000010355 14031400662 021542 0ustar00marcstaff000000 000000 package Twitter::API::Trait::AppAuth; # ABSTRACT: App-only (OAuth2) Authentication $Twitter::API::Trait::AppAuth::VERSION = '1.0006'; use Moo::Role; use Carp; use URL::Encode qw/url_encode url_decode/; use namespace::clean; requires qw/ _url_for access_token add_authorization api_url consumer_key consumer_secret request /; # private methods sub oauth2_url_for { my $self = shift; $self->_url_for('', $self->api_url, 'oauth2', @_); } my $add_consumer_auth_header = sub { my ( $self, $req ) = @_; $req->headers->authorization_basic( $self->consumer_key, $self->consumer_secret); }; # public methods #pod =method oauth2_token #pod #pod Call the C endpoint to get a bearer token. The token is not #pod stored in Twitter::API's state. If you want that, set the C #pod attribute with the returned token. #pod #pod See L for details. #pod #pod =cut sub oauth2_token { my $self = shift; my ( $r, $c ) = $self->request(post => $self->oauth2_url_for('token'), { -add_consumer_auth_header => 1, grant_type => 'client_credentials', }); # In their wisdom, Twitter sends us a URL encoded token. We need to decode # it, so if/when we call invalidate_token, and properly URL encode our # parameters, we don't end up with a double-encoded token. my $token = url_decode $$r{access_token}; return wantarray ? ( $token, $c ) : $token; } #pod =method invalidate_token($token) #pod #pod Calls the C endpoint to revoke a token. See #pod L for #pod details. #pod #pod =cut sub invalidate_token { my ( $self, $token ) = @_; my ( $r, $c ) = $self->request( post =>$self->oauth2_url_for('invalidate_token'), { -add_consumer_auth_header => 1, access_token => $token, }); my $token_returned = url_decode $$r{access_token}; return wantarray ? ( $token_returned, $c ) : $token_returned; } # request chain modifiers around add_authorization => sub { shift; # we're overriding the base, so we won't call it my ( $self, $c ) = @_; my $req = $c->http_request; if ( $c->get_option('add_consumer_auth_header') ) { $self->$add_consumer_auth_header($req); } else { my $token = $c->get_option('token') // $self->access_token // return; $req->header(authorization => join ' ', Bearer => url_encode($token)); } }; 1; __END__ =pod =encoding UTF-8 =head1 NAME Twitter::API::Trait::AppAuth - App-only (OAuth2) Authentication =head1 VERSION version 1.0006 =head1 SYNOPSIS use Twitter::API; my $client = Twitter::API->new_with_traits( traits => [ qw/ApiMethods AppAuth/ ]); my $r = $client->oauth2_token; # return value is hash ref: # { token_type => 'bearer', access_token => 'AA...' } my $token = $r->{access_token}; # you can use the token explicitly with the -token argument: my $user = $client->show_user('twitter_api', { -token => $token }); # or you can set the access_token attribute to use it implicitly $client->access_token($token); my $user = $client->show_user('twitterapi'); # to revoke a token $client->invalidate_token($token); # if you revoke the token stored in the access_token attribute, clear it: $client->clear_access_token; =head1 METHODS =head2 oauth2_token Call the C endpoint to get a bearer token. The token is not stored in Twitter::API's state. If you want that, set the C attribute with the returned token. See L for details. =head2 invalidate_token($token) Calls the C endpoint to revoke a token. See L for details. =head1 AUTHOR Marc Mims =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2015-2021 by Marc Mims. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut Twitter-API-1.0006/lib/Twitter/API/Trait/Migration.pm000644 000765 000024 00000024727 14031400662 022141 0ustar00marcstaff000000 000000 package Twitter::API::Trait::Migration; # ABSTRACT: Migration support Net::Twitter/::Lite users $Twitter::API::Trait::Migration::VERSION = '1.0006'; use 5.14.1; use Carp; use Moo::Role; use Ref::Util qw/is_ref/; use namespace::clean; has [ qw/request_token request_token_secret/ ] => ( is => 'rw', predicate => 1, clearer => 1, ); has wrap_result => ( is => 'ro', default => sub { 0 }, ); around request => sub { my ( $next, $self ) = splice @_, 0, 2; my ( $r, $c ) = $self->$next(@_); # Early exit? Actually just a context object; return it. return $r unless defined $c; # Net::Twitter/::Lite migraton support if ( $self->wrap_result ) { unless ( $ENV{TWITTER_API_NO_MIGRATION_WARNINGS} ) { carp 'wrap_result is enabled. It will be removed in a future ' .'version. See Twitter::API::Trait::Migration'; } return $c; } return wantarray ? ( $c->result, $c ) : $c->result; }; sub ua { shift->user_agent(@_) } sub _get_auth_url { my ( $self, $endpoint ) = splice @_, 0, 2; my %args = @_ == 1 && is_ref($_[0]) ? %{ $_[0] } : @_; my $callback = delete $args{callback} // 'oob'; my ( $r, $c ) = $self->oauth_request_token(callback => $callback); $self->request_token($$r{oauth_token}); $self->request_token_secret($$r{oauth_token_secret}); my $uri = $self->_auth_url($endpoint, oauth_token => $$r{oauth_token}, %args ); return wantarray ? ( $uri, $c ) : $uri; } sub get_authentication_url { shift->_get_auth_url(authenticate => @_) } sub get_authorization_url { shift->_get_auth_url(authorize => @_) } sub request_access_token { my ( $self, %params ) = @_; # request_access_token is defined in both Net::Twitter's OAuth and AppAuth # traits. We need to know which one to call, here. if ( $self->does('Twitter::API::Trait::AppAuth') ) { return $self->access_token($self->oauth2_token(@_)); } my ( $r, $c ) = $self->oauth_access_token({ token => $self->request_token, token_secret => $self->request_token_secret, %params, # verifier => $verifier }); # Net::Twitter stores access tokens in the client instance $self->access_token($$r{oauth_token}); $self->access_token_secret($$r{oauth_token_secret}); $self->clear_request_token; $self->clear_request_token_secret; return ( @{$r}{qw/oauth_token oauth_token_secret user_id screen_name/}, $c, ); } for my $method ( qw/ get_authentication_url get_authorization_url request_access_token ua /) { around $method => sub { my ( $next, $self ) = splice @_, 0, 2; unless ( $ENV{TWITTER_API_NO_MIGRATION_WARNINGS} ) { carp $method.' will be removed in a future release. ' .'Please see Twitter::API::Trait::Migration'; } $self->$next(@_); }; } 1; __END__ =pod =encoding UTF-8 =head1 NAME Twitter::API::Trait::Migration - Migration support Net::Twitter/::Lite users =head1 VERSION version 1.0006 =head1 DESCRIPTION Twitter::API is a rewrite of L. It's leaner, lighter, and has faster—fewer dependencies, and less baggage. This trait helps Net::Twitter and Net::Twitter::Lite users migrate to Twitter::API by providing Net::Twitter compatible behavior where possible and warning politely where code should be changed. =head1 Migrating from Net::Twitter Twitter::API requires a minimum perl version of 5.14.1. Make sure you have that. Just change your constructor call: my $client = Net::Twitter->new( traits => [ qw/API::RESTv1_1 OAuth RetryOnError/ ], consumer_key => $key, consumer_secret => $secret, access_token => $token, access_token_secret => $token_secret, ); Becomes: my $client = Twitter::API->new_with_traits( traits => [ qw/Migration ApiMethods RetryOnError/ ], consumer_key => $key, consumer_secret => $secret, access_token => $token, access_token_secret => $token_secret, ); Differences: =over 4 =item * replace C with C =item * replace trait C with C =item * drop trait C, Twitter::API's core includes it =item * add the Migration trait so Twitter::API will handle oauth key management in a Net::Twitter compatible way and warn =back =head2 Traits Twitter::API supports the following traits: =over 4 =item * L =item * L =item * L =item * L =item * L =item * L =back Bis a direct replacement for Net::Twitter's API::RESTv1_1 trait. Net::Twitter's B trait will be released as a separate distribution to minimize Twitter::API's dependencies. If you are using the Net::Twitter's B trait, Twitter::API provides a better way to access the what it provides. In list context, API calls return both the API call results and a L object that provides the same accessors and attributes B provided, including the B accessor. So, if you had: my $r = $client->home_timeline; $r->result; $r->rate_limit_remaining; You can change that to: my ( $result, $context ) = $client->home_timeline; $result; $context->rate_limit_remaining; Or for the smallest change to your code: my ( undef, $r ) = $client->home_timeline; $r->result; i # same as before $r->rate_limit_remaning; # same as before However, there is migration support for B. Call the constructor with option C<< wrap_result => 1 >> and Twitter::API will return the context object, only, for API calls. This should give you the same behavior you had with B while you modify your code. Twitter::API will warn when this option is used. You may disale warnings with C<$ENV{TWITTER_API_NO_MIGRATION_WARNINGS} = 1>. If you are using any other Net::Twitter traits, please contact the author of Twitter::API. Additional traits may be added to Twitter::API or released as separate distributions. If you are using C<< decode_html_entities => 1 >> in Net::Twitter, drop that option and add trait B. Traits B and B provide the same functionality in Twitter::API as their Net::Twitter counterparts. So, no changes required, there, if you're using them. (Although there is a change to one of B's methods. See the L discussion.) NormalizeBooleans is something you'll probably want. See the L documentation. Enchilda just bundles ApiMethods, NormalizeBooleans, RetryOnError, and DecodeHtmlEntities. =head2 Other constructor options Drop option C<< ssl => 1 >>. It is no longer necessary. By default, all connections use SSL. If you are setting B and/or B to customize the user agent, just construct your own pass it to new with C<< user_agent => $custom_user_agent >>. If you are using B to set a custom user agent, the attribute name has changed to B. So, pass it to new with C<< user_agent => $custom_user_agent >>. By default, Twitter::API uses L as its user agent. You should be able to use any user agent you like, as long as it has a B method that takes an L and returns an L. If you used B, B, B, or B, see L and L. If all you're after is a custom User-Agent header, just pass C<< agent => $user_agent_string >>. It will be used for both User-Agent header and the X-Twitter-Client header on requests. If you want to include your own application version and url, pass C<< default_headers => \%my_request_headers >>. =head2 OAuth changes Net::Twitter saved request and access tokens in the client instance as part of the 3-legged OAuth handshake. That was a poor design decision. Twitter::API returns request and access tokens to the caller. It is the caller's responsibility to store and cache them appropriately. Hovever, transitional support is provided, with client instance storage, so your code can run unmodified while you make the transition. The following methods exist only for migration from Net::Twitter and will be removed in a future release. A warning is issued on each call to these methods. To disable the warnings, set C<$ENV{TWITTER_API_NO_MIGRATION_WARNINGS} = 1>. =over 4 =item * B replace with L or L and L =item * B replace with L or L and L =item * B replace with L =back If you are using the B trait, replace B calls with B calls. Method B does not set the C attribute. Method C is provided for transitional support, only. It warns like the OAuth methods discussed above, and it sets the C attribute so existing code should work as expected during migration. It will be removed in a future release. =head1 Migrating from Net::Twitter::Lite The discussion, above applies for L with a few exceptions. Net::Twitter::Lite does not use traits. Change your constructor call from: my $client = Net::Twitter::Lite::WithAPIv1_1->new(%args); To: my $client = Twitter::API->new_with_traits( traits => [ qw/Migration ApiMethods/ ], %args, ); If you're using the option B, see the discussion above about the Net::Twitter WrapResult trait. There is migration support for B. It will be removed in a future release. =head1 AUTHOR Marc Mims =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2015-2021 by Marc Mims. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut Twitter-API-1.0006/lib/Twitter/API/Trait/DecodeHtmlEntities.pm000644 000765 000024 00000004607 14031400662 023720 0ustar00marcstaff000000 000000 package Twitter::API::Trait::DecodeHtmlEntities; # ABSTRACT: Decode HTML entities in strings $Twitter::API::Trait::DecodeHtmlEntities::VERSION = '1.0006'; use Moo::Role; use HTML::Entities qw/decode_entities/; use Scalar::Util qw/refaddr/; use Ref::Util qw/is_arrayref is_hashref is_ref/; use namespace::clean; sub _decode_html_entities { my ( $self, $ref, $seen ) = @_; $seen //= {}; # Recursively walk data structure; decode entities is place on strings for ( is_arrayref($ref) ? @$ref : is_hashref($ref) ? values %$ref : () ) { next unless defined; # There shouldn't be any circular references in Twitter results, but # guard against it, anyway. if ( my $id = refaddr($_) ) { $self->_decode_html_entities($_, $seen) unless $$seen{$id}++; } else { # decode in place; happily, numbers remain untouched, no PV created decode_entities($_); } } } around inflate_response => sub { my ( $orig, $self, $c ) = @_; $self->$orig($c); $self->_decode_html_entities($c->result); }; 1; __END__ =pod =encoding UTF-8 =head1 NAME Twitter::API::Trait::DecodeHtmlEntities - Decode HTML entities in strings =head1 VERSION version 1.0006 =head1 SYNOPSIS use Twitter::API; use open qw/:std :utf8/; my $client = Twitter::API->new_with_traits( traits => [ qw/ApiMethods DecodeHtmlEntites/ ], %other_options ); my $status = $client->show_status(801814387723935744); say $status->{text}; # output: # Test DecodeHtmlEntities trait. < & > ⚠️ 🏉 'single' "double" # # output without the DecodeHtmlEntities trait: # Test DecodeHtmlEntities trait. < & > ⚠️ 🏉 'single' "double" =head1 DESCRIPTION Twitter has trust issues. They assume you're going to push the text you receive in API responses to a web page without HTML encoding it. But you HTML encode all of your output right? And Twitter's lack of trust has you double encoding entities. So, include this trait and Twitter::API will decode HTML entities in all of the text returned by the API. You're welcome. =head1 AUTHOR Marc Mims =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2015-2021 by Marc Mims. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut Twitter-API-1.0006/lib/Twitter/API/Trait/Enchilada.pm000644 000765 000024 00000002666 14031400662 022056 0ustar00marcstaff000000 000000 package Twitter::API::Trait::Enchilada; # ABSTRACT: Sometimes you want the whole enchilada $Twitter::API::Trait::Enchilada::VERSION = '1.0006'; use Moo::Role; use namespace::clean; # because you usually want the whole enchilada my $namespace = __PACKAGE__ =~ s/\w+$//r; with map join('', $namespace, $_), qw/ ApiMethods NormalizeBooleans RetryOnError DecodeHtmlEntities /; 1; __END__ =pod =encoding UTF-8 =head1 NAME Twitter::API::Trait::Enchilada - Sometimes you want the whole enchilada =head1 VERSION version 1.0006 =head1 SYNOPSIS use Twitter::API; my $client = Twitter::API->new_with_traits( traits => 'Enchilada', %other_new_options ); =head1 DESCRIPTION This is just a shortcut for applying commonly used traits. Because, sometimes, you just want the whole enchilada. This role simply bundles the following traits. See those modules for details. =over 4 =item * L =item * L =item * L =item * L =back =head1 AUTHOR Marc Mims =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2015-2021 by Marc Mims. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut Twitter-API-1.0006/lib/Twitter/API/Role/RequestArgs.pm000644 000765 000024 00000024711 14031400662 022264 0ustar00marcstaff000000 000000 package Twitter::API::Role::RequestArgs; # ABSTRACT: API request method helpers $Twitter::API::Role::RequestArgs::VERSION = '1.0006'; use 5.14.1; use warnings; use Carp; use Moo::Role; use Ref::Util qw/is_arrayref is_hashref/; use namespace::clean; requires 'request'; #pod =method request_with_id #pod #pod Transforms an argument list with a required C or C, #pod optionally passed as a leading, positional argument, a hashref argument. #pod #pod If a hashref follows the optional plain scalar, the user_id or screen_name is #pod added to it. Otherwise a new hashref is created and inserted into C<@_>. #pod #pod If the optional plain scalar argument is missing, and there is hashref of #pod arguments, or if the hashref does not contain the key C or #pod C, request_with_id croaks. #pod #pod Examples: #pod #pod $self->request_with_id(get => 'some/endpoint', 'foo'); #pod # is transformed to: #pod $self->request(get => 'some/endpoint', { screen_name => 'foo' }); #pod #pod $self->request_with_id(get => 'some/endpoint', 8575429); #pod # is transformed to: #pod $self->request(get => 'some/endpoint', { user_id => 8675429 }); #pod #pod $self->request_with_id(get => 'some/endpoint', { #pod screen_name => 'semifor', #pod }); #pod # is transformed to: #pod $self->request(get => 'some/endpoint', { screen_name => 'semifor' }); #pod #pod $self->request_with_id(get => 'some/endpoint', { #pod foo => 'bar', #pod }); ### croaks ### #pod #pod =cut # if there is a positional arg, it's an :ID (screen_name or user_id) sub request_with_id { splice @_, 1, 0, []; push @{$_[1]}, ':ID' if @_ > 4 && !is_hashref($_[4]); goto $_[0]->can('request_with_pos_args'); } #pod =method request_with_pos_args #pod #pod Transforms a list of required arguments, optionally provided positionally in a #pod determined order, into a hashref of named arguments. If a hashref follows the #pod positional arguments, the named arguments are added to it. Otherwise, a new #pod hashref in inserted into C<@_>. #pod #pod Zero or more of the required arguments may be provided positionally, as long as #pod the appear in the specified order. I any of the required arguments are not #pod provided positionally, they must be provided in the hashref or #pod request_with_pos_args croaks. #pod #pod The positional name C<:ID> is treated specially. It is transformed to #pod C if the value it represents contains only digits. Otherwise, it is #pod transformed to C. #pod #pod Examples: #pod #pod $self->request_with_pos_args( #pod [ 'id', 'name' ], get => 'some/endpoint', #pod '007', 'Bond' #pod ); #pod # is transformed to: #pod $self->request(get => 'some/endpoint', { #pod id => '007', #pod name => 'Bond', #pod }); #pod #pod $self->request_with_pos_args( #pod [ 'id', 'name' ], get => 'some/endpoint', #pod '007', { name => 'Bond' } #pod ); #pod # is also transformed to: #pod $self->request(get => 'some/endpoint', { #pod id => '007', #pod name => 'Bond', #pod }); #pod #pod $self->request_with_pos_args( #pod [ ':ID', 'status' ], get => 'some/endpoint', #pod 'alice', 'down the rabbit hole' #pod ); #pod # is transformed to: #pod $self->request(get => 'some/endpoint', { #pod sreen_name => 'alice', #pod status => 'down the rabbit hole', #pod }); #pod #pod =cut sub request_with_pos_args { my $self = shift; $self->request($self->normalize_pos_args(@_)); } #pod =method normalize_pos_args #pod #pod Helper method for C. Takes the same arguments described in #pod C above, and returns a list of arguments ready for a #pod call to C. #pod #pod Individual methods in L use #pod C if they need to do further processing on the args hashref #pod before calling C. #pod #pod =cut sub normalize_pos_args { my $self = shift; my @pos_names = shift; my $http_method = shift; my $path = shift; my %args; # names can be a single value or an arrayref @pos_names = @{ $pos_names[0] } if is_arrayref($pos_names[0]); # gather positional arguments and name them while ( @pos_names ) { last if @_ == 0 || is_hashref($_[0]); $args{shift @pos_names} = shift; } # get the optional, following args hashref and expand it my %args_hash; %args_hash = %{ shift() } if is_hashref($_[0]); # extract any required args if we still have names while ( my $name = shift @pos_names ) { if ( $name eq ':ID' ) { $name = exists $args_hash{screen_name} ? 'screen_name' : 'user_id'; croak 'missing required screen_name or user_id' unless exists $args_hash{$name}; } croak "missing required '$name' arg" unless exists $args_hash{$name}; $args{$name} = delete $args_hash{$name}; } # name the :ID value (if any) based on its value if ( my $id = delete $args{':ID'} ) { $args{$id =~/\D/ ? 'screen_name' : 'user_id'} = $id; } # merge in the remaining optional values for my $name ( keys %args_hash ) { croak "'$name' specified in both positional and named args" if exists $args{$name}; $args{$name} = $args_hash{$name}; } return ($http_method, $path, \%args, @_); } #pod =method flatten_list_args([ $key | \@keys ], \%args) #pod #pod Some Twitter API arguments take a list of values as a string of comma separated #pod items. To allow callers to pass an array reference of items instead, this #pod method is used to flatten array references to strings. The key or keys identify #pod which values to flatten in the C<\%args> hash reference, if they exist. #pod #pod =cut sub flatten_list_args { my ( $self, $keys, $args ) = @_; for my $key ( is_arrayref($keys) ? @$keys : $keys ) { if ( my $value = $args->{$key} ) { $args->{$key} = join ',' => @$value if is_arrayref($value); } } } 1; __END__ =pod =encoding UTF-8 =head1 NAME Twitter::API::Role::RequestArgs - API request method helpers =head1 VERSION version 1.0006 =head1 SYNOPSIS package MyApiMethods; use Moo::Role; sub timeline { shift->request_with_id(get => 'statuses/user_timeline, @_); } Then, in your application code: use Twitter::API; my $client = Twitter::API->new_with_traits( traits => '+MyApiMethods', %othe_new_options, ); my $statuses = $client->timeline('semifor'); # equivalent to: my $statuses = $client->get('statuses/user_timeline', { screen_name => 'semifor', }); =head1 DESCRIPTION Helper methods for implementers of custom traits for creating concise Twitter API methods. Used in L. =head1 METHODS =head2 request_with_id Transforms an argument list with a required C or C, optionally passed as a leading, positional argument, a hashref argument. If a hashref follows the optional plain scalar, the user_id or screen_name is added to it. Otherwise a new hashref is created and inserted into C<@_>. If the optional plain scalar argument is missing, and there is hashref of arguments, or if the hashref does not contain the key C or C, request_with_id croaks. Examples: $self->request_with_id(get => 'some/endpoint', 'foo'); # is transformed to: $self->request(get => 'some/endpoint', { screen_name => 'foo' }); $self->request_with_id(get => 'some/endpoint', 8575429); # is transformed to: $self->request(get => 'some/endpoint', { user_id => 8675429 }); $self->request_with_id(get => 'some/endpoint', { screen_name => 'semifor', }); # is transformed to: $self->request(get => 'some/endpoint', { screen_name => 'semifor' }); $self->request_with_id(get => 'some/endpoint', { foo => 'bar', }); ### croaks ### =head2 request_with_pos_args Transforms a list of required arguments, optionally provided positionally in a determined order, into a hashref of named arguments. If a hashref follows the positional arguments, the named arguments are added to it. Otherwise, a new hashref in inserted into C<@_>. Zero or more of the required arguments may be provided positionally, as long as the appear in the specified order. I any of the required arguments are not provided positionally, they must be provided in the hashref or request_with_pos_args croaks. The positional name C<:ID> is treated specially. It is transformed to C if the value it represents contains only digits. Otherwise, it is transformed to C. Examples: $self->request_with_pos_args( [ 'id', 'name' ], get => 'some/endpoint', '007', 'Bond' ); # is transformed to: $self->request(get => 'some/endpoint', { id => '007', name => 'Bond', }); $self->request_with_pos_args( [ 'id', 'name' ], get => 'some/endpoint', '007', { name => 'Bond' } ); # is also transformed to: $self->request(get => 'some/endpoint', { id => '007', name => 'Bond', }); $self->request_with_pos_args( [ ':ID', 'status' ], get => 'some/endpoint', 'alice', 'down the rabbit hole' ); # is transformed to: $self->request(get => 'some/endpoint', { sreen_name => 'alice', status => 'down the rabbit hole', }); =head2 normalize_pos_args Helper method for C. Takes the same arguments described in C above, and returns a list of arguments ready for a call to C. Individual methods in L use C if they need to do further processing on the args hashref before calling C. =head2 flatten_list_args([ $key | \@keys ], \%args) Some Twitter API arguments take a list of values as a string of comma separated items. To allow callers to pass an array reference of items instead, this method is used to flatten array references to strings. The key or keys identify which values to flatten in the C<\%args> hash reference, if they exist. =head1 AUTHOR Marc Mims =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2015-2021 by Marc Mims. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut Twitter-API-1.0006/examples/oauth_web.psgi000644 000765 000024 00000011764 14031400662 020422 0ustar00marcstaff000000 000000 package WebApp; # Simple web app example using Plack. To run it: # plackup examples/oauth_web.psgi use 5.14.1; use Moo; use Encode qw/encode_utf8/; use HTML::Escape qw/escape_html/; use Plack::Request; use Plack::Response; use Twitter::API; has twitter_client => ( is => 'ro', default => sub { Twitter::API->new_with_traits( traits => 'Enchilada', # Net::Twitter example app credentials: map(tr/A-Za-z/N-ZA-Mn-za-m/r, qw/ pbafhzre_xrl i8g3WVYxFglyotakTYBD pbafhzre_frperg 5e31eFZp0ACgOcUpX8ZiaPYt2bNlSYk5rTBZxKZ /), # To use your own app credentials: # consumer_key => 'your-app-key', # consumer_secret => 'your-app-secret', ); }, ); # In a production application, use something like Redis to store request token # secrets. Twitter expires request tokens after 15 minutes. Your app should # keep them just a bit longer to ensure you don't discard them before Twitter # does. Maybe 20 minutes. In this simple demo, we won't worry about expiration. has secret_cache => ( is => 'ro', default => sub { {} }, ); # We only need it once, so remove it from the cache sub get_secret { delete $_[0]->secret_cache->{$_[1]} } sub set_secret { $_[0]->secret_cache->{$_[1]} = $_[2] } sub uri_for { my ( $self, $req, $path ) = @_; my $uri = $req->base; $uri->path($uri->path . $path); return $uri; } my %route = ( '/' => 'home_page', '/oauth_callback' => 'oauth_callback', ); sub dispatch { my ( $self, $req, $res ) = @_; my $method = $route{$req->path} // 'not_found'; $self->$method($req, $res); } sub handle_request { my ( $self, $env ) = @_; my $req = Plack::Request->new($env); my $res = Plack::Response->new(200); $res->content_type('text/html; charset=utf-8'); $self->dispatch($req, $res); $res->finalize; } sub to_app { my $self = shift->new; sub { $self->handle_request(@_) }; }; sub home_page { my ( $self, $req, $res ) = @_; my $client = $self->twitter_client; # If we have access token/secret, display verify_credentials response if ( my $credentials = $req->cookies->{'dont-do-this-at-home'} ) { my ( $token, $secret ) = split /\s+/, $credentials; my $r = $client->verify_credentials({ -token => $token, -token_secret => $secret, }); my $body = escape_html( $client->json_parser->pretty(1)->encode($r) ); $res->body("
$body
"); } else { # Otherwise, prompt the user to authenticate my $r = $client->oauth_request_token({ callback => $self->uri_for($req, 'oauth_callback'), }); my ( $token, $secret ) = @{$r}{qw/oauth_token oauth_token_secret/}; # Save the request token and secret; we'll need them later. $self->set_secret($token, $secret); my $url = $client->oauth_authentication_url({ oauth_token => $token, }); $res->body(qq{Authenticate with Twitter}); } } sub oauth_callback { my ( $self, $req, $res ) = @_; my $token = $req->param('oauth_token'); my $verifier = $req->param('oauth_verifier'); if ( $token && $verifier ) { # The user authenticated! # Get our cached request token secret my $secret = $self->get_secret($token) // die 'missing secret'; my $r = $self->twitter_client->oauth_access_token({ token => $token, token_secret => $secret, verifier => $verifier, }); # DON'T DO THIS AT HOME! # # In a production app, you will store the access_token and # access_token_secret in a database. They can be used to make Twitter # API calls on behalf of the authenticated user. Ideally, you should # treat them like you would user names and passwords. Encrypt them. # # For our simple demo, since we don't have a permanent data store, # we'll store them in a session cookie. $res->cookies->{'dont-do-this-at-home'} = join ' ', $$r{oauth_token}, $$r{oauth_token_secret}; $res->redirect($self->uri_for($req, '')); return; } my $home = $self->uri_for($req, ''); if ( $token = $req->param('denied') ) { # The user canceled the authentication request and select "return to # the application". $self->get_secret($token); # discard; no longer valid or useful $res->body(qq{You denied us access. Go home}); return; } # /oauth_callback was requested without the expected parameters; let's just # redirect to the root page $res->redirect($home); } sub not_found { my ( $self, $req, $res ) = @_; my $home = $self->uri_for($req, ''); $res->status(404); $res->body($req->path_info . qq{ does not live here, try the main page}); } my $app = __PACKAGE__->to_app; Twitter-API-1.0006/examples/app-auth.pl000755 000765 000024 00000000757 14031400662 017640 0ustar00marcstaff000000 000000 #!/usr/bin/env perl use 5.14.1; use warnings; use utf8; use open qw/:std :utf8/; use Twitter::API; my $client = Twitter::API->new_with_traits( traits => [ qw/AppAuth ApiMethods/ ], consumer_key => $ENV{CONSUMER_KEY}, consumer_secret => $ENV{CONSUMER_SECRET}, ); my $token = $client->oauth2_token; $client->access_token($token); my $r = $client->user_timeline(twitterapi => { count => 10 }); for my $status ( @$r ) { say "$status->{user}{screen_name}: $status->{text}"; } Twitter-API-1.0006/examples/upload.pl000755 000765 000024 00000000705 14031400662 017376 0ustar00marcstaff000000 000000 #!/usr/bin/env perl use 5.14.1; use warnings; use Twitter::API; my $client = Twitter::API->new_with_traits( traits => [ qw/ApiMethods/ ], consumer_key => $ENV{CONSUMER_KEY}, consumer_secret => $ENV{CONSUMER_SECRET}, access_token => $ENV{ACCESS_TOKEN}, access_token_secret => $ENV{ACCESS_TOKEN_SECRET}, ); my $r = $client->upload_media([ "$ENV{HOME}/Downloads/hello-world.png" ]); say "media_id: $$r{media_id}"; Twitter-API-1.0006/examples/retry-on-error.pl000755 000765 000024 00000000726 14031400662 021023 0ustar00marcstaff000000 000000 #!/usr/bin/env perl use 5.14.1; use warnings; use utf8; use open qw/:std :utf8/; use Twitter::API; my $client = Twitter::API->new_with_traits( traits => [ qw/ApiMethods RetryOnError/ ], consumer_key => $ENV{CONSUMER_KEY}, consumer_secret => $ENV{CONSUMER_SECRET}, access_token => $ENV{ACCESS_TOKEN}, access_token_secret => $ENV{ACCESS_TOKEN_SECRET}, ); my $r = $client->verify_credentials; say "$$r{screen_name} is authorized"; Twitter-API-1.0006/examples/decode-html-entities.pl000755 000765 000024 00000001153 14031400662 022117 0ustar00marcstaff000000 000000 #!/usr/bin/env perl use 5.14.1; use warnings; use utf8; use open qw/:std :utf8/; use Test::More; use Twitter::API; my $client = Twitter::API->new_with_traits( traits => [ qw/ApiMethods DecodeHtmlEntities/ ], consumer_key => $ENV{CONSUMER_KEY}, consumer_secret => $ENV{CONSUMER_SECRET}, access_token => $ENV{ACCESS_TOKEN}, access_token_secret => $ENV{ACCESS_TOKEN_SECRET}, ); my $r = $client->show_status(801814387723935744); diag $$r{text}; is $$r{text}, q{Test DecodeHtmlEntities trait. < & > ⚠️ 🏉 'single' "double"}, 'has no encoded HTML entities'; done_testing; Twitter-API-1.0006/examples/api.pl000755 000765 000024 00000001046 14031400662 016662 0ustar00marcstaff000000 000000 #!/usr/bin/env perl use 5.14.1; use warnings; use utf8; use open qw/:std :utf8/; use Twitter::API; my $client = Twitter::API->new_with_traits( traits => [ qw/ApiMethods/ ], consumer_key => $ENV{CONSUMER_KEY}, consumer_secret => $ENV{CONSUMER_SECRET}, access_token => $ENV{ACCESS_TOKEN}, access_token_secret => $ENV{ACCESS_TOKEN_SECRET}, ); my $r = $client->verify_credentials; say "$$r{screen_name} is authorized"; my $mentions = $client->mentions; for my $status ( @$mentions ) { say $$status{text}; } Twitter-API-1.0006/examples/oauth_desktop.pl000755 000765 000024 00000003233 14031400662 020762 0ustar00marcstaff000000 000000 #!/usr/bin/env perl # Twitter::API - OAuth desktop app example # use 5.14.1; use warnings; use Data::Dumper; use Twitter::API; # You can replace the consumer key/secret with your own. These credentials are # for the Net::Twitter example app. my $client = Twitter::API->new_with_traits( traits => 'Enchilada', # Net::Twitter example app credentials map(tr/A-Za-z/N-ZA-Mn-za-m/r, qw/ pbafhzre_xrl i8g3WVYxFglyotakTYBD pbafhzre_frperg 5e31eFZp0ACgOcUpX8ZiaPYt2bNlSYk5rTBZxKZ /), # To use your own: # consumer_key => 'your-app-key', # consumer_secret => 'your-app-secret', ); # 1. First, we get a request token and secret: my $request = $client->oauth_request_token; # 2. We use the request token to generate an authorization URL: my $auth_url = $client->oauth_authorization_url({ oauth_token => $request->{oauth_token}, }); # 3. Authorize the app in a web browser to get a verifier PIN: print " Authorize this application at: $auth_url Then, enter the returned PIN number displayed in the browser: "; # 4. Enter the PIN my $pin = ; # wait for input chomp $pin; say ''; # 5. Exchange the request token for an access token my $access = $client->oauth_access_token({ token => $request->{oauth_token}, token_secret => $request->{oauth_token_secret}, verifier => $pin, }); my ( $token, $secret ) = @{$access}{qw(oauth_token oauth_token_secret)}; # Now you have user credentials say 'access_token.......: ', $token; say 'access_token_secret: ', $secret; my $status = $client->user_timeline({ count => 1, -token => $token, -token_secret => $secret, }); say Dumper $status; Twitter-API-1.0006/t/error.t000644 000765 000024 00000002411 14031400662 015511 0ustar00marcstaff000000 000000 #!perl use 5.14.1; use warnings; use HTTP::Response; use Test::Spec; use Twitter::API::Context; use Twitter::API::Error; sub construct_error { my ( $http_status_code, $twitter_error_code, $twitter_error_text ) = @_; my $http_response = HTTP::Response->new($http_status_code); my $result = { errors => [ { code => $twitter_error_code, message => $twitter_error_text }, ] } if defined $twitter_error_code; my $context = Twitter::API::Context->new( http_response => $http_response, $result ? ( result => $result ) : (), ); return Twitter::API::Error->new(context => $context); } describe 'Twitter::API::Error' => sub { it 'is always true' => sub { ok !!construct_error(0); }; describe is_token_error => sub { for my $code ( 32, 64, 88, 89, 99, 135, 136, 215, 226, 326 ) { it "recognizes $code as a token error" => sub { ok construct_error(400, $code)->is_token_error; }; } }; describe is_token_error => sub { for my $code ( 34, 50, 63, 144 ) { it "does not recognize $code as a token error" => sub { ok !construct_error(400, $code)->is_token_error; }; } }; }; runtests; Twitter-API-1.0006/t/role/000755 000765 000024 00000000000 14031400662 015136 5ustar00marcstaff000000 000000 Twitter-API-1.0006/t/000_load.t000644 000765 000024 00000000141 14031400662 015654 0ustar00marcstaff000000 000000 use strict; use warnings; use Test::More; BEGIN { use_ok('Twitter::API'); } done_testing; Twitter-API-1.0006/t/trait/000755 000765 000024 00000000000 14031400662 015320 5ustar00marcstaff000000 000000 Twitter-API-1.0006/t/util.t000644 000765 000024 00000002627 14031400662 015346 0ustar00marcstaff000000 000000 use 5.14.1; use warnings; use Test::More; use Test::Fatal; use Twitter::API::Util qw/:all/; ## Timestamp methods sub ex_timestamp { 'Wed Jun 06 20:07:10 +0000 2012' } sub ex_time { 1339013230 } can_ok('Twitter::API::Util', $_) for qw/ timestamp_to_time timestamp_to_gmtime timestamp_to_localtime is_twitter_api_error /; is timestamp_to_time(ex_timestamp), ex_time, 'example timestamp to time'; is( scalar timestamp_to_gmtime(ex_timestamp), scalar gmtime(ex_time), 'example timestamp to gmtime (scalar)' ); is( scalar timestamp_to_localtime(ex_timestamp), scalar localtime(ex_time), 'example timestamp to localtime (scalar)' ); is_deeply( [ timestamp_to_gmtime(ex_timestamp) ], [ gmtime(ex_time) ], 'example timestamp to gmtime (list context)' ); is( scalar timestamp_to_localtime(ex_timestamp), scalar localtime(ex_time), 'example timestamp to localtime (list context)' ); is timestamp_to_time(), undef, 'returns undef on undef input'; like( exception { timestamp_to_time('bougus') }, qr/invalid timestamp/, 'croaks on invalid format' ); ## Twitter::API::Error ok is_twitter_api_error(bless {}, 'Twitter::API::Error'), 'is api error'; ok !is_twitter_api_error(bless {}, 'Foo'), 'other object is not api error'; ok !is_twitter_api_error(1234), 'plain scalar is not api error'; ok !is_twitter_api_error(), 'empty is not api error'; done_testing; Twitter-API-1.0006/t/author-pod-syntax.t000644 000765 000024 00000000454 14031400662 017773 0ustar00marcstaff000000 000000 #!perl BEGIN { unless ($ENV{AUTHOR_TESTING}) { print qq{1..0 # SKIP these tests are for testing by the author\n}; exit } } # This file was automatically generated by Dist::Zilla::Plugin::PodSyntaxTests. use strict; use warnings; use Test::More; use Test::Pod 1.41; all_pod_files_ok(); Twitter-API-1.0006/t/url-for.t000644 000765 000024 00000001463 14031400662 015754 0ustar00marcstaff000000 000000 use strict; use warnings; use Test::More; use Twitter::API; my $client = Twitter::API->new_with_traits( traits => 'AppAuth', consumer_key => 'key', consumer_secret => 'secret', ); is( $client->api_url_for('some/endpoint'), 'https://api.twitter.com/1.1/some/endpoint.json', 'api url' ); is( $client->upload_url_for('some/endpoint'), 'https://upload.twitter.com/1.1/some/endpoint.json', 'upload url' ); is( $client->oauth_url_for('some/endpoint'), 'https://api.twitter.com/oauth/some/endpoint', 'oauth url' ); is( $client->oauth2_url_for('some/endpoint'), 'https://api.twitter.com/oauth2/some/endpoint', 'oauth2 url' ); { my $url = 'http://my.custom.url/endpoint'; is($client->api_url_for($url), $url, 'custom url'); } done_testing; Twitter-API-1.0006/t/oauth.t000644 000765 000024 00000014070 14031400662 015504 0ustar00marcstaff000000 000000 #!perl use 5.14.1; use warnings; use HTTP::Response; use Test::Spec; use Twitter::API; sub new_client { Twitter::API->new( consumer_key => 'key', consumer_secret => 'secret', ); } my $token = 'Z6eEdO8MOmk394WozF5oKyuAv855l4Mlqo7hhlSLik'; my $secret = 'Kd75W4OQfb2oJTV0vzGzeXftVAwgMnEK9MumzYcM'; describe oauth => sub { my $client; before each => sub { $client = new_client }; describe 'authentication urls' => sub { for ( [ authenticate => 'oauth_authentication_url' ], [ authorize => 'oauth_authorization_url' ] ) { my ( $endpoint, $method ) = @$_; describe $method => sub { my $uri; before each => sub { $uri = $client->$method( oauth_token => $token, force_login => 'true', screen_name => 'bogus', ); }; it 'has correct scheme' => sub { is($uri->scheme, 'https'); }; it 'has correct host ' => sub { is($uri->host, 'api.twitter.com'); }; it 'has correct path' => sub { is($uri->path, "/oauth/$endpoint"); }; it 'has correct query' => sub { is_deeply { $uri->query_form }, { oauth_token => $token, force_login => 'true', screen_name => 'bogus', }; }; }; } }; describe oauth_request_token => sub { before each => sub { my $content = "oauth_token=$token&oauth_token_secret=$secret" ."&oauth_callback_confirmed=true"; $client->user_agent->stubs(request => sub { HTTP::Response->new(200, 'OK', [ content_type => 'application/x-www-form-urlencoded', content_length => length $content, ], $content, ); }); }; it 'has valid authorization header' => sub { my ( $r, $c ) = $client->oauth_request_token; like $c->http_request->header('authorization'), qr/ OAuth\s+ oauth_callback="oob",\s+ oauth_consumer_key="key",\s+ oauth_nonce="[^"]+",\s+ oauth_signature="[^"]+",\s+ oauth_signature_method="HMAC-SHA1",\s+ oauth_timestamp="\d+",\s+ oauth_version="1.0" /x; }; it 'returns a hashref with oauth_token/secret' => sub { my $r = $client->oauth_request_token; is_deeply $r, { oauth_token => $token, oauth_token_secret => $secret, oauth_callback_confirmed => 'true', }; }; }; describe oauth_access_token => sub { before each => sub { # from the Twitter docs my $content = 'oauth_token=6253282-eWudHldSbIaelX7swmsiHImEL4Kinw' .'aGloHANdrY&oauth_token_secret=2EEfA6BG3ly3sR3RjE0IBSnlQu4Zr' .'UzPiYKmrkVU&user_id=6253282&screen_name=twitterapi'; $client->user_agent->stubs(request => sub { HTTP::Response->new(200, 'OK', [ content_type => 'application/x-www-form-urlencoded', content_length => length $content, ], $content, ); }); }; it 'has a valid autorization header' => sub { my ( $r, $c ) = $client->oauth_access_token( token => 'request-token', token_secret => 'request-token-secret', verifier => 'verifier', ); like $c->http_request->header('authorization'), qr/ OAuth\s+ oauth_consumer_key="key",\s+ oauth_nonce="[^"]+",\s+ oauth_signature="[^"]+",\s+ oauth_signature_method="HMAC-SHA1",\s+ oauth_timestamp="\d+",\s+ oauth_token="request-token",\s+ oauth_verifier="verifier",\s+ oauth_version="1.0" /x; }; it 'returns a hashref with oauth_token/secret' => sub { my $r = $client->oauth_access_token( token => 'request-token', token_secret => 'request-token-secret', verifier => 'verifier', ); is $$r{user_id}, 6253282; is $$r{screen_name}, 'twitterapi'; like $$r{oauth_token}, qr/^6253282-eWudHldSb/; like $$r{oauth_token_secret}, qr/^2EEfA6BG3l/; }; describe xauth => sub { it 'has a valid authorization header' => sub { my ( $r, $c ) = $client->xauth('alice', 'rabbit'); like $c->http_request->header('authorization'), qr/ OAuth\s+ oauth_consumer_key="key",\s+ oauth_nonce="[^"]+",\s+ oauth_signature="[^"]+",\s+ oauth_signature_method="HMAC-SHA1",\s+ oauth_timestamp="\d+",\s+ oauth_version="1.0" /x; }; it 'has required args' => sub { my ( $r, $c ) = $client->xauth('alice', 'rabbit'); my $args = URL::Encode::url_params_mixed( $c->http_request->content); is_deeply [ sort keys %$args ], [ qw/x_auth_mode x_auth_password x_auth_username/ ]; }; it 'returns a hashref with oauth_token/secret' => sub { my $r = $client->xauth('foo@bar.baz', 'SeCrEt'); is_deeply [ sort keys %$r ], [ qw/oauth_token oauth_token_secret screen_name user_id/ ]; }; }; }; }; runtests; Twitter-API-1.0006/t/base.t000644 000765 000024 00000014321 14031400662 015275 0ustar00marcstaff000000 000000 #!perl use strict; use warnings; use HTTP::Response; use Test::Fatal; use Test::Spec; use URL::Encode qw/url_params_mixed/; use Twitter::API; sub http_response_ok { HTTP::Response->new( 200, 'OK', [ content_type => 'application/json;charset=utf-8', contest_length => 4, ], '{}' ); } describe 'construction' => sub { it 'requires consumer_key' => sub { like( exception { Twitter::API->new }, qr/Missing .* consumer_key/, ); }; it 'requires consumer_secret' => sub { like( exception { Twitter::API->new(consumer_key => 'key') }, qr/Missing .* consumer_secret/, ); }; it 'instantiates a minimal object' => sub { exception { Twitter::API->new( consumer_key => 'key', consumer_secret => 'secret', ); }, undef; }; }; describe 'request' => sub { my $client; before each => sub { $client = Twitter::API->new( consumer_key => 'key', consumer_secret => 'secret', ); $client->stubs(send_request => \&http_response_ok); }; it 'requires an access_token' => sub { like( exception { $client->request(get => 'fake/endpoint') }, qr/token/, ); }; it 'accepts per request tokens' => sub { exception { $client->get('fake/endpoint', { -token => 'my-token', -token_secret => 'my-secret', }); }, undef; }; it "uses object's user credentials" => sub { exception { $client->access_token('token'); $client->access_token_secret('token-secret'); $client->get('fake/endpoint'); }, undef; }; it 'prioritizes per-request user credentials' => sub { $client->access_token('token'); $client->access_token_secret('token-secret'); my ($r, $c) = $client->get('fake/endpoint', { -token => 'per-request', -token_secret => 'per-request-secret', }); my $req = $c->http_request; like $req->header('authorization'), qr/oauth_token="per-request"/; }; }; describe 'get' => sub { my $req; before each => sub { my $client = Twitter::API->new( consumer_key => 'key', consumer_secret => 'secret', ); $client->stubs(send_request => \&http_response_ok); my ($r, $c) = $client->get('fake/endpoint', { -token => 'token', -token_secret => 'access_token_secret', foo => 'bar', baz => 'bop', }); $req = $c->http_request; }; it 'uses method GET' => sub { is $req->method, 'GET' }; it 'expands url' => sub { like $req->uri, qr{^\Qhttps://api.twitter.com/1.1/fake/endpoint.json}; }; it 'passes API arguments' => sub { is_deeply { $req->uri->query_form }, { foo => 'bar', baz => 'bop' }; }; it 'creates valid authorization header' => sub { like $req->header('authorization'), qr/ OAuth\ oauth_consumer_key="key",\s* oauth_nonce="[^"]+",\s* oauth_signature="[^"]+",\s* oauth_signature_method="HMAC-SHA1",\s* oauth_timestamp="\d+",\s* oauth_token="token",\s* oauth_version="1\.0" /x; }; }; describe 'post' => sub { my $req; before each => sub { my $client = Twitter::API->new( consumer_key => 'key', consumer_secret => 'secret', access_token => 'token', access_token_secret => 'secret', ); $client->stubs(send_request => \&http_response_ok); my ($r, $c) = $client->post('fake/endpoint', { foo => 'bar', baz => 'bop', }); $req = $c->http_request; }; it 'has method POST' => sub { is $req->method, 'POST' }; it 'has uses correct Contect-Type' => sub { is $req->content_type, 'application/x-www-form-urlencoded'; }; it 'passes API arguments' => sub { is_deeply url_params_mixed($req->decoded_content), { foo => 'bar', baz => 'bop', }; }; }; describe 'post (file upload)' => sub { my $req; before each => sub { my $client = Twitter::API->new( consumer_key => 'key', consumer_secret => 'secret', access_token => 'token', access_token_secret => 'secret', ); $client->stubs(send_request => \&http_response_ok); my ($r, $c) = $client->post('fake/endpoint', { foo => 'bar', baz => 'bop', file => [ undef, 'file', content => 'just some text' ], }); $req = $c->http_request; }; it 'has correct content type' => sub { is $req->content_type, 'multipart/form-data'; }; it 'passes API args' => sub { my %args; for ( $req->parts ) { my ( $name ) = $_->header('content_disposition') =~ / name="([^"]+)"/; my $value = $_->decoded_content; $args{$name} = $value; } is_deeply \%args, { foo => 'bar', baz => 'bop', file => 'just some text', }; }; }; describe 'post (json body)' => sub { my ( $client, $req ); before each => sub { $client = Twitter::API->new( consumer_key => 'key', consumer_secret => 'secret', access_token => 'token', access_token_secret => 'secret', ); $client->stubs(send_request => \&http_response_ok); my ($r, $c) = $client->post('fake/endpoint', { -to_json => { foo => 'bar', baz => 'bop' }, }); $req = $c->http_request; }; it 'has correct content type' => sub { is $req->content_type, 'application/json'; }; it 'has carrect content' => sub { my $json = $req->decoded_content; my $data = $client->from_json($json); is_deeply $data, { foo => 'bar', baz => 'bop' }; }; }; runtests; Twitter-API-1.0006/t/trait/retry-on-error.t000644 000765 000024 00000007161 14031400662 020420 0ustar00marcstaff000000 000000 #!perl use 5.14.1; use warnings; use HTTP::Response; use Ref::Util qw/is_ref/; use Test::Fatal; use Test::Spec; use Twitter::API; sub new_client { my $response = shift; my $user_agent = mock(); $user_agent->stubs(request => sub { like $_[1]->headers->header('authorization'), qr/^OAuth\s/, 'The Authorization header should start with OAuth'; my ( $code, $reason ) = is_ref($$response[0]) ? @{ shift @$response // [ 999 => 'off the end' ] } : @{ $response }; HTTP::Response->new($code, $reason); }); return Twitter::API->new_with_traits( traits => 'RetryOnError', consumer_key => 'key', consumer_secret => 'secret', access_token => 'token', access_token_secret => 'token-secret', user_agent => $user_agent, @_ ); } describe RetryOnError => sub { it 'dies after 5 retries' => sub { my $client = new_client([ 503 => 'Temporarily Unavailable' ]); my $retry = mock(); $retry->expects('ping')->exactly(5); $client->retry_delay_code(sub { $retry->ping }); like exception { $client->get('foo'); }, qr/Temporarily Unavailable/; }; it 'dies immediately on 404' => sub { my $client = new_client([ 404 => 'Not Found' ]); my $retry = mock(); $retry->expects('ping')->exactly(0); $client->retry_delay_code(sub { $retry->ping }); like exception { $client->get('foo'); }, qr/Not Found/; }; it 'dies no first perm error' => sub { my $client = new_client([ [ 500 => 'Internal Server Error' ], [ 503 => 'Temporarily Unavailable' ], [ 403 => 'Forbidden' ], ]); my $retry = mock(); $retry->expects('ping')->exactly(2); $client->retry_delay_code(sub { $retry->ping }); like exception { $client->get('foo'); }, qr/Forbidden/; }; it 'succeeds after retry' => sub { my $client = new_client([ [ 500 => 'Internal Server Error' ], [ 503 => 'Temporarily Unavailable' ], [ 200 => 'OK' ], ]); my $retry = mock(); $retry->expects('ping')->exactly(2); $client->retry_delay_code(sub { $retry->ping }); is exception { $client->get('foo'); }, undef; }; it 'succeeds immediately on 200' => sub { my $client = new_client([ 200 => 'OK' ]); my $retry = mock(); $retry->expects('ping')->exactly(0); $client->retry_delay_code(sub { $retry->ping }); is exception { $client->get('foo'); }, undef; }; it 'has expected initial delay' => sub { my $client = new_client([ [ 500 => 'Internal Server Error' ], [ 200 => 'OK' ], ]); my $retry = mock(); $retry->expects('ping')->with(0.25); $client->retry_delay_code(sub { $retry->ping(@_) }); is exception { $client->get('foo'); }, undef; }; it 'delay doubles' => sub { my $client = new_client([ [ 500 => 'Internal Server Error' ], [ 500 => 'Internal Server Error' ], [ 200 => 'OK' ], ]); my $expected_delay = 0.25; $client->retry_delay_code(sub { my $delay = shift; is $delay, $expected_delay; $expected_delay *= 2; }); is exception { $client->get('foo') }, undef; }; }; runtests; Twitter-API-1.0006/t/trait/normalize-booleans.t000644 000765 000024 00000001035 14031400662 021304 0ustar00marcstaff000000 000000 #!perl use 5.14.1; use warnings; use Test::More; package Foo { use Moo; with 'Twitter::API::Trait::NormalizeBooleans'; # required sub preprocess_args {} } my $args = { foo => 'bar', include_email => 1, skip_user => 't', skip_status => '', text => 'test', }; my $foo = Foo->new; $foo->normalize_bools($args); is_deeply $args, { foo => 'bar', include_email => 'true', skip_user => 'true', text => 'test', }, 'normalized'; done_testing; Twitter-API-1.0006/t/trait/decode-html-entities.t000644 000765 000024 00000003657 14031400662 021527 0ustar00marcstaff000000 000000 #!perl use 5.14.1; use warnings; use Test::Spec; package Client { use Moo; with 'Twitter::API::Trait::DecodeHtmlEntities'; sub inflate_response {} } my $client = Client->new; describe _decode_html_entities => sub { it 'decodes html entities' => sub { my $data = { string => 'With & and <', number => 1234, array => [ 'With & and <', 1234, array => [ 'With & and <', 1234, ], hash => { string => 'With & and <', number => 1234, }, ], hash => { string => 'With & and <', number => 1234, array => [ 'With & and <', 1234, hash => { string => 'With & and <', number => 1234, }, ], }, }; $client->_decode_html_entities($data); is_deeply $data, { string => 'With & and <', number => 1234, array => [ 'With & and <', 1234, array => [ 'With & and <', 1234, ], hash => { string => 'With & and <', number => 1234, }, ], hash => { string => 'With & and <', number => 1234, array => [ 'With & and <', 1234, hash => { string => 'With & and <', number => 1234, }, ], }, }; }; }; runtests; Twitter-API-1.0006/t/trait/app-auth.t000644 000765 000024 00000005730 14031400662 017231 0ustar00marcstaff000000 000000 #!perl use 5.14.1; use warnings; use HTTP::Response; use URL::Encode qw/url_decode/; use Test::Spec; use Twitter::API; # token straight out of Twitter docs my $url_encoded_token = 'AAAA%2FAAA%3DAAAAAAAA'; my $url_decoded_token = url_decode($url_encoded_token); describe AppAuth => sub { my $client; before each => sub { $client = Twitter::API->new_with_traits( traits => 'AppAuth', consumer_key => 'key', consumer_secret => 'secret', ); }; describe oauth2_token => sub { before each => sub { my $content = '{"token_type":"bearer","access_token"' .':"'.$url_encoded_token.'"}'; $client->user_agent->stubs(request => sub { HTTP::Response->new(200, 'OK', [ content_type => 'application/json; charset=utf-8', content_length => length $content, ], $content, ); }); }; it 'uses Basic auth' => sub { my ( $r, $c ) = $client->oauth2_token; like $c->http_request->header('authorization'), qr/^Basic /; }; it 'returns a url_decoded token' => sub { my $r = $client->oauth2_token; # Twitter sends it url_encoded, AppAuth decodes it is $r, $url_decoded_token; }; }; describe invalidate_token => sub { my $token; before each => sub { my $content = '{"access_token":"'.$url_encoded_token.'"}'; $client->user_agent->stubs(request => sub { HTTP::Response->new(200, 'OK', [ content_type => 'application/json; charset=utf-8', content_length => length $content, ], $content, ); }); }; it 'sends a url encoded token' => sub { my ( $r, $c ) = $client->invalidate_token($url_decoded_token); like $c->http_request->content, qr/access_token=\Q$url_encoded_token/; }; it 'it returns the url_decoded token' => sub { my $r = $client->invalidate_token($url_decoded_token); is $r, $url_decoded_token; }; }; describe 'authenticated request' => sub { my $req; before each => sub { $client->user_agent->stubs(request => sub { HTTP::Response->new(200, 'OK', [ content_type => 'application/json; charset=utf-8' ], '{}' ); }); my ( undef, $c ) = $client->get('some/endpoint', { -token => $url_decoded_token, }); $req = $c->http_request; }; it 'adds Bearer authorization' => sub { like $req->header('authorization'), qr/Bearer \Q$url_encoded_token/; }; }; }; runtests; Twitter-API-1.0006/t/trait/migration.t000644 000765 000024 00000016200 14031400662 017475 0ustar00marcstaff000000 000000 #!perl use 5.14.1; use warnings; use HTTP::Response; use Test::Fatal; use Test::Spec; use Test::Warnings qw/warning/; use URL::Encode qw/url_decode/; # WWW::OAuth recommends WWW::Form::UrlEncoded::XS. If it is available, but its # version number is less than WWW::Form::UrlEncoded's version, it warns and # falls back to WWW::Form::UrlEncoded::PP. That warning causes test failures. # We can avoid that by forcing use of WWW::Form:UrlEncoded::PP, which is # sufficient for tests. BEGIN { $ENV{WWW_FORM_URLENCODED_PP} = 1 } use Twitter::API; sub new_client { Twitter::API->new_with_traits( traits => 'Migration', consumer_key => 'key', consumer_secret => 'secret', ); } context 'Net::Twitter migration' => sub { my $client; before each => sub { $client = new_client; }; it 'dies with traits' => sub { like exception { Twitter::API->new( traits => [ qw/Migration WrapResult/ ], consumer_key => 'key', consumer_secret => 'secret', ); }, qr/use new_with_traits/; }; for ( [ authenticate => 'get_authentication_url' ], [ authorize => 'get_authorization_url' ] ) { my ( $endpoint, $method ) = @$_; describe $method => sub { my $uri; before each => sub { $client->stubs(oauth_request_token => { oauth_token => 'token', oauth_token_secret => 'token-secret', oauth_callback_confirmed => 'true', })->exactly(1); local $ENV{TWITTER_API_NO_MIGRATION_WARNINGS} = 1; $uri = $client->$method(callback => 'foo/bar'); }; it 'calls oauth_request_token' => sub {}; it 'sets request_token' => sub { is $client->request_token, 'token'; }; it 'sets request_token_secret' => sub { is $client->request_token_secret, 'token-secret'; }; it 'has correct scheme' => sub { is($uri->scheme, 'https'); }; it 'has correct host ' => sub { is($uri->host, 'api.twitter.com'); }; it 'has correct path' => sub { is($uri->path, "/oauth/$endpoint"); }; it 'has correct query' => sub { is_deeply { $uri->query_form }, { oauth_token => 'token', }; }; }; } for my $method ( qw/ get_authentication_url get_authorization_url request_access_token / ) { describe $method => sub { it 'has migration warning' => sub { my $client = new_client; $client->stubs('request'); like( warning { $client->$method(callback => 'nope'); }, qr/will be removed in a future release/ ); }; }; } describe get_access_token => sub { my @result; before each => sub { my $content = 'oauth_token=token&oauth_token_secret=token-secret' .'&user_id=666&screen_name=trump'; $client->user_agent->stubs(request => sub { HTTP::Response->new(200, 'OK', [ content_type => 'application/x-www-form-urlencoded', content_length => length $content, ], $content, ); }); $client->request_token('request-token'); $client->request_token_secret('request-token-secret'); local $ENV{TWITTER_API_NO_MIGRATION_WARNINGS} = 1; @result = $client->request_access_token( verifier => 'callback-verifier'); }; it 'returns (token, secret, screen_name, user_id)' => sub { is_deeply [ @result[0..3] ], [ qw/token token-secret 666 trump/ ]; }; it 'sets access_token' => sub { is $client->access_token, 'token'; }; it 'sets access_token_secret' => sub { is $client->access_token_secret, 'token-secret'; }; it 'clears request_token' => sub { ok !$client->has_request_token; }; it 'clears request_token_secret' => sub { ok !$client->has_request_token_secret; }; }; describe wrap_result => sub { before each => sub { $client = Twitter::API->new_with_traits( traits => 'Migration', wrap_result => 1, consumer_key => 'key', consumer_secret => 'secret', access_token => 'token', access_token_secret => 'token-secret', ); $client->stubs(send_request => 1); $client->stubs(inflate_response => sub { $_[1]->set_result({}); }); $client->access_token('token'); $client->access_token_secret('token-secret'); }; it 'returns a context object' => sub { local $ENV{TWITTER_API_NO_MIGRATION_WARNINGS} = 1; my $r = $client->get('some/endpoint'); ok( blessed $r && $r->isa('Twitter::API::Context')); }; it 'has migration warning' => sub { like( warning { $client->get('some/endpoint') }, qr/wrap_result is enabled/); }; }; describe ua => sub { it 'has migration warning' => sub { like warning { $client->ua }, qr/will be removed/; }; it 'returns an HTTP::Thin' => sub { local $ENV{TWITTER_API_NO_MIGRATION_WARNINGS} = 1; my $ua = $client->ua; ok blessed $ua && $ua->isa('HTTP::Thin'); }; }; }; context 'with AppAuth' => sub { # token straight out of Twitter docs my $url_encoded_token = 'AAAA%2FAAA%3DAAAAAAAA'; my $url_decoded_token = url_decode($url_encoded_token); my $client; before each => sub { $client = Twitter::API->new_with_traits( traits => [ qw/Migration AppAuth/ ], consumer_key => 'key', consumer_secret => 'secret', ); my $content = '{"token_type":"bearer","access_token"' .':"'.$url_encoded_token.'"}'; $client->user_agent->stubs(request => sub { HTTP::Response->new(200, 'OK', [ content_type => 'application/json; charset=utf-8', content_length => length $content, ], $content, ); }); }; describe request_access_token => sub { it 'client does not have an access_token before the call' => sub { ok !$client->has_access_token; }; it 'sets access_token' => sub { local $ENV{TWITTER_API_NO_MIGRATION_WARNINGS} = 1; $client->request_access_token; is $client->access_token, $url_decoded_token; }; }; }; runtests; Twitter-API-1.0006/t/trait/rate-limiting.t000644 000765 000024 00000007701 14031400662 020257 0ustar00marcstaff000000 000000 #!perl use 5.14.1; use warnings; use HTTP::Status qw(HTTP_TOO_MANY_REQUESTS); use HTTP::Response; use Test::Fatal; use Test::Spec; BEGIN { my $time = 1_500_000_000; *CORE::GLOBAL::time = sub () { my ( $caller ) = caller(); if($caller eq 'main' || $caller =~ /^Twitter::API/) { return $time++; } else { return CORE::time(); } }; } use Twitter::API; sub new_client { my $limit = shift; my $exception_info; if(@_) { $exception_info = shift; } else { $exception_info = [ HTTP_TOO_MANY_REQUESTS, 'Rate Limit Exceeded', 'x-rate-limit-reset' => time + 900 ]; } my $remaining = $limit; my $user_agent = mock(); $user_agent->stubs(request => sub { if(!defined($remaining) || $remaining-- > 0 ) { HTTP::Response->new(200, 'OK'); } else { $remaining = $limit; my ( $code, $reason, %headers ) = @$exception_info; my $res = HTTP::Response->new($code, $reason); for my $name (keys %headers) { $res->header($name, $headers{$name}); } $res } }); return Twitter::API->new_with_traits( traits => 'RateLimiting', consumer_key => 'key', consumer_secret => 'secret', access_token => 'token', access_token_secret => 'token-secret', user_agent => $user_agent, ); } describe RateLimiting => sub { it 'should be unaffected if the rate limit is never hit' => sub { my $client = new_client(); my $sleep = mock(); $sleep->expects('ping')->exactly(0); $client->rate_limit_sleep_code(sub { $sleep->ping }); $client->get('foo'); $client->get('foo'); $client->get('foo'); $client->get('foo'); pass; }; it 'should invoke the sleep callback if the rate limit is hit' => sub { my $client = new_client(2); my $n_calls = 0; $client->rate_limit_sleep_code(sub { $n_calls++ }); $client->get('foo'); $client->get('foo'); is $n_calls, 0, 'sleep should not be called before the 3rd request'; $client->get('foo'); is $n_calls, 1, 'sleep should be called exactly once after the 3rd request'; $client->get('foo'); is $n_calls, 1, 'the rate limit should have reset for the 4th request'; }; it 'should not intercept any other 4xx errors' => sub { my $client = new_client(2, [ 400, 'Unknown Error' ]); my $n_calls = 0; $client->rate_limit_sleep_code(sub { $n_calls++ }); $client->get('foo'); $client->get('foo'); like exception { $client->get('foo'); }, qr/Unknown Error/; is $n_calls, 0, 'sleep should not be called if a different 4xx error happens'; }; it 'should not intercept any 5xx errors' => sub { my $client = new_client(2, [ 500, 'Internal Server Error' ]); my $n_calls = 0; $client->rate_limit_sleep_code(sub { $n_calls++ }); $client->get('foo'); $client->get('foo'); like exception { $client->get('foo'); }, qr/Internal Server Error/; is $n_calls, 0, 'sleep should not be called if a 5xx error happens'; }; it 'should be called with the correct amount of time to sleep' => sub { my $reset_time = time + 900; my $client = new_client(2, [ HTTP_TOO_MANY_REQUESTS, 'Rate Limit Exceeded', 'x-rate-limit-reset' => $reset_time, ]); my $sleep_amount; $client->rate_limit_sleep_code(sub { ( $sleep_amount ) = @_; }); $client->get('foo'); $client->get('foo'); my $next_time = time + 1; $client->get('foo'); is $sleep_amount, ($reset_time - $next_time), 'sleep should be called with the correct time interval'; }; }; runtests; Twitter-API-1.0006/t/trait/api-methods/000755 000765 000024 00000000000 14031400662 017532 5ustar00marcstaff000000 000000 Twitter-API-1.0006/t/trait/api-methods/net-twitter-compatibility.t000644 000765 000024 00000011122 14031400662 025051 0ustar00marcstaff000000 000000 #!perl use 5.14.1; use warnings; use Ref::Util qw/is_hashref/; use Test::Spec; use HTTP::Response; use Test::Fatal; use Twitter::API; BEGIN { eval { require Net::Twitter }; plan skip_all => 'Net::Twitter >= 4.01041 not installed' if $@ || Net::Twitter->VERSION lt '4.01041'; } my %skip = map +($_ => 1), ( 'contributees', # deprecated 'contributors', # deprecated 'create_media_metadata', # described incorrectly in Net::Twitter 'destroy_direct_message', # deprecated 'direct_messages', # deprecated 'new_direct_message', # deprecated 'sent_direct_messages', # deprecated 'show_direct_message', # deprecated 'similar_places', # no longer documented 'update_delivery_device', # no longer documented 'update_profile_colors', # no longer documented 'update_with_media', # deprecated 'upload_status', # no longer documented ); # These methods are either modified with an "around" or defined incorrectly in # Net::Twitter, override what we expect for required parameters. my %override_required = ( show_user => [ ':ID' ], create_friend => [ ':ID' ], destroy_friend => [ ':ID' ], friends_ids => [ ':ID' ], followers_ids => [ ':ID' ], create_block => [ ':ID' ], destroy_block => [ ':ID' ], report_spam => [ ':ID' ], update_friendship => [ ':ID' ], create_mute => [ ':ID' ], destroy_mute => [ ':ID' ], ); # aliases for ( \%override_required ) { # damned name is too long! $_->{follow} = $_->{follow_new} = $_->{create_friendship} = $_->{create_friend}; $_->{destroy_friendship} = $_->{unfollow} = $_->{destroy_friend}; $_->{following_ids} = $_->{friends_ids}; } sub new_client { my $client = Twitter::API->new_with_traits( traits => 'ApiMethods', consumer_key => 'key', consumer_secret => 'secret', ); $client->stubs(request => sub { my ( $self, $method, $path, $args ) = @_; die 'too many args' if @_ > 4; die 'too few args' if @_ < 3; die 'final arg must be HASH' if @_ > 3 && !is_hashref($args); return ( uc $method, $args ); }); return $client; } sub http_response_ok { HTTP::Response->new( 200, 'OK', [ content_type => 'application/json;charset=utf-8', contest_length => 4, ], '{}' ); } my $nt = Net::Twitter->new(traits => [ qw/API::RESTv1_1/ ]); my @nt_methods = # We'll test all methods through their aliases, too map { my $meta = $_; my @names = ($_->name, @{ $_->aliases }); map [ $_, $meta ], @names; } sort { $a->name cmp $b->name } grep !$_->deprecated, grep $_->isa('Net::Twitter::Meta::Method'), map $_->original_method // $_, # may be wrapped $nt->meta->get_all_methods; for my $pair ( @nt_methods ) { my ( $name, $nt_method ) = @$pair; next if $skip{$nt_method->name}; describe $name => sub { my ( $client, @required ); before each => sub { $client = new_client; @required = @{ $override_required{$name} // $nt_method->required }; }; it 'method exists' => sub { ok $client->can($name); }; it 'has correct HTTP method' => sub { # path-part arguments must be passed my %must_have_args; @must_have_args{ ( $nt_method->path =~ /:(\w+)/g ), map $_ eq ':ID' ? 'screen_name' : $_, @required } = 'a' .. 'z'; my ( $http_method, undef ) = $client->$name( keys %must_have_args ? \%must_have_args : () ); is $http_method, $nt_method->method; }; it "handles ${ \(0+@required) } positional args" => sub { my @args; @args[0 .. $#required] = 'a' .. 'z'; my %expected; @expected{ map $_ eq ':ID' ? 'screen_name' : '$_', @required } = 'a' .. 'z'; my ( undef, $args ) = $client->$name(@args); is_deeply $args, \%expected; } if @required > 0; it "handles mixed positional and named args" => sub { my %args; @args{@required[1..$#required]} = 'a' .. 'z'; my %expected; @expected{ map $_ eq ':ID' ? 'screen_name' : '$_', @required } = ( 'foo', 'a' .. 'z' ); my ( undef, $args ) = $client->$name('foo', \%args); is_deeply $args, \%expected; } if @required > 1; }; } runtests; Twitter-API-1.0006/t/trait/api-methods/direct-messages.t000644 000765 000024 00000011064 14031400662 023000 0ustar00marcstaff000000 000000 #!perl use 5.14.1; use warnings; use Test::Spec; use JSON::MaybeXS qw/decode_json/; use Twitter::API; describe direct_messages => sub { my $client; my $is_stub_test; my $new_message_id; before each => sub { $client = Twitter::API->new_with_traits( traits => 'ApiMethods', consumer_key => 'key', consumer_secret => 'secret', access_token => 'token', access_token_secret => 'token-secret', ); if ($client->consumer_key eq 'key') { $client->stubs(send_request => sub { return }); $is_stub_test = 1; } }; it 'new_direct_messages_event' => sub { my $user_id_str = $client->verify_credentials()->{id_str}; my $text = 'test message ' . time; my $context = $client->new_direct_messages_event($text, $user_id_str); if ($is_stub_test) { ok $context->http_request->method eq 'POST' && $context->http_request->uri =~ m'/direct_messages/events/new\.json$' && eq_hash(decode_json($context->http_request->content), { event => { type => 'message_create', message_create => { target => { recipient_id => $user_id_str }, message_data => { text => $text }, }, }, }); $new_message_id = 'dummy'; } else { ok(exists $context->{event}); $new_message_id = $context->{event}->{id}; } }; it 'new_direct_messages_event with synthetic args' => sub { if ($is_stub_test) { my $user_id_str = '666'; my $text = 'test message ' . time; my $context = $client->new_direct_messages_event($text, $user_id_str, { -token => 'passed-token', -token_secret => 'passed-secret', }); ok $context->http_request->method eq 'POST' && $context->http_request->uri =~ m'/direct_messages/events/new\.json$' && eq_hash(decode_json($context->http_request->content), { event => { type => 'message_create', message_create => { target => { recipient_id => $user_id_str }, message_data => { text => $text }, }, }, }) && $context->http_request->header('authorization') =~ /oauth_token="passed-token"/; } }; it 'new_direct_messages_event with event paylod' => sub { if ($is_stub_test) { my $event = { type => 'message_create', message_create => { target => { recipient_id => 666 }, message_data => { text => 'test message ' . time, attachment => { type => 'media', media => { id => 1234 }, }, }, }, }; my $context = $client->new_direct_messages_event($event); ok $context->http_request->method eq 'POST' && $context->http_request->uri =~ m'/direct_messages/events/new\.json$' && eq_hash(decode_json($context->http_request->content), { event => $event }); $new_message_id = 'dummy'; } }; it 'direct_messages_events' => sub { my $context = $client->direct_messages_events(); if ($is_stub_test) { ok $context->http_request->method eq 'GET' && $context->http_request->uri =~ m'/direct_messages/events/list\.json$'; } else { ok(exists $context->{events} && @{ $context->{events} } > 0); } }; it 'show_direct_messages_event' => sub { my $context = $client->show_direct_messages_event($new_message_id); if ($is_stub_test) { ok $context->http_request->method eq 'GET' && $context->http_request->uri =~ m'/direct_messages/events/show\.json\?id=dummy$'; } else { ok(exists $context->{event}); } }; it 'destroy_direct_messages_event' => sub { my $context = $client->destroy_direct_messages_event($new_message_id); if ($is_stub_test) { ok $context->http_request->method eq 'DELETE' && $context->http_request->uri =~ m'/direct_messages/events/destroy\.json\?id=dummy$'; } else { ok($context eq ''); } }; }; runtests; Twitter-API-1.0006/t/trait/api-methods/upload-media.t000644 000765 000024 00000005426 14031400662 022267 0ustar00marcstaff000000 000000 #!perl use 5.14.1; use warnings; use Test::Spec; use Twitter::API; describe upload_media => sub { my $client; before each => sub { $client = Twitter::API->new_with_traits( traits => 'ApiMethods', consumer_key => 'key', consumer_secret => 'secret', access_token => 'token', access_token_secret => 'token-secret', ); $client->stubs(send_request => sub { return }); }; context 'positional media parameter' => sub { my $req; before each => sub { my $context = $client->upload_media( [ undef, 'file.dat', content => 'data' ], ); $req = $context->http_request; }; it 'has content-type miltipart/form-data' => sub { is $req->content_type, 'multipart/form-data'; }; it 'has part with name "media"' => sub { my ( $part ) = $req->parts; my $disposition = $part->header('content_disposition'); like $disposition, qr/\bname="media"/; }; it 'has part with filename' => sub { my ( $part ) = $req->parts; my $disposition = $part->header('content_disposition'); like $disposition, qr/\bfilename="file\.dat"/; }; it 'has part with expected content' => sub { my ( $part ) = $req->parts; is $part->content, 'data'; }; }; context 'positional media_data parameter' => sub { my $req; before each => sub { my $context = $client->upload_media( 'base64 data here', ); $req = $context->http_request; }; it 'has content-type miltipart/form-data' => sub { is $req->content_type, 'multipart/form-data'; }; it 'has part with name "media_data"' => sub { my ( $part ) = $req->parts; my $disposition = $part->header('content_disposition'); like $disposition, qr/\bname="media_data"/; }; it 'has part with expected content' => sub { my ( $part ) = $req->parts; is $part->content, 'base64 data here'; }; }; context 'with additional_owners' => sub { my $req; before each => sub { my $context = $client->upload_media({ media_data => 'test', additional_owners => [ 1..5 ], }); $req = $context->http_request; }; it 'flattens additional_owners' => sub { my ( $part ) = grep { $_->header('content_disposition') =~ /\bname="additional_owners"/; } $req->parts; is $part->content, '1,2,3,4,5'; }; }; }; runtests; Twitter-API-1.0006/t/trait/api-methods/invalidate-access-token.t000644 000765 000024 00000004410 14031400662 024413 0ustar00marcstaff000000 000000 #!perl use warnings; use Test::Spec; use Twitter::API; describe invalidate_access_token => sub { my $client; before each => sub { $client = Twitter::API->new_with_traits( traits => 'ApiMethods', consumer_key => 'key', consumer_secret => 'secret', access_token => 'token', access_token_secret => 'token-secret', ); $client->stubs(send_request => sub { return }); }; it 'inferrs token/secret' => sub { my $c = $client->invalidate_access_token; my $req = $c->http_request; my $params = do { my $uri = URI->new; $uri->query($req->decoded_content); +{ $uri->query_form }; }; ok $req->method eq 'POST' && $req->uri->path =~ m{/oauth/invalidate_token\.json$} && eq_hash($params, { access_token => 'token', access_token_secret => 'token-secret', }); }; it 'accepts synthetic token/secret args' => sub { my $c = $client->invalidate_access_token({ -token => 'token', -token_secret => 'token-secret', }); my $req = $c->http_request; my $params = do { my $uri = URI->new; $uri->query($req->decoded_content); +{ $uri->query_form }; }; ok $req->method eq 'POST' && $req->uri->path =~ m{/oauth/invalidate_token\.json$} && eq_hash($params, { access_token => 'token', access_token_secret => 'token-secret', }); }; it 'accepts access_token/access_token_secret args' => sub { my $c = $client->invalidate_access_token({ access_token => 'token', access_token_secret => 'token-secret', }); my $req = $c->http_request; my $params = do { my $uri = URI->new; $uri->query($req->decoded_content); +{ $uri->query_form }; }; ok $req->method eq 'POST' && $req->uri->path =~ m{/oauth/invalidate_token\.json$} && eq_hash($params, { access_token => 'token', access_token_secret => 'token-secret', }); }; }; runtests; Twitter-API-1.0006/t/trait/api-methods/update.t000644 000765 000024 00000001426 14031400662 021204 0ustar00marcstaff000000 000000 #!perl use 5.14.1; use warnings; use Test::Spec; use Twitter::API; describe update => sub { my $client; before each => sub { $client = Twitter::API->new_with_traits( traits => 'ApiMethods', consumer_key => 'key', consumer_secret => 'secret', access_token => 'token', access_token_secret => 'token-secret', ); $client->stubs(send_request => sub { return }); }; it 'flattens media_ids' => sub { my $req; my $context = $client->update('hello world', { media_ids => [ 1..3 ], }); is_deeply $context->args, { status => 'hello world', media_ids => '1,2,3', }; }; }; runtests; Twitter-API-1.0006/t/role/request-args.t000644 000765 000024 00000006563 14031400662 017757 0ustar00marcstaff000000 000000 #!perl use 5.14.1; use strict; use Test::Fatal; use Test::Spec; package Foo { use Moo; use Carp; use Ref::Util qw/is_hashref/; with 'Twitter::API::Role::RequestArgs'; sub request { my ( $self, $http_method, $url, $args ) = @_; croak 'too many args' if @_ > 4; croak 'too few args' if @_ < 4; croak 'final arg must be HASH' unless is_hashref($args); return $args; } } describe request_with_pos_args => sub { my $client; before each => sub { $client = Foo->new; }; it 'croaks without args' => sub{ like exception { $client->request_with_pos_args([':ID'], 'get', 'path'); }, qr/missing required screen_name or user_id/; }; it ':ID croaks with both screen_name or user_id' => sub { exception { $client->request_with_pos_args([':ID'], GET => 'path', { screen_name => 'twinsies', user_id => '666', }); }, qr/only one of screen_name or user_id allowed/; }, it 'croaks with too many args' => sub { like exception { $client->request_with_pos_args([':ID'], 'GET', 'path', 'who', 'extra'); }, qr/too many args/; }, it ':ID croaks without user_id or screen_name' => sub { like exception { $client->request_with_pos_args([':ID'], GET => 'path', { foo => 'bar' }); }, qr/missing required screen_name or user_id/; }, it 'croaks without required args' => sub { like exception { $client->request_with_pos_args([ qw/foo bar/ ], GET => 'path', { foo => 'baz', }); }, qr/missing required 'bar' arg/; }, it 'croaks with duplicate args' => sub { like exception { $client->request_with_pos_args(['foo'], GET => 'path', 'bar', { foo => 'baz', }); }, qr/'foo' specified in both positional and named args/; }; it 'croaks with duplicate :ID' => sub { like exception { $client->request_with_pos_args([':ID'], GET => 'path', 'bar', { screen_name => 'baz', }); }, qr/'screen_name' specified in both positional and named args/; }; it ':ID handles user_id' => sub { my $args = $client->request_with_pos_args([':ID'], GET => 'path', 666, { foo => 'bar', }); is_deeply $args, { user_id => 666, foo => 'bar' }; }; it ':ID handles screen_name' => sub { my $args = $client->request_with_pos_args([':ID'], GET => 'path', 'evil', { foo => 'bar', }); is_deeply $args, { screen_name => 'evil', foo => 'bar' }; }; it 'handles pos args in the hash' => sub { my $args = $client->request_with_pos_args([ qw/foo bar/ ], GET => 'path', 'baz', { bar => 'bop', and => 'more' } ); is_deeply $args, { foo => 'baz', bar => 'bop', and => 'more' }; }; describe 'request_with_id' => sub { it 'handles optional :ID when it exists' => sub { my $args = $client->request_with_id(get => 'path', 'just_me'); is_deeply $args, { screen_name => 'just_me' }; }, it 'handles optional :ID when it does not exist' => sub { my $args = $client->request_with_id(get => 'path'); is_deeply $args, {}; }, }; }; runtests;