├── .babelrc
├── .eslintrc
├── .gitattributes
├── .gitignore
├── .travis.yml
├── CHANGELOG
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── bower.json
├── codebird.es7.js
├── codebird.js
├── package-lock.json
├── package.json
└── test
├── README
├── codebirdm.es7.js
├── codebirdt.es7.js
├── detection_tests.js
├── functional_tests.js
├── media_tests.js
└── oauth_tests.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015"]
3 | }
4 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | // http://eslint.org/docs/rules/
3 |
4 | "ecmaFeatures": {
5 | "binaryLiterals": false, // enable binary literals
6 | "blockBindings": true, // enable let and const (aka block bindings)
7 | "defaultParams": false, // enable default function parameters
8 | "forOf": true, // enable for-of loops
9 | "generators": false, // enable generators
10 | "objectLiteralComputedProperties": false, // enable computed object literal property names
11 | "objectLiteralDuplicateProperties": false, // enable duplicate object literal properties in strict mode
12 | "objectLiteralShorthandMethods": false, // enable object literal shorthand methods
13 | "objectLiteralShorthandProperties": false, // enable object literal shorthand properties
14 | "octalLiterals": false, // enable octal literals
15 | "regexUFlag": false, // enable the regular expression u flag
16 | "regexYFlag": false, // enable the regular expression y flag
17 | "templateStrings": true , // enable template strings
18 | "unicodeCodePointEscapes": false, // enable code point escapes
19 | "jsx": false, // enable JSX
20 | "modules": true
21 | },
22 |
23 | "env": {
24 | "browser": true, // browser global variables.
25 | "node": true, // Node.js global variables and Node.js-specific rules.
26 | "amd": true, // defines require() and define() as global variables as per the amd spec.
27 | "es6": true // EcmaScript 6
28 | },
29 |
30 | "globals": {
31 | // e.g. "angular": true
32 | },
33 |
34 | "plugins": [
35 | // e.g. "react" (must run `npm install eslint-plugin-react` first)
36 | ],
37 |
38 | "rules": {
39 | ////////// Possible Errors //////////
40 |
41 | "valid-typeof": 2, // Ensure that the results of typeof are compared against a valid string
42 |
43 |
44 | ////////// Best Practices //////////
45 |
46 | "eqeqeq": 2, // require the use of === and !==
47 | "no-alert": 2, // disallow the use of alert, confirm, and prompt
48 |
49 | ////////// Stylistic Issues //////////
50 |
51 | "eol-last": 2, // enforce newline at the end of file, with no multiple empty lines
52 | "no-lonely-if": 2, // disallow if as the only statement in an else block (off by default)
53 | "no-mixed-spaces-and-tabs": 2, // disallow mixed spaces and tabs for indentation
54 | "no-multiple-empty-lines": 2, // disallow multiple empty lines (off by default)
55 | "no-trailing-spaces": 2, // disallow trailing whitespace at the end of lines
56 |
57 | "quotes": [
58 | 2,
59 | "double"
60 | ]
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | RELEASE_MESSAGE*
4 | test/codebirdm.js
5 | test/codebirdt.js
6 | test*.html
7 | *.jpg
8 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | # The Travis setup:
2 | # - run lint for every JS version
3 | # - run testsuite for every JS version
4 |
5 | language: node_js
6 |
7 | node_js:
8 | - "8"
9 | - "9"
10 | - "10"
11 |
12 | sudo: false
13 |
14 | script:
15 | - ./node_modules/eslint/bin/eslint.js codebird.js
16 | - npm test
17 |
18 | matrix:
19 | fast_finish: true
20 |
21 | # trigger Buildtime Trend Service to parse Travis CI log
22 | notifications:
23 | webhooks:
24 | - https://buildtimetrend.herokuapp.com/travis
25 |
--------------------------------------------------------------------------------
/CHANGELOG:
--------------------------------------------------------------------------------
1 | codebird-js - changelog
2 | =======================
3 |
4 | 3.0.0 (not yet released)
5 | + #106 Add error callback
6 | + #96 Add logout method
7 | + #98 Support promises as alternative to callbacks
8 | - Drop support for undocumented API methods
9 | + Add security check hasOwnProperty
10 | + #110 Add support for Collections API
11 | + Transform codebase to EcmaScript 7
12 | + #25 Add unit testing suite
13 | + Add POST statuses/unretweet/:id
14 | + #116 Update GET statuses/oembed with new endpoint
15 | + Support POST media/upload with media_data base64
16 | - Fix check for require for webpack compatibility
17 | + Add Account Activity API methods
18 | + Add new Direct Messages API
19 | - Remove sunsetting Direct Messages API
20 | - Remove contributor API methods
21 | - Remove deprecated statuses/update_with_media method
22 | - Remove deprecated statuses/update_profile_background_image method
23 | + Detect XML error replies (re-implemented detection)
24 | + Add oauth/invalidate_token method
25 |
26 | 2.6.0 (2015-04-08)
27 | + Allow to get the supported API methods as array
28 | - #79 Use POST for users/lookup and statuses/lookup, params may get too long for GET
29 | - #81 Special oauth_ methods: Fix force_login parameter
30 |
31 | 2.5.0 (2014-06-23)
32 | + #24 Add README section about xAuth
33 | + #46 Don't force to send base64 media uploads via proxy
34 | + #23 Support internal API methods
35 | + #50 Current rate sent back with each call
36 | + Support custom timelines beta API
37 | - Remove POST geo/place API method, deprecated
38 | + #52 Don't use proxy for non-browser environments
39 | - #49 Places & Geo API call issue
40 | + #53 Support AMD module definition syntax
41 | + #54 Integrate sha1 hash library
42 | + Add NPM package.json
43 | - #55 Typo in _clone function
44 | - #57 Fix XHR in Safari
45 | + Add new API methods
46 | + #60 Multiple media per tweet
47 | + #66 Support Titanium
48 | - #71 Return rate-limiting info only if contained in response
49 | - #72 Don't require consumer key if bearer token is already known
50 |
51 | 2.4.3 (2013-07-24)
52 | - #31 HTTP error on multiple oauth_requestToken calls
53 | - #30 HTML-escaping typo in readme code sample
54 |
55 | 2.4.2 (2013-07-09)
56 | - #21 Typo extra bracket in readme sample code
57 | - #27 Array bad behavior in iOS
58 |
59 | 2.4.1 (2013-06-23)
60 | + Pass JSHint validation
61 | + #15 Allow to assign custom proxy address
62 | - #19 API parameters with escaped characters fail in IE7-9
63 |
64 | 2.4.0 (2013-06-15)
65 | + Support multipart file uploads
66 | - Drop unnecessary media format settings ported from PHP getimagesize
67 | - Don't unintendedly overwrite global "url" variable
68 | + Add contributing guidelines
69 |
70 | 2.3.3 (2013-05-31)
71 | - oauth2_token doesn't respect _useProxy CORS setting
72 | - #10 setBearerToken was not public
73 |
74 | 2.3.2 (2013-05-21)
75 | - Sorting the OAuth parameter list in IE incorrectly
76 | - Undefined function c for debug logging
77 | + Correctly detect templated methods and insert their parameters
78 |
79 | 2.3.1 (2013-04-26)
80 | - Wrong OAuth signature generation for POST requests with oauth_callback
81 | - Wrong _ksort implementation
82 |
83 | 2.3.0 (2013-04-26)
84 | - Bug in _detectMultipart for statuses/update calls
85 | + Add Array.indexOf polyfill
86 | + Circumvent same-origin policy with CORS
87 | + Make oauth_authenticate and oauth_authorize asynchronous
88 | + Send GET requests via JSONP for IE7-9 (don't support CORS)
89 |
90 | 2.2.3 (2012-12-03)
91 | + Detect HTTP method properly when accessing multi HTTP method endpoint (account/settings)
92 | + Add friends/list, followers/list API methods
93 | + Move users/recommendations to GET list
94 |
95 | 2.2.2 (2012-10-19)
96 | + Update methods list for API 1.1
97 | + Add profile banner methods
98 | - Drop separate media upload method
99 | - Remove OAuth token message. 1.1 is always signed.
100 | + Update endpoints to 1.1 endpoints, add support for old endpoints
101 |
102 | 2.2.1 (2012-09-17)
103 | + Update to Twitter API v1.1
104 |
105 | 2.2.0 (2012-09-17)
106 | + Port Codebird from PHP to JavaScript
107 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ### Which branch?
2 | - Critical bugs are fixed in the master branch.
3 | - Normal bugs are fixed in the develop branch.
4 | - New features are added to the develop branch, too.
5 |
6 | ### Which file?
7 | - Please apply all edits to the `codebird.es7.js` file.
8 | The codebird.js file is transpiled from it using BabelJS.
9 |
10 | ### Code style
11 | - Please use 2 soft spaces per indent level.
12 | - Take a look at the coding style used in `codebird.es7.js` and apply the same convention to your contributed code.
13 |
14 | ### License
15 | - Code contributed by you will get the same license as Codebird itself, that is, GPU General Public License V3.
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # codebird-js
2 |
3 | _A Twitter library in JavaScript._
4 |
5 | Copyright (C) 2010-2018 Jublo Limited
6 |
7 | This program is free software: you can redistribute it and/or modify
8 | it under the terms of the GNU General Public License as published by
9 | the Free Software Foundation, either version 3 of the License, or
10 | (at your option) any later version.
11 |
12 | This program is distributed in the hope that it will be useful,
13 | but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | GNU General Public License for more details.
16 |
17 | You should have received a copy of the GNU General Public License
18 | along with this program. If not, see .
19 |
20 | [](https://travis-ci.org/jublonet/codebird-js/branches)
21 |
22 | ## Including Codebird
23 |
24 | To include Codebird in your code, add its scripts to your markup:
25 |
26 | ```html
27 |
28 |
29 |
33 | ```
34 |
35 | You may also use a JavaScript module loader of your choice
36 | (such as [RequireJS](http://requirejs.org/) or the one bundled in Node.js)
37 | to load Codebird unobtrusively. In Node.js, loading Codebird looks like this:
38 |
39 | ```javascript
40 | var Codebird = require("codebird");
41 | // or with leading "./", if the codebird.js file is in your main folder:
42 | // var Codebird = require("./codebird");
43 |
44 | var cb = new Codebird();
45 | cb.setConsumerKey("YOURKEY", "YOURSECRET");
46 | ```
47 |
48 | ## Authentication
49 |
50 | To authenticate your API requests on behalf of a certain Twitter user
51 | (following OAuth 1.0a), take a look at these steps:
52 |
53 | ```html
54 |
55 |
56 |
60 | ```
61 |
62 | You may either set the OAuth token and secret, if you already have them:
63 |
64 | ```javascript
65 | cb.setToken("YOURTOKEN", "YOURTOKENSECRET");
66 | ```
67 |
68 | Or you authenticate, like this:
69 |
70 | ```javascript
71 | // gets a request token
72 | cb.__call("oauth_requestToken", { oauth_callback: "oob" }, function(
73 | reply,
74 | rate,
75 | err
76 | ) {
77 | if (err) {
78 | console.log("error response or timeout exceeded" + err.error);
79 | }
80 | if (reply) {
81 | if (reply.errors && reply.errors["415"]) {
82 | // check your callback URL
83 | console.log(reply.errors["415"]);
84 | return;
85 | }
86 |
87 | // stores the token
88 | cb.setToken(reply.oauth_token, reply.oauth_token_secret);
89 |
90 | // gets the authorize screen URL
91 | cb.__call("oauth_authorize", {}, function(auth_url) {
92 | window.codebird_auth = window.open(auth_url);
93 | });
94 | }
95 | });
96 | ```
97 |
98 | :warning: Codebird server calls do not always go through when
99 | being processed in a hyperlink onclick handler. Be sure to cancel
100 | the default procedure before calling Codebird, like this (jQuery):
101 |
102 | ```javascript
103 | $(function() {
104 |
105 | $('#auth').click(function(e) {
106 | e.preventDefault();
107 |
108 | var cb = new Codebird;
109 | // ...
110 | ```
111 |
112 | Now you need to add a PIN box to your website.
113 | After the user enters the PIN, complete the authentication:
114 |
115 | ```javascript
116 | cb.__call(
117 | "oauth_accessToken",
118 | { oauth_verifier: document.getElementById("PINFIELD").value },
119 | function(reply, rate, err) {
120 | if (err) {
121 | console.log("error response or timeout exceeded" + err.error);
122 | }
123 | if (reply) {
124 | // store the authenticated token, which may be different from the request token (!)
125 | cb.setToken(reply.oauth_token, reply.oauth_token_secret);
126 | }
127 |
128 | // if you need to persist the login after page reload,
129 | // consider storing the token in a cookie or HTML5 local storage
130 | }
131 | );
132 | ```
133 |
134 | ### Logging out
135 |
136 | In case you want to log out the current user (to log in a different user without
137 | creating a new Codebird object), just call the `logout()` method.
138 |
139 | ```javascript
140 | cb.logout().then(() => {
141 | // user is now logged out
142 | });
143 | ```
144 |
145 | Codebird also supports calling the oauth/invalidate_token method directly:
146 |
147 | ```
148 | cb.__call("oauth_invalidateToken", {
149 | access_key: "1234",
150 | access_key_secret: "5678"
151 | }).then(() => {
152 | // tokens are now reset
153 | });
154 | ```
155 |
156 | ### Application-only auth
157 |
158 | Some API methods also support authenticating on a per-application level.
159 | This is useful for getting data that are not directly related to a specific
160 | Twitter user, but generic to the Twitter ecosystem (such as `search/tweets`).
161 |
162 | To obtain an app-only bearer token, call the appropriate API:
163 |
164 | ```javascript
165 | cb.__call("oauth2_token", {}, function(reply, err) {
166 | var bearer_token;
167 | if (err) {
168 | console.log("error response or timeout exceeded" + err.error);
169 | }
170 | if (reply) {
171 | bearer_token = reply.access_token;
172 | }
173 | });
174 | ```
175 |
176 | I strongly recommend that you store the obtained bearer token in your database.
177 | There is no need to re-obtain the token with each page load, as it becomes invalid
178 | only when you call the `oauth2/invalidate_token` method.
179 |
180 | If you already have your token, tell Codebird to use it:
181 |
182 | ```javascript
183 | cb.setBearerToken("YOURBEARERTOKEN");
184 | ```
185 |
186 | In this case, you don't need to set the consumer key and secret.
187 | For sending an API request with app-only auth, see the ‘Usage examples’ section.
188 |
189 | ### Authenticating using a callback URL, without PIN
190 |
191 | 1. Before sending your user off to Twitter, you have to store the request token and its secret, for example in a cookie.
192 | 2. In the callback URL, extract those values and assign them to the Codebird object.
193 | 3. Extract the `oauth_verifier` field from the request URI.
194 |
195 | In Javascript, try extracting the URL parameter like this:
196 |
197 | ```javascript
198 | var cb = new Codebird();
199 | var current_url = location.toString();
200 | var query = current_url.match(/\?(.+)$/).split("&");
201 | var parameters = {};
202 | var parameter;
203 |
204 | cb.setConsumerKey("STUFF", "HERE");
205 |
206 | for (var i = 0; i < query.length; i++) {
207 | parameter = query[i].split("=");
208 | if (parameter.length === 1) {
209 | parameter[1] = "";
210 | }
211 | parameters[decodeURIComponent(parameter[0])] = decodeURIComponent(
212 | parameter[1]
213 | );
214 | }
215 |
216 | // check if oauth_verifier is set
217 | if (typeof parameters.oauth_verifier !== "undefined") {
218 | // assign stored request token parameters to codebird here
219 | // ...
220 | cb.setToken(
221 | stored_somewhere.oauth_token,
222 | stored_somewhere.oauth_token_secret
223 | );
224 |
225 | cb.__call(
226 | "oauth_accessToken",
227 | {
228 | oauth_verifier: parameters.oauth_verifier
229 | },
230 | function(reply, rate, err) {
231 | if (err) {
232 | console.log("error response or timeout exceeded" + err.error);
233 | }
234 | if (reply) {
235 | cb.setToken(reply.oauth_token, reply.oauth_token_secret);
236 | }
237 |
238 | // if you need to persist the login after page reload,
239 | // consider storing the token in a cookie or HTML5 local storage
240 | }
241 | );
242 | }
243 | ```
244 |
245 | ## Usage examples
246 |
247 | :warning: _Because the Consumer Key and Token Secret are available in the code,
248 | it is important that you configure your app as read-only at Twitter,
249 | unless you are sure to know what you are doing._
250 |
251 | When you have an access token, calling the API is simple:
252 |
253 | ```javascript
254 | cb.setToken("YOURTOKEN", "YOURTOKENSECRET"); // see above
255 |
256 | cb.__call("statuses_homeTimeline", {}, function(reply, rate, err) {
257 | console.log(reply);
258 | console.log(err);
259 | });
260 | ```
261 |
262 | Tweeting is as easy as this:
263 |
264 | ```javascript
265 | cb.__call("statuses_update", { status: "Whohoo, I just tweeted!" }, function(
266 | reply,
267 | rate,
268 | err
269 | ) {
270 | // ...
271 | });
272 | ```
273 |
274 | :warning: _Make sure to urlencode any parameter values that contain
275 | query-reserved characters, like tweeting the `&` sign:_
276 |
277 | ```javascript
278 | var params = "status=" + encodeURIComponent("Fish & chips");
279 | cb.__call("statuses_update", params, function(reply, rate, err) {
280 | // ...
281 | });
282 | ```
283 |
284 | In most cases, giving all parameters in an array is easier,
285 | because no encoding is needed:
286 |
287 | ```javascript
288 | var params = {
289 | status: "Fish & chips"
290 | };
291 | cb.__call("statuses_update", params, function(reply, rate, err) {
292 | // ...
293 | });
294 | ```
295 |
296 | ```javascript
297 | var params = {
298 | screen_name: "jublonet"
299 | };
300 | cb.__call("users_show", params, function(reply, rate, err) {
301 | // ...
302 | });
303 | ```
304 |
305 | ```javascript
306 | var params = {
307 | q: "NYC"
308 | };
309 | cb.__call("search_tweets", params, function(reply) {
310 | // ...
311 | });
312 | ```
313 |
314 | ### Uploading media to Twitter
315 |
316 | Tweet media can be uploaded in a 2-step process, and the media have to be
317 | base64-encoded. **First** you send each image to Twitter, like this:
318 |
319 | ```javascript
320 | var params = {
321 | "media_data": "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAB+0lEQVR42mP8//8/Ay0BEwONwagFoxZQDljI0PP8x7/Z93/e+PxXmpMpXp5dh4+ZgYHh0bd/clxYnMuINaMtfvRLgp3RVZwVU+rkuz+eRz+//wXVxcrEkKnEceXTX0dRlhoNTmKDaOvzXwHHv6x9+gtN/M9/hpjTX+GmMzAw/P7HMOnOj+ff//35x/Ds+z9iLfjPwPDt7//QE1/Sz319/RNh3PkPf+58+Yup/t7Xf9p8zFKcTMRa4CLGCrFm1v2fSjs+pJ/7uuvl7w+//yO7HRkUq3GEyrCREMk+kqy2IiyH3/xhYGD48uf/rPs/Z93/yczIwM3CiFU9Hw5xnD4ouvTt4Tf0AP37n+HTb+w+UOBmIs2CICm2R9/+EZlqGRkYzIVYSLMgRIYtUYGdSAsMBFgUuJhIy2iMDAwt2pysjAwLHv78RcgnOcrs5BQVHEyMG579Imi6Nh9zrBxZFgixMW624pXnwldYcTAzLjDhZmUit7AzE2K54c7fp8eF1QhWRobFptwmgiwkF3b//jMwMjJ8+P3/zPs/yx/9Wvr412+MgBJlZ1xsyuOOrbAibMHH3/87b32fce/nR2ypnpuFMVGevU6TQ5SdqKKeEVez5cuf/7te/j727s+9L/++/v3PzcyowM1kIcTiLs7Kz8pIfNnOONouGrVg1AIGAJ6gvN4J6V9GAAAAAElFTkSuQmCC"
322 | );
323 | cb.__call(
324 | "media_upload",
325 | params,
326 | function (reply, rate, err) {
327 | // you get a media id back:
328 | console.log(reply.media_id_string);
329 |
330 | // continue upload of 2nd image here, if any (just 1 image works, too!)
331 | }
332 | );
333 | ```
334 |
335 | **Second,** you attach the collected media ids for all images to your call
336 | to `statuses/update`, like this:
337 |
338 | ```javascript
339 | cb.__call(
340 | "statuses_update",
341 | {
342 | "media_ids": "12345678901234567890,9876543210987654321"
343 | "status": "Whohoo, I just tweeted two images!"
344 | },
345 | function (reply, rate, err) {
346 | // ...
347 | }
348 | );
349 | ```
350 |
351 | More [documentation for uploading media](https://developer.twitter.com/en/docs/media/upload-media/overview) is available on the Twitter Developer site.
352 |
353 | ### Requests with app-only auth
354 |
355 | To send API requests without an access token for a user (app-only auth),
356 | add another parameter to your method call, like this:
357 |
358 | ```javascript
359 | cb.__call(
360 | "search_tweets",
361 | "q=Twitter",
362 | function(reply) {
363 | // ...
364 | },
365 | true // this parameter required
366 | );
367 | ```
368 |
369 | Bear in mind that not all API methods support application-only auth.
370 |
371 | ## Mapping API methods to Codebird function calls
372 |
373 | As you can see from the last example, there is a general way how Twitter’s API methods
374 | map to Codebird function calls. The general rules are:
375 |
376 | 1. For each slash in a Twitter API method, use an underscore in the Codebird function.
377 |
378 | Example: `statuses/update` maps to `cb.__call("statuses_update", ...)`.
379 |
380 | 2. For each underscore in a Twitter API method, use camelCase in the Codebird function.
381 |
382 | Example: `statuses/home_timeline` maps to `cb.__call("statuses_homeTimeline", ...)`.
383 |
384 | 3. For each parameter template in method, use UPPERCASE in the Codebird function.
385 | Also don’t forget to include the parameter in your parameter list.
386 |
387 | Examples:
388 |
389 | - `statuses/show/:id` maps to `cb.__call("statuses_show_ID", 'id=12345', ...)`.
390 | - `users/profile_image/:screen_name` maps to
391 | `cb.__call("users_profileImage_SCREEN_NAME", "screen_name=jublonet", ...)`.
392 |
393 | ## HTTP methods (GET, POST, DELETE etc.)
394 |
395 | Never care about which HTTP method (verb) to use when calling a Twitter API.
396 | Codebird is intelligent enough to find out on its own.
397 |
398 | ## Response codes
399 |
400 | The HTTP response code that the API gave is included in any return values.
401 | You can find it within the return object’s `httpstatus` property.
402 |
403 | ### Dealing with rate-limits
404 |
405 | Basically, Codebird leaves it up to you to handle Twitter’s rate limit.
406 | The library returns the response HTTP status code, so you can detect rate limits.
407 |
408 | I suggest you to check if the `reply.httpstatus` property is `400`
409 | and check with the Twitter API to find out if you are currently being
410 | rate-limited.
411 | See the [Rate Limiting FAQ](https://developer.twitter.com/en/docs/basics/rate-limiting)
412 | for more information.
413 |
414 | If you allow your callback function to accept a second parameter,
415 | you will receive rate-limiting details in this parameter,
416 | if the Twitter API responds with rate-limiting HTTP headers.
417 |
418 | ```javascript
419 | cb.__call("search_tweets", "q=Twitter", function(reply, rate_limit_status) {
420 | console.log(rate_limit_status);
421 | // ...
422 | });
423 | ```
424 |
425 | ## API calls and the same-origin policy
426 |
427 | Normally, browsers only allow requests being sent to addresses that are on
428 | the same base domain. This is a security feature called the “same-origin
429 | policy.” However, this policy is in your way when you try to access the
430 | (remote) Twitter API domain and its methods.
431 |
432 | ### Cross-domain requests
433 |
434 | With Codebird, don’t worry about this. We automatically send cross-domain
435 | requests using a secured proxy that sends back the required headers to the
436 | user’s browser.
437 |
438 | This CORS proxy is using an encrypted SSL connection.
439 | _We do not record data sent to or from the Twitter API.
440 | Using Codebird’s CORS proxy is subject to the Acceptable use policy._
441 |
442 | If your JavaScript environment is not restricted under the same-origin policy
443 | (for example in node.js), direct connections to the Twitter API are established
444 | automatically, instead of contacting the CORS proxy.
445 |
446 | You may also turn off the CORS compatibility manually like this:
447 |
448 | ```javascript
449 | cb.setUseProxy(false);
450 | ```
451 |
452 | ### Support for Internet Explorer 7 to 9
453 |
454 | Cross-domain requests work well in any browser except for
455 | Internet Explorer 7-9. Codebird cannot send POST requests in these browsers.
456 | For IE7-9, Codebird works in limited operation mode:
457 |
458 | - Calls to GET methods work fine,
459 | - calling POST methods is impossible,
460 | - Application-only auth does not work.
461 |
462 | ### Using your own proxy server
463 |
464 | The source code of the CORS proxy is publicly available. If you want to,
465 | set up your own instance on your server. Afterwards, tell Codebird the
466 | address:
467 |
468 | ```javascript
469 | cb.setProxy("https://example.com/codebird-cors-proxy/");
470 | ```
471 |
472 | Heads up! Follow the notes in the [codebird-cors-proxy README](https://github.com/jublonet/codebird-cors-proxy/#readme) for details.
473 |
474 | ## Using multiple Codebird instances
475 |
476 | By default, each Codebird instance works on its own.
477 |
478 | If you need to run requests to the Twitter API for multiple users at once,
479 | Codebird supports this automatically. Just create a new object:
480 |
481 | ```javascript
482 | var cb1 = new Codebird();
483 | var cb2 = new Codebird();
484 | ```
485 |
486 | Please note that your OAuth consumer key and secret is shared within
487 | multiple Codebird instances, while the OAuth request and access tokens with their
488 | secrets are _not_ shared.
489 |
490 | ## How Do I…?
491 |
492 | ### …get user ID, screen name and more details about the current user?
493 |
494 | When the user returns from the authentication screen, you need to trade
495 | the obtained request token for an access token, using the OAuth verifier.
496 | As discussed in the section ‘Usage example,’ you use a call to
497 | `oauth/access_token` to do that.
498 |
499 | The API reply to this method call tells you details about the user that just logged in.
500 | These details contain the **user ID** and the **screen name.**
501 |
502 | Take a look at the returned data as follows:
503 |
504 | ```javascript
505 | {
506 | oauth_token: "14648265-rPn8EJwfB**********************",
507 | oauth_token_secret: "agvf3L3**************************",
508 | user_id: 14648265,
509 | screen_name: "jublonet",
510 | httpstatus: 200
511 | }
512 | ```
513 |
514 | If you need to get more details, such as the user’s latest tweet,
515 | you should fetch the complete User Entity. The simplest way to get the
516 | user entity of the currently authenticated user is to use the
517 | `account/verify_credentials` API method. In Codebird, it works like this:
518 |
519 | ```javascript
520 | cb.__call("account_verifyCredentials", {}, function(reply) {
521 | console.log(reply);
522 | });
523 | ```
524 |
525 | I suggest to cache the User Entity after obtaining it, as the
526 | `account/verify_credentials` method is rate-limited by 15 calls per 15 minutes.
527 |
528 | ### …walk through cursored results?
529 |
530 | The Twitter REST API utilizes a technique called ‘cursoring’ to paginate
531 | large result sets. Cursoring separates results into pages of no more than
532 | 5000 results at a time, and provides a means to move backwards and
533 | forwards through these pages.
534 |
535 | Here is how you can walk through cursored results with Codebird.
536 |
537 | 1. Get the first result set of a cursored method:
538 |
539 | ```javascript
540 | cb.__call("followers_list", {}, function(result1) {
541 | // ...
542 | });
543 | ```
544 |
545 | 2. To navigate forth, take the `next_cursor_str`:
546 |
547 | ```javascript
548 | var nextCursor = result1.next_cursor_str;
549 | ```
550 |
551 | 3. If `nextCursor` is not 0, use this cursor to request the next result page:
552 |
553 | ```javascript
554 | if (nextCursor > 0) {
555 | cb.__call("followers_list", { cursor: nextCursor }, function(result2) {
556 | // ...
557 | });
558 | }
559 | ```
560 |
561 | To navigate back instead of forth, use the field `resultX.previous_cursor_str`
562 | instead of `next_cursor_str`.
563 |
564 | It might make sense to use the cursors in a loop. Watch out, though,
565 | not to send more than the allowed number of requests to `followers/list`
566 | per rate-limit timeframe, or else you will hit your rate-limit.
567 |
568 | ### …use xAuth with Codebird?
569 |
570 | Codebird supports xAuth just like every other authentication used at Twitter.
571 | Remember that your application needs to be whitelisted to be able to use xAuth.
572 |
573 | Here’s an example:
574 |
575 | ```javascript
576 | cb.__call(
577 | "oauth_accessToken",
578 | {
579 | x_auth_username: "username",
580 | x_auth_password: "4h3_p4$$w0rd",
581 | x_auth_mode: "client_auth"
582 | },
583 | function(reply) {
584 | console.log(reply);
585 | // ...
586 | }
587 | );
588 | ```
589 |
590 | If everything went fine, you will get an object like this:
591 |
592 | ```javascript
593 | {
594 | "oauth_token": "14648265-ABLfBFlE*********************************",
595 | "oauth_token_secret": "9yTBY3pEfj*********************************",
596 | "user_id": "14648265",
597 | "screen_name": "jublonet",
598 | "x_auth_expires": "0",
599 | "httpstatus": 200
600 | }
601 | ```
602 |
603 | Are you getting a strange error message, an empty error, or status "0"?
604 | If the user is enrolled in login verification, the server will return a
605 | HTTP 401 error with a custom body (that may be filtered by your browser).
606 |
607 | You may check the browser web console for an error message.
608 |
609 | When this error occurs, advise the user to
610 | [generate a temporary password](https://twitter.com/settings/applications)
611 | on twitter.com and use that to complete signing in to the application.
612 |
613 | ### …access the Collections API?
614 |
615 | Collections are a type of timeline that you control and can be hand curated
616 | and/or programmed using an API.
617 |
618 | Pay close attention to the differences in how collections are presented —
619 | often they will be decomposed, efficient objects with information about users,
620 | Tweets, and timelines grouped, simplified, and stripped of unnecessary repetition.
621 |
622 | Never care about the OAuth signing specialities and the JSON POST body
623 | for POST and PUT calls to these special APIs. Codebird takes off the work for you
624 | and will always send the correct Content-Type automatically.
625 |
626 | Find out more about the [Collections API](https://developer.twitter.com/en/docs/tweets/curate-a-collection/overview/about_collections) in the Twitter API docs.
627 | More information on the [Direct Messages API](https://developer.twitter.com/en/docs/direct-messages/api-features) and the [Account Activity API](https://developer.twitter.com/en/docs/accounts-and-users/subscribe-account-activity/overview) is available there as well.
628 |
629 | Here’s a sample for adding a Tweet using the Collections API:
630 |
631 | ```javascript
632 | cb.__call(
633 | "collections_entries_curate",
634 | {
635 | id: "custom-672852634622144512",
636 | changes: [{ op: "add", tweet_id: "672727928262828032" }]
637 | },
638 | function(reply, rate) {
639 | document.body.innerText = JSON.stringify(reply);
640 | }
641 | );
642 | ```
643 |
644 | ### …use promises instead of callback functions?
645 |
646 | Have you ever heard of the [Pyramid of Doom](http://calculist.org/blog/2011/12/14/why-coroutines-wont-work-on-the-web/)?
647 | It’s when code progresses more to the right because of excessive nesting
648 | than it progresses from top to bottom.
649 |
650 | Because of the asynchronous requests, Codebird will use callbacks that you provide.
651 | They are called when the result from the Twitter API has arrived.
652 | However, to streamline code, there is a sleeker concept for this: Promises.
653 |
654 | There are several popular libraries that support promises.
655 | Codebird will auto-detect and use any of the following:
656 |
657 | - jQuery Deferred
658 | - Q
659 | - RSVP
660 | - when
661 |
662 | Here’s a usage sample for promises:
663 |
664 | ```javascript
665 | cb.__call("statuses_update", { status: "Whohoo, I just tweeted!" }).then(
666 | function(data) {
667 | var reply = data.reply,
668 | rate = data.rate;
669 | // ...
670 | },
671 | function(err) {
672 | // ...
673 | }
674 | );
675 | ```
676 |
677 | Since the app-only flag is the fourth parameter for `__call`,
678 | you’ll have to provide a callback stub nonetheless even with promises:
679 |
680 | ```javascript
681 | cb.__call(
682 | "search_tweets",
683 | { q: "#PHP7" },
684 | null, // no callback needed, we have the promise
685 | true // app-only auth
686 | ).then(
687 | function(data) {
688 | var reply = data.reply,
689 | rate = data.rate;
690 | // ...
691 | },
692 | function(err) {
693 | // ...
694 | }
695 | );
696 | ```
697 |
698 | **Tips:**
699 |
700 | - If you provide **both** (callback and promise.then),
701 | Codebird will first call the callback, then resolve the promise.
702 |
703 | - If the request fails due to any errors, Codebird will reject the promise.
704 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "codebird-js",
3 | "version": "3.0.0-dev",
4 | "homepage": "http://www.jublo.net/projects/codebird/js",
5 | "authors": [
6 | "Joshua Atkins ",
7 | "J.M. "
8 | ],
9 | "description": "A Twitter library in JavaScript.",
10 | "main": "codebird.es7.js",
11 | "moduleType": [
12 | "amd",
13 | "globals",
14 | "node"
15 | ],
16 | "keywords": [
17 | "Twitter",
18 | "API",
19 | "networking",
20 | "CORS"
21 | ],
22 | "license": "GPL-3.0+",
23 | "ignore": [
24 | "**/.*",
25 | "node_modules",
26 | "bower_components",
27 | "RELEASE_MESSAGE*",
28 | "test"
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/codebird.es7.js:
--------------------------------------------------------------------------------
1 | /**
2 | * A Twitter library in JavaScript
3 | *
4 | * @package codebird
5 | * @version 3.0.0-dev
6 | * @author Jublo Limited
7 | * @copyright 2010-2018 Jublo Limited
8 | * @license http://opensource.org/licenses/GPL-3.0 GNU Public License 3.0
9 | * @link https://github.com/jublonet/codebird-php
10 | */
11 |
12 | /* global window,
13 | document,
14 | navigator,
15 | Ti,
16 | ActiveXObject,
17 | module,
18 | define,
19 | require */
20 |
21 | (() => {
22 | /**
23 | * A Twitter library in JavaScript
24 | *
25 | * @package codebird
26 | * @subpackage codebird-js
27 | */
28 | class Codebird {
29 | constructor() {
30 | /**
31 | * The OAuth consumer key of your registered app
32 | */
33 | this._oauth_consumer_key = null;
34 |
35 | /**
36 | * The corresponding consumer secret
37 | */
38 | this._oauth_consumer_secret = null;
39 |
40 | /**
41 | * The app-only bearer token. Used to authorize app-only requests
42 | */
43 | this._oauth_bearer_token = null;
44 |
45 | /**
46 | * The API endpoint base to use
47 | */
48 | this._endpoint_base = "https://api.twitter.com/";
49 |
50 | /**
51 | * The media API endpoint base to use
52 | */
53 | this._endpoint_base_media = "https://upload.twitter.com/";
54 |
55 | /**
56 | * The API endpoint to use
57 | */
58 | this._endpoint = `${this._endpoint_base}1.1/`;
59 |
60 | /**
61 | * The media API endpoint to use
62 | */
63 | this._endpoint_media = `${this._endpoint_base_media}1.1/`;
64 |
65 | /**
66 | * The publish API endpoint to use
67 | */
68 | this._endpoint_publish = "https://publish.twitter.com/";
69 |
70 | /**
71 | * The API endpoint base to use
72 | */
73 | this._endpoint_oauth = this._endpoint_base;
74 |
75 | /**
76 | * API proxy endpoint
77 | */
78 | this._endpoint_proxy = "https://api.jublo.net/codebird/";
79 |
80 | /**
81 | * Whether to access the API via a proxy that is allowed by CORS
82 | * Assume that CORS is only necessary in browsers
83 | */
84 | this._use_proxy =
85 | typeof navigator !== "undefined" &&
86 | typeof navigator.userAgent !== "undefined";
87 |
88 | /**
89 | * The Request or access token. Used to sign requests
90 | */
91 | this._oauth_token = null;
92 |
93 | /**
94 | * The corresponding request or access token secret
95 | */
96 | this._oauth_token_secret = null;
97 |
98 | /**
99 | * The current Codebird version
100 | */
101 | this._version = "3.0.0-dev";
102 |
103 | this.b64_alphabet =
104 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
105 | }
106 |
107 | /**
108 | * Sets the OAuth consumer key and secret (App key)
109 | *
110 | * @param string key OAuth consumer key
111 | * @param string secret OAuth consumer secret
112 | *
113 | * @return void
114 | */
115 | setConsumerKey(key, secret) {
116 | this._oauth_consumer_key = key;
117 | this._oauth_consumer_secret = secret;
118 | }
119 |
120 | /**
121 | * Sets the OAuth2 app-only auth bearer token
122 | *
123 | * @param string token OAuth2 bearer token
124 | *
125 | * @return void
126 | */
127 | setBearerToken(token) {
128 | this._oauth_bearer_token = token;
129 | }
130 |
131 | /**
132 | * Gets the current Codebird version
133 | *
134 | * @return string The version number
135 | */
136 | getVersion() {
137 | return this._version;
138 | }
139 |
140 | /**
141 | * Sets the OAuth request or access token and secret (User key)
142 | *
143 | * @param string token OAuth request or access token
144 | * @param string secret OAuth request or access token secret
145 | *
146 | * @return void
147 | */
148 | setToken(token, secret) {
149 | this._oauth_token = token;
150 | this._oauth_token_secret = secret;
151 | }
152 |
153 | /**
154 | * Forgets the OAuth request or access token and secret (User key)
155 | *
156 | * @return bool
157 | */
158 | logout(callback) {
159 | const dfd = this._getDfd();
160 |
161 | if (!dfd && typeof callback === "undefined") {
162 | callback = () => {};
163 | }
164 |
165 | this.__call("oauth_invalidateToken", {
166 | access_key: this._oauth_token,
167 | access_key_secret: this._oauth_token_secret
168 | }).then(() => {
169 | this._oauth_token = this._oauth_token_secret = null;
170 | if (typeof callback === "function") {
171 | callback(true);
172 | }
173 | if (dfd) {
174 | dfd.resolve(true);
175 | }
176 | });
177 |
178 | if (dfd) {
179 | return this._getPromise(dfd);
180 | }
181 | }
182 |
183 | /**
184 | * Enables or disables CORS proxy
185 | *
186 | * @param bool use_proxy Whether to use CORS proxy or not
187 | *
188 | * @return void
189 | */
190 | setUseProxy(use_proxy) {
191 | this._use_proxy = !!use_proxy;
192 | }
193 |
194 | /**
195 | * Sets custom CORS proxy server
196 | *
197 | * @param string proxy Address of proxy server to use
198 | *
199 | * @return void
200 | */
201 | setProxy(proxy) {
202 | // add trailing slash if missing
203 | if (!proxy.match(/\/$/)) {
204 | proxy += "/";
205 | }
206 | this._endpoint_proxy = proxy;
207 | }
208 |
209 | /**
210 | * Signing helpers
211 | */
212 |
213 | /**
214 | * URL-encodes the given data
215 | *
216 | * @param mixed data
217 | *
218 | * @return mixed The encoded data
219 | */
220 | _url(data) {
221 | if (/boolean|number|string/.test(typeof data)) {
222 | return encodeURIComponent(data)
223 | .replace(/!/g, "%21")
224 | .replace(/'/g, "%27")
225 | .replace(/\(/g, "%28")
226 | .replace(/\)/g, "%29")
227 | .replace(/\*/g, "%2A");
228 | } else {
229 | return "";
230 | }
231 | }
232 |
233 | /**
234 | * Gets the base64-encoded SHA1 hash for the given data
235 | *
236 | * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined
237 | * in FIPS PUB 180-1
238 | * Based on version 2.1 Copyright Paul Johnston 2000 - 2002.
239 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
240 | * Distributed under the BSD License
241 | * See http://pajhome.org.uk/crypt/md5 for details.
242 | *
243 | * @param string data The data to calculate the hash from
244 | *
245 | * @return string The hash
246 | */
247 | _sha1(e) {
248 | function n(e, b) {
249 | e[b >> 5] |= 128 << (24 - (b % 32));
250 | e[(((b + 64) >> 9) << 4) + 15] = b;
251 | for (
252 | var c = new Array(80),
253 | a = 1732584193,
254 | d = -271733879,
255 | h = -1732584194,
256 | k = 271733878,
257 | g = -1009589776,
258 | p = 0;
259 | p < e.length;
260 | p += 16
261 | ) {
262 | for (var o = a, q = d, r = h, s = k, t = g, f = 0; 80 > f; f++) {
263 | let m;
264 |
265 | if (f < 16) {
266 | m = e[p + f];
267 | } else {
268 | m = c[f - 3] ^ c[f - 8] ^ c[f - 14] ^ c[f - 16];
269 | m = (m << 1) | (m >>> 31);
270 | }
271 |
272 | c[f] = m;
273 | m = l(
274 | l(
275 | (a << 5) | (a >>> 27),
276 | 20 > f
277 | ? (d & h) | (~d & k)
278 | : 40 > f
279 | ? d ^ h ^ k
280 | : 60 > f
281 | ? (d & h) | (d & k) | (h & k)
282 | : d ^ h ^ k
283 | ),
284 | l(
285 | l(g, c[f]),
286 | 20 > f
287 | ? 1518500249
288 | : 40 > f
289 | ? 1859775393
290 | : 60 > f
291 | ? -1894007588
292 | : -899497514
293 | )
294 | );
295 | g = k;
296 | k = h;
297 | h = (d << 30) | (d >>> 2);
298 | d = a;
299 | a = m;
300 | }
301 | a = l(a, o);
302 | d = l(d, q);
303 | h = l(h, r);
304 | k = l(k, s);
305 | g = l(g, t);
306 | }
307 | return [a, d, h, k, g];
308 | }
309 |
310 | function l(e, b) {
311 | var c = (e & 65535) + (b & 65535);
312 | return (((e >> 16) + (b >> 16) + (c >> 16)) << 16) | (c & 65535);
313 | }
314 |
315 | function q(e) {
316 | for (var b = [], c = (1 << g) - 1, a = 0; a < e.length * g; a += g) {
317 | b[a >> 5] |= (e.charCodeAt(a / g) & c) << (24 - (a % 32));
318 | }
319 | return b;
320 | }
321 | var g = 8;
322 |
323 | let b = `${this._oauth_consumer_secret}&${
324 | null !== this._oauth_token_secret ? this._oauth_token_secret : ""
325 | }`;
326 | if (this._oauth_consumer_secret === null) {
327 | throw "To generate a hash, the consumer secret must be set.";
328 | }
329 | let c = q(b);
330 | if (c.length > 16) {
331 | c = n(c, b.length * g);
332 | }
333 | let bb = new Array(16);
334 | for (var a = new Array(16), d = 0; d < 16; d++) {
335 | a[d] = c[d] ^ 909522486;
336 | bb[d] = c[d] ^ 1549556828;
337 | }
338 | c = n(a.concat(q(e)), 512 + e.length * g);
339 | bb = n(bb.concat(c), 672);
340 | b = "";
341 | for (g = 0; g < 4 * bb.length; g += 3) {
342 | for (
343 | d =
344 | (((bb[g >> 2] >> (8 * (3 - (g % 4)))) & 255) << 16) |
345 | (((bb[(g + 1) >> 2] >> (8 * (3 - ((g + 1) % 4)))) & 255) << 8) |
346 | ((bb[(g + 2) >> 2] >> (8 * (3 - ((g + 2) % 4)))) & 255),
347 | e = 0;
348 | 4 > e;
349 | e++
350 | ) {
351 | b =
352 | 8 * g + 6 * e > 32 * bb.length
353 | ? b + "="
354 | : b + this.b64_alphabet.charAt((d >> (6 * (3 - e))) & 63);
355 | }
356 | }
357 | return b;
358 | }
359 |
360 | /*
361 | * Gets the base64 representation for the given data
362 | *
363 | * http://phpjs.org
364 | * + original by: Tyler Akins (http://rumkin.com)
365 | * + improved by: Bayron Guevara
366 | * + improved by: Thunder.m
367 | * + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
368 | * + bugfixed by: Pellentesque Malesuada
369 | * + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
370 | * + improved by: Rafał Kukawski (http://kukawski.pl)
371 | *
372 | * @param string data The data to calculate the base64 representation from
373 | *
374 | * @return string The base64 representation
375 | */
376 | _base64_encode(a) {
377 | let d,
378 | e,
379 | f,
380 | b,
381 | g = 0,
382 | h = 0,
383 | i = this.b64_alphabet,
384 | c = [];
385 | if (!a) {
386 | return a;
387 | }
388 | do {
389 | d = a.charCodeAt(g++);
390 | e = a.charCodeAt(g++);
391 | f = a.charCodeAt(g++);
392 | b = (d << 16) | (e << 8) | f;
393 | d = (b >> 18) & 63;
394 | e = (b >> 12) & 63;
395 | f = (b >> 6) & 63;
396 | b &= 63;
397 | c[h++] = i.charAt(d) + i.charAt(e) + i.charAt(f) + i.charAt(b);
398 | } while (g < a.length);
399 | i = c.join("");
400 | a = a.length % 3;
401 | return (a ? i.slice(0, a - 3) : i) + "===".slice(a || 3);
402 | }
403 |
404 | /*
405 | * Builds a HTTP query string from the given data
406 | *
407 | * http://phpjs.org
408 | * + original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
409 | * + improved by: Legaev Andrey
410 | * + improved by: Michael White (http://getsprink.com)
411 | * + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
412 | * + improved by: Brett Zamir (http://brett-zamir.me)
413 | * + revised by: stag019
414 | * + input by: Dreamer
415 | * + bugfixed by: Brett Zamir (http://brett-zamir.me)
416 | * + bugfixed by: MIO_KODUKI (http://mio-koduki.blogspot.com/)
417 | *
418 | * @param string data The data to concatenate
419 | *
420 | * @return string The HTTP query
421 | */
422 | _http_build_query(e, f, b) {
423 | function g(c, a, d) {
424 | let b,
425 | e = [];
426 | if (a === true) {
427 | a = "1";
428 | } else if (a === false) {
429 | a = "0";
430 | }
431 | if (null !== a) {
432 | if (typeof a === "object") {
433 | for (b in a) {
434 | if (a.hasOwnProperty(b) && a[b] !== null) {
435 | e.push(g.call(this, c + "[" + b + "]", a[b], d));
436 | }
437 | }
438 | return e.join(d);
439 | }
440 | if (typeof a !== "function") {
441 | return this._url(c) + "=" + this._url(a);
442 | }
443 | throw "There was an error processing for http_build_query().";
444 | } else {
445 | return "";
446 | }
447 | }
448 | var d,
449 | c,
450 | h = [];
451 | if (!b) {
452 | b = "&";
453 | }
454 | for (c in e) {
455 | if (!e.hasOwnProperty(c)) {
456 | continue;
457 | }
458 | d = e[c];
459 | if (f && !isNaN(c)) {
460 | c = String(f) + c;
461 | }
462 | d = g.call(this, c, d, b);
463 | if (d !== "") {
464 | h.push(d);
465 | }
466 | }
467 | return h.join(b);
468 | }
469 |
470 | /**
471 | * Generates a (hopefully) unique random string
472 | *
473 | * @param int optional length The length of the string to generate
474 | *
475 | * @return string The random string
476 | */
477 | _nonce(length = 8) {
478 | if (length < 1) {
479 | throw "Invalid nonce length.";
480 | }
481 | let nonce = "";
482 | for (let i = 0; i < length; i++) {
483 | let character = Math.floor(Math.random() * 61);
484 | nonce += this.b64_alphabet.substring(character, character + 1);
485 | }
486 | return nonce;
487 | }
488 |
489 | /**
490 | * Sort array elements by key
491 | *
492 | * @param array input_arr The array to sort
493 | *
494 | * @return array The sorted keys
495 | */
496 | _ksort(input_arr) {
497 | let keys = [],
498 | sorter,
499 | k;
500 |
501 | sorter = (a, b) => {
502 | let a_float = parseFloat(a),
503 | b_float = parseFloat(b),
504 | a_numeric = a_float + "" === a,
505 | b_numeric = b_float + "" === b;
506 | if (a_numeric && b_numeric) {
507 | return a_float > b_float ? 1 : a_float < b_float ? -1 : 0;
508 | } else if (a_numeric && !b_numeric) {
509 | return 1;
510 | } else if (!a_numeric && b_numeric) {
511 | return -1;
512 | }
513 | return a > b ? 1 : a < b ? -1 : 0;
514 | };
515 |
516 | // Make a list of key names
517 | for (k in input_arr) {
518 | if (input_arr.hasOwnProperty(k)) {
519 | keys.push(k);
520 | }
521 | }
522 | keys.sort(sorter);
523 | return keys;
524 | }
525 |
526 | /**
527 | * Clone objects
528 | *
529 | * @param object obj The object to clone
530 | *
531 | * @return object clone The cloned object
532 | */
533 | _clone(obj) {
534 | let clone = {};
535 | for (let i in obj) {
536 | if (typeof obj[i] === "object") {
537 | clone[i] = this._clone(obj[i]);
538 | } else {
539 | clone[i] = obj[i];
540 | }
541 | }
542 | return clone;
543 | }
544 |
545 | /**
546 | * Gets the XML HTTP Request object, trying to load it in various ways
547 | *
548 | * @return object The XMLHttpRequest object instance
549 | */
550 | _getXmlRequestObject() {
551 | let xml = null;
552 | // first, try the W3-standard object
553 | if (
554 | typeof window === "object" &&
555 | window &&
556 | typeof window.XMLHttpRequest !== "undefined"
557 | ) {
558 | xml = new window.XMLHttpRequest();
559 | // then, try Titanium framework object
560 | } else if (
561 | typeof Ti === "object" &&
562 | Ti &&
563 | typeof Ti.Network.createHTTPClient !== "undefined"
564 | ) {
565 | xml = Ti.Network.createHTTPClient();
566 | // are we in an old Internet Explorer?
567 | } else if (typeof ActiveXObject !== "undefined") {
568 | try {
569 | xml = new ActiveXObject("Microsoft.XMLHTTP");
570 | } catch (e) {
571 | throw "ActiveXObject object not defined.";
572 | }
573 | // now, consider RequireJS and/or Node.js objects
574 | } else if (typeof require === "function") {
575 | var XMLHttpRequest;
576 | // look for xmlhttprequest module
577 | try {
578 | XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;
579 | xml = new XMLHttpRequest();
580 | } catch (e1) {
581 | // or maybe the user is using xhr2
582 | try {
583 | XMLHttpRequest = require("xhr2");
584 | xml = new XMLHttpRequest();
585 | } catch (e2) {
586 | throw "xhr2 object not defined, cancelling.";
587 | }
588 | }
589 | }
590 | return xml;
591 | }
592 |
593 | /**
594 | * Parse URL-style parameters into object
595 | *
596 | * version: 1109.2015
597 | * discuss at: http://phpjs.org/functions/parse_str
598 | * + original by: Cagri Ekin
599 | * + improved by: Michael White (http://getsprink.com)
600 | * + tweaked by: Jack
601 | * + bugfixed by: Onno Marsman
602 | * + reimplemented by: stag019
603 | * + bugfixed by: Brett Zamir (http://brett-zamir.me)
604 | * + bugfixed by: stag019
605 | * - depends on: urldecode
606 | * + input by: Dreamer
607 | * + bugfixed by: Brett Zamir (http://brett-zamir.me)
608 | * % note 1: When no argument is specified, will put variables in global scope.
609 | *
610 | * @param string str String to parse
611 | * @param array array to load data into
612 | *
613 | * @return object
614 | */
615 | _parse_str(str, array) {
616 | var glue1 = "=",
617 | glue2 = "&",
618 | array2 = String(str)
619 | .replace(/^&?([\s\S]*?)&?$/, "$1")
620 | .split(glue2),
621 | i,
622 | j,
623 | chr,
624 | tmp,
625 | key,
626 | value,
627 | bracket,
628 | keys,
629 | evalStr,
630 | fixStr = str => {
631 | return decodeURIComponent(str)
632 | .replace(/([\\"'])/g, "\\$1")
633 | .replace(/\n/g, "\\n")
634 | .replace(/\r/g, "\\r");
635 | };
636 | if (!array) {
637 | array = this.window;
638 | }
639 |
640 | for (i = 0; i < array2.length; i++) {
641 | tmp = array2[i].split(glue1);
642 | if (tmp.length < 2) {
643 | tmp = [tmp, ""];
644 | }
645 | key = fixStr(tmp[0]);
646 | value = fixStr(tmp[1]);
647 | while (key.charAt(0) === " ") {
648 | key = key.substr(1);
649 | }
650 | if (key.indexOf("\0") > -1) {
651 | key = key.substr(0, key.indexOf("\0"));
652 | }
653 | if (key && key.charAt(0) !== "[") {
654 | keys = [];
655 | bracket = 0;
656 | for (j = 0; j < key.length; j++) {
657 | if (key.charAt(j) === "[" && !bracket) {
658 | bracket = j + 1;
659 | } else if (key.charAt(j) === "]") {
660 | if (bracket) {
661 | if (!keys.length) {
662 | keys.push(key.substr(0, bracket - 1));
663 | }
664 | keys.push(key.substr(bracket, j - bracket));
665 | bracket = 0;
666 | if (key.charAt(j + 1) !== "[") {
667 | break;
668 | }
669 | }
670 | }
671 | }
672 | if (!keys.length) {
673 | keys = [key];
674 | }
675 | for (j = 0; j < keys[0].length; j++) {
676 | chr = keys[0].charAt(j);
677 | if (chr === " " || chr === "." || chr === "[") {
678 | keys[0] = keys[0].substr(0, j) + "_" + keys[0].substr(j + 1);
679 | }
680 | if (chr === "[") {
681 | break;
682 | }
683 | }
684 | evalStr = "array";
685 | for (j = 0; j < keys.length; j++) {
686 | key = keys[j];
687 | if ((key !== "" && key !== " ") || j === 0) {
688 | key = `'${key}'`;
689 | } else {
690 | key = eval(evalStr + ".push([]);") - 1;
691 | }
692 | evalStr += `[${key}]`;
693 | if (
694 | j !== keys.length - 1 &&
695 | eval("typeof " + evalStr) === "undefined"
696 | ) {
697 | eval(evalStr + " = [];");
698 | }
699 | }
700 | evalStr += " = '" + value + "';\n";
701 | eval(evalStr);
702 | }
703 | }
704 | }
705 |
706 | /**
707 | * Get allowed API methods, sorted by GET or POST
708 | * Watch out for multiple-method "account/settings"!
709 | *
710 | * @return array $apimethods
711 | */
712 | getApiMethods() {
713 | const httpmethods = {
714 | GET: [
715 | "account/settings",
716 | "account/verify_credentials",
717 | "account_activity/all/:env_name/subscriptions",
718 | "account_activity/all/:env_name/subscriptions/list",
719 | "account_activity/all/:env_name/webhooks",
720 | "account_activity/all/webhooks",
721 | "account_activity/subscriptions/count",
722 | "account_activity/webhooks",
723 | "account_activity/webhooks/:webhook_id/subscriptions/all",
724 | "account_activity/webhooks/:webhook_id/subscriptions/all/list",
725 | "application/rate_limit_status",
726 | "blocks/ids",
727 | "blocks/list",
728 | "collections/entries",
729 | "collections/list",
730 | "collections/show",
731 | "custom_profiles/:id",
732 | "custom_profiles/list",
733 | "direct_messages/events/list",
734 | "direct_messages/events/show",
735 | "direct_messages/welcome_messages/list",
736 | "direct_messages/welcome_messages/rules/list",
737 | "direct_messages/welcome_messages/rules/show",
738 | "direct_messages/welcome_messages/show",
739 | "favorites/list",
740 | "feedback/events",
741 | "feedback/show/:id",
742 | "followers/ids",
743 | "followers/list",
744 | "friends/ids",
745 | "friends/list",
746 | "friendships/incoming",
747 | "friendships/lookup",
748 | "friendships/lookup",
749 | "friendships/no_retweets/ids",
750 | "friendships/outgoing",
751 | "friendships/show",
752 | "geo/id/:place_id",
753 | "geo/reverse_geocode",
754 | "geo/search",
755 | "help/configuration",
756 | "help/languages",
757 | "help/privacy",
758 | "help/tos",
759 | "lists/list",
760 | "lists/members",
761 | "lists/members/show",
762 | "lists/memberships",
763 | "lists/ownerships",
764 | "lists/show",
765 | "lists/statuses",
766 | "lists/subscribers",
767 | "lists/subscribers/show",
768 | "lists/subscriptions",
769 | "mutes/users/ids",
770 | "mutes/users/list",
771 | "oauth/authenticate",
772 | "oauth/authorize",
773 | "saved_searches/list",
774 | "saved_searches/show/:id",
775 | "search/tweets",
776 | "statuses/home_timeline",
777 | "statuses/mentions_timeline",
778 | "statuses/oembed",
779 | "statuses/retweeters/ids",
780 | "statuses/retweets/:id",
781 | "statuses/retweets_of_me",
782 | "statuses/sample",
783 | "statuses/show/:id",
784 | "statuses/user_timeline",
785 | "trends/available",
786 | "trends/closest",
787 | "trends/place",
788 | "users/profile_banner",
789 | "users/search",
790 | "users/show",
791 | "users/suggestions",
792 | "users/suggestions/:slug",
793 | "users/suggestions/:slug/members"
794 | ],
795 | POST: [
796 | "account/remove_profile_banner",
797 | "account/settings__post",
798 | "account/update_profile",
799 | "account/update_profile_banner",
800 | "account/update_profile_image",
801 | "account_activity/all/:env_name/subscriptions",
802 | "account_activity/all/:env_name/webhooks",
803 | "account_activity/webhooks",
804 | "account_activity/webhooks/:webhook_id/subscriptions/all",
805 | "blocks/create",
806 | "blocks/destroy",
807 | "collections/create",
808 | "collections/destroy",
809 | "collections/entries/add",
810 | "collections/entries/curate",
811 | "collections/entries/move",
812 | "collections/entries/remove",
813 | "collections/update",
814 | "custom_profiles/new",
815 | "direct_messages/events/new",
816 | "direct_messages/indicate_typing",
817 | "direct_messages/mark_read",
818 | "direct_messages/welcome_messages/new",
819 | "direct_messages/welcome_messages/rules/new",
820 | "favorites/create",
821 | "favorites/destroy",
822 | "feedback/create",
823 | "friendships/create",
824 | "friendships/destroy",
825 | "friendships/update",
826 | "lists/create",
827 | "lists/destroy",
828 | "lists/members/create",
829 | "lists/members/create_all",
830 | "lists/members/destroy",
831 | "lists/members/destroy_all",
832 | "lists/subscribers/create",
833 | "lists/subscribers/destroy",
834 | "lists/update",
835 | "media/metadata/create",
836 | "media/upload",
837 | "mutes/users/create",
838 | "mutes/users/destroy",
839 | "oauth/access_token",
840 | "oauth/invalidate_token",
841 | "oauth/request_token",
842 | "oauth2/invalidate_token",
843 | "oauth2/token",
844 | "saved_searches/create",
845 | "saved_searches/destroy/:id",
846 | "statuses/destroy/:id",
847 | "statuses/filter",
848 | "statuses/lookup",
849 | "statuses/retweet/:id",
850 | "statuses/unretweet/:id",
851 | "statuses/update",
852 | "users/lookup",
853 | "users/report_spam"
854 | ]
855 | };
856 | return httpmethods;
857 | }
858 |
859 | /**
860 | * Promise helpers
861 | */
862 |
863 | /**
864 | * Get a deferred object
865 | */
866 | _getDfd() {
867 | if (typeof window !== "undefined") {
868 | if (typeof window.jQuery !== "undefined" && window.jQuery.Deferred) {
869 | return window.jQuery.Deferred();
870 | }
871 | if (typeof window.Q !== "undefined" && window.Q.defer) {
872 | return window.Q.defer();
873 | }
874 | if (typeof window.RSVP !== "undefined" && window.RSVP.defer) {
875 | return window.RSVP.defer();
876 | }
877 | if (typeof window.when !== "undefined" && window.when.defer) {
878 | return window.when.defer();
879 | }
880 | }
881 | if (typeof require !== "undefined") {
882 | let promise_class = false;
883 | try {
884 | promise_class = require("jquery");
885 | } catch (e) {}
886 | if (promise_class) {
887 | return promise_class.Deferred();
888 | }
889 | try {
890 | promise_class = require("q");
891 | } catch (e) {}
892 | if (!promise_class) {
893 | try {
894 | promise_class = require("rsvp");
895 | } catch (e) {}
896 | }
897 | if (!promise_class) {
898 | try {
899 | promise_class = require("when");
900 | } catch (e) {}
901 | }
902 | if (promise_class) {
903 | try {
904 | return promise_class.defer();
905 | } catch (e) {}
906 | }
907 | }
908 | return false;
909 | }
910 |
911 | /**
912 | * Get a promise from the dfd object
913 | */
914 | _getPromise(dfd) {
915 | if (typeof dfd.promise === "function") {
916 | return dfd.promise();
917 | }
918 | return dfd.promise; // object
919 | }
920 |
921 | /**
922 | * __call() helpers
923 | */
924 |
925 | /**
926 | * Parse given params, detect query-style params
927 | *
928 | * @param array|string params Parameters to parse
929 | *
930 | * @return array apiparams
931 | */
932 | _parseApiParams(params) {
933 | let apiparams = {};
934 | if (typeof params === "object") {
935 | apiparams = params;
936 | } else {
937 | this._parse_str(params, apiparams); //TODO
938 | }
939 |
940 | return apiparams;
941 | }
942 |
943 | /**
944 | * Replace null and boolean parameters with their string representations
945 | *
946 | * @param array apiparams Parameter array to replace in
947 | *
948 | * @return array apiparams
949 | */
950 | _stringifyNullBoolParams(apiparams) {
951 | for (let key in apiparams) {
952 | if (!apiparams.hasOwnProperty(key)) {
953 | continue;
954 | }
955 | let value = apiparams[key];
956 | if (value === null) {
957 | apiparams[key] = "null";
958 | } else if (value === true || value === false) {
959 | apiparams[key] = value ? "true" : "false";
960 | }
961 | }
962 |
963 | return apiparams;
964 | }
965 |
966 | /**
967 | * API method mapping: Replaces _ with / character
968 | *
969 | * @param string fn Function called
970 | *
971 | * @return string API method to call
972 | */
973 | _mapFnInsertSlashes(fn) {
974 | return fn.split("_").join("/");
975 | }
976 |
977 | /**
978 | * API method mapping: Restore _ character in named parameters
979 | *
980 | * @param string method API method to call
981 | *
982 | * @return string API method with restored underscores
983 | */
984 | _mapFnRestoreParamUnderscores(method) {
985 | const url_parameters_with_underscore = ["screen_name", "place_id"];
986 | let i, param, replacement_was;
987 | for (i = 0; i < url_parameters_with_underscore.length; i++) {
988 | param = url_parameters_with_underscore[i].toUpperCase();
989 | replacement_was = param.split("_").join("/");
990 | method = method.split(replacement_was).join(param);
991 | }
992 |
993 | return method;
994 | }
995 |
996 | /**
997 | * Maps called PHP magic method name to Twitter API method
998 | *
999 | * @param string $fn Function called
1000 | * @param array $apiparams byref API parameters
1001 | *
1002 | * @return string[] (string method, string method_template)
1003 | */
1004 | _mapFnToApiMethod(fn, apiparams) {
1005 | let method = "",
1006 | param,
1007 | i,
1008 | j;
1009 |
1010 | // replace _ by /
1011 | method = this._mapFnInsertSlashes(fn);
1012 |
1013 | // undo replacement for URL parameters
1014 | method = this._mapFnRestoreParamUnderscores(method);
1015 |
1016 | // replace AA by URL parameters
1017 | let method_template = method;
1018 | const match = method.match(/[A-Z_]{2,}/);
1019 | if (match) {
1020 | for (i = 0; i < match.length; i++) {
1021 | param = match[i];
1022 | let param_l = param.toLowerCase();
1023 | method_template = method_template.split(param).join(":" + param_l);
1024 | if (typeof apiparams[param_l] === "undefined") {
1025 | for (j = 0; j < 26; j++) {
1026 | method_template = method_template
1027 | .split(String.fromCharCode(65 + j))
1028 | .join("_" + String.fromCharCode(97 + j));
1029 | }
1030 | throw `To call the templated method "${method_template}", specify the parameter value for "${param_l}".`;
1031 | }
1032 | method = method.split(param).join(apiparams[param_l]);
1033 | delete apiparams[param_l];
1034 | }
1035 | }
1036 |
1037 | // replace A-Z by _a-z
1038 | for (i = 0; i < 26; i++) {
1039 | method = method
1040 | .split(String.fromCharCode(65 + i))
1041 | .join("_" + String.fromCharCode(97 + i));
1042 | method_template = method_template
1043 | .split(String.fromCharCode(65 + i))
1044 | .join("_" + String.fromCharCode(97 + i));
1045 | }
1046 |
1047 | return [method, method_template];
1048 | }
1049 |
1050 | /**
1051 | * Detects HTTP method to use for API call
1052 | *
1053 | * @param string method The API method to call
1054 | * @param array params The parameters to send along
1055 | *
1056 | * @return string The HTTP method that should be used
1057 | */
1058 | _detectMethod(method, params) {
1059 | if (typeof params.httpmethod !== "undefined") {
1060 | let httpmethod = params.httpmethod;
1061 | delete params.httpmethod;
1062 | return httpmethod;
1063 | }
1064 |
1065 | // multi-HTTP method endpoints
1066 | switch (method) {
1067 | case "account/settings":
1068 | case "account/login_verification_enrollment":
1069 | case "account/login_verification_request":
1070 | method = Object.keys(params).length ? `${method}__post` : method;
1071 | break;
1072 | }
1073 |
1074 | const apimethods = this.getApiMethods();
1075 | for (let httpmethod in apimethods) {
1076 | if (
1077 | apimethods.hasOwnProperty(httpmethod) &&
1078 | apimethods[httpmethod].indexOf(method) > -1
1079 | ) {
1080 | return httpmethod;
1081 | }
1082 | }
1083 | throw `Can't find HTTP method to use for "${method}".`;
1084 | }
1085 |
1086 | /**
1087 | * Detects if API call should use multipart/form-data
1088 | *
1089 | * @param string method The API method to call
1090 | *
1091 | * @return bool Whether the method should be sent as multipart
1092 | */
1093 | _detectMultipart(method) {
1094 | const multiparts = [
1095 | // Tweets
1096 | "media/upload",
1097 |
1098 | // Users
1099 | "account/update_profile_image",
1100 | "account/update_profile_banner"
1101 | ];
1102 | return multiparts.indexOf(method) > -1;
1103 | }
1104 |
1105 | /**
1106 | * Signature helper
1107 | *
1108 | * @param string httpmethod Usually either 'GET' or 'POST' or 'DELETE'
1109 | * @param string method The API method to call
1110 | * @param array base_params The signature base parameters
1111 | *
1112 | * @return string signature
1113 | */
1114 | _getSignature(httpmethod, method, keys, base_params) {
1115 | // convert params to string
1116 | let base_string = "",
1117 | key,
1118 | value;
1119 | for (let i = 0; i < keys.length; i++) {
1120 | key = keys[i];
1121 | value = base_params[key];
1122 | base_string += `${key}=${this._url(value)}&`;
1123 | }
1124 | base_string = base_string.substring(0, base_string.length - 1);
1125 | return this._sha1(
1126 | `${httpmethod}&${this._url(method)}&${this._url(base_string)}`
1127 | );
1128 | }
1129 |
1130 | /**
1131 | * Generates the UNIX timestamp
1132 | */
1133 | _time() {
1134 | return Math.round(new Date().getTime() / 1000);
1135 | }
1136 |
1137 | /**
1138 | * Generates an OAuth signature
1139 | *
1140 | * @param string httpmethod Usually either 'GET' or 'POST' or 'DELETE'
1141 | * @param string method The API method to call
1142 | * @param array optional params The API call parameters, associative
1143 | *
1144 | * @return string Authorization HTTP header
1145 | */
1146 | _sign(httpmethod, method, params = {}) {
1147 | if (this._oauth_consumer_key === null) {
1148 | throw "To generate a signature, the consumer key must be set.";
1149 | }
1150 | const sign_params = {
1151 | consumer_key: this._oauth_consumer_key,
1152 | version: "1.0",
1153 | timestamp: this._time(),
1154 | nonce: this._nonce(),
1155 | signature_method: "HMAC-SHA1"
1156 | };
1157 | let sign_base_params = {};
1158 | for (var key in sign_params) {
1159 | if (!sign_params.hasOwnProperty(key)) {
1160 | continue;
1161 | }
1162 | let value = sign_params[key];
1163 | sign_base_params[`oauth_${key}`] = this._url(value);
1164 | }
1165 | if (this._oauth_token !== null) {
1166 | sign_base_params.oauth_token = this._url(this._oauth_token);
1167 | }
1168 | const oauth_params = this._clone(sign_base_params);
1169 | for (key in params) {
1170 | if (!params.hasOwnProperty(key)) {
1171 | continue;
1172 | }
1173 | sign_base_params[key] = params[key];
1174 | }
1175 | let keys = this._ksort(sign_base_params);
1176 |
1177 | const signature = this._getSignature(
1178 | httpmethod,
1179 | method,
1180 | keys,
1181 | sign_base_params
1182 | );
1183 |
1184 | params = oauth_params;
1185 | params.oauth_signature = signature;
1186 | keys = this._ksort(params);
1187 | let authorization = "OAuth ";
1188 | for (let i = 0; i < keys.length; i++) {
1189 | key = keys[i];
1190 | authorization += `${key}="${this._url(params[key])}", `;
1191 | }
1192 | return authorization.substring(0, authorization.length - 2);
1193 | }
1194 |
1195 | /**
1196 | * Build multipart request from upload params
1197 | *
1198 | * @param string method The API method to call
1199 | * @param array params The parameters to send along
1200 | *
1201 | * @return null|string The built multipart request body
1202 | */
1203 | _buildMultipart(method, params) {
1204 | // well, files will only work in multipart methods
1205 | if (!this._detectMultipart(method)) {
1206 | return;
1207 | }
1208 |
1209 | // only check specific parameters
1210 | const possible_methods = [
1211 | // Tweets
1212 | "media/upload",
1213 | // Accounts
1214 | "account/update_profile_image",
1215 | "account/update_profile_banner"
1216 | ];
1217 | let possible_files = {
1218 | // Tweets
1219 | "media/upload": "media",
1220 | // Accounts
1221 | "account/update_profile_image": "image",
1222 | "account/update_profile_banner": "banner"
1223 | };
1224 | // method might have files?
1225 | if (possible_methods.indexOf(method) === -1) {
1226 | return;
1227 | }
1228 |
1229 | // check for filenames
1230 | possible_files = possible_files[method].split(" ");
1231 |
1232 | const multipart_border = `--------------------${this._nonce()}`;
1233 | let multipart_request = "";
1234 | for (let key in params) {
1235 | if (!params.hasOwnProperty(key)) {
1236 | continue;
1237 | }
1238 | multipart_request += `--${multipart_border}\r\nContent-Disposition: form-data; name="${key}"`;
1239 | if (possible_files.indexOf(key) === -1) {
1240 | multipart_request += "\r\nContent-Transfer-Encoding: base64";
1241 | }
1242 | multipart_request += `\r\n\r\n${params[key]}\r\n`;
1243 | }
1244 | multipart_request += `--${multipart_border}--`;
1245 | return multipart_request;
1246 | }
1247 |
1248 | /**
1249 | * Detects if API call should use media endpoint
1250 | *
1251 | * @param string method The API method to call
1252 | *
1253 | * @return bool Whether the method is defined in media API
1254 | */
1255 | _detectMedia(method) {
1256 | const medias = ["media/metadata/create", "media/upload"];
1257 | return medias.indexOf(method) > -1;
1258 | }
1259 |
1260 | /**
1261 | * Detects if API call should use JSON body
1262 | *
1263 | * @param string method The API method to call
1264 | *
1265 | * @return bool Whether the method is defined as accepting JSON body
1266 | */
1267 | _detectJsonBody(method) {
1268 | const json_bodies = [
1269 | "collections/entries/curate",
1270 | "custom_profiles/new",
1271 | "direct_messages/events/new",
1272 | "direct_messages/indicate_typing",
1273 | "direct_messages/mark_read",
1274 | "direct_messages/welcome_messages/new",
1275 | "direct_messages/welcome_messages/rules/new",
1276 | "direct_messages/welcome_messages/update",
1277 | "media/metadata/create"
1278 | ];
1279 | return json_bodies.indexOf(method) > -1;
1280 | }
1281 |
1282 | /**
1283 | * Builds the complete API endpoint url
1284 | *
1285 | * @param string method The API method to call
1286 | *
1287 | * @return string The URL to send the request to
1288 | */
1289 | _getEndpoint(method) {
1290 | let url;
1291 | if (method.substring(0, 22) === "oauth/invalidate_token") {
1292 | url = this._endpoint + method + ".json";
1293 | } else if (method.substring(0, 5) === "oauth") {
1294 | url = this._endpoint_oauth + method;
1295 | } else if (this._detectMedia(method)) {
1296 | url = this._endpoint_media + method + ".json";
1297 | } else if (method === "statuses/oembed") {
1298 | url = this._endpoint_publish + "oembed";
1299 | } else {
1300 | url = this._endpoint + method + ".json";
1301 | }
1302 | return url;
1303 | }
1304 |
1305 | /**
1306 | * Parses the API reply to encode it in the set return_format
1307 | *
1308 | * @param string reply The actual reply, JSON-encoded or URL-encoded
1309 | *
1310 | * @return array|object The parsed reply
1311 | */
1312 | _parseApiReply(reply) {
1313 | if (typeof reply !== "string" || reply === "") {
1314 | return {};
1315 | }
1316 | if (reply === "[]") {
1317 | return [];
1318 | }
1319 | let parsed;
1320 | try {
1321 | parsed = JSON.parse(reply);
1322 | } catch (e) {
1323 | parsed = {};
1324 | // assume XML
1325 | if (reply.match(/^<\?xml/)) {
1326 | let errors;
1327 | if ((errors = reply.match(/(.+)<\/errors>/))) {
1328 | parsed.errors = {};
1329 | errors = errors[1].match(/(.+)/);
1332 | parsed.errors[error[1]] = error[2];
1333 | }
1334 | }
1335 | return parsed;
1336 | }
1337 | // assume query format
1338 | let elements = reply.split("&");
1339 | for (let i = 0; i < elements.length; i++) {
1340 | let element = elements[i].split("=", 2);
1341 | if (element.length > 1) {
1342 | parsed[element[0]] = decodeURIComponent(element[1]);
1343 | } else {
1344 | parsed[element[0]] = null;
1345 | }
1346 | }
1347 | }
1348 | return parsed;
1349 | }
1350 |
1351 | /**
1352 | * Uncommon API methods
1353 | */
1354 |
1355 | /**
1356 | * Gets the OAuth authenticate URL for the current request token
1357 | *
1358 | * @return object Promise
1359 | */
1360 | oauth_authenticate(
1361 | params = {},
1362 | callback = undefined,
1363 | type = "authenticate"
1364 | ) {
1365 | const dfd = this._getDfd();
1366 | if (typeof params.force_login === "undefined") {
1367 | params.force_login = null;
1368 | }
1369 | if (typeof params.screen_name === "undefined") {
1370 | params.screen_name = null;
1371 | }
1372 | if (["authenticate", "authorize"].indexOf(type) === -1) {
1373 | type = "authenticate";
1374 | }
1375 | if (this._oauth_token === null) {
1376 | const error = `To get the ${type} URL, the OAuth token must be set.`;
1377 | if (dfd) {
1378 | dfd.reject({ error });
1379 | return this._getPromise(dfd);
1380 | }
1381 | throw error;
1382 | }
1383 | let url = `${this._endpoint_oauth}oauth/${type}?oauth_token=${this._url(
1384 | this._oauth_token
1385 | )}`;
1386 | if (params.force_login === true) {
1387 | url += "&force_login=1";
1388 | }
1389 | if (params.screen_name !== null) {
1390 | url += `&screen_name=${params.screen_name}`;
1391 | }
1392 | if (typeof callback === "function") {
1393 | callback(url);
1394 | }
1395 | if (dfd) {
1396 | dfd.resolve({ reply: url });
1397 | return this._getPromise(dfd);
1398 | }
1399 | // no promises
1400 | return true;
1401 | }
1402 |
1403 | /**
1404 | * Gets the OAuth authorize URL for the current request token
1405 | *
1406 | * @return string The OAuth authorize URL
1407 | */
1408 | oauth_authorize(params, callback) {
1409 | return this.oauth_authenticate(params, callback, "authorize");
1410 | }
1411 |
1412 | /**
1413 | * Gets the OAuth bearer token
1414 | *
1415 | * @return object Promise
1416 | */
1417 | oauth2_token(callback) {
1418 | const dfd = this._getDfd();
1419 |
1420 | if (this._oauth_consumer_key === null) {
1421 | const error = "To obtain a bearer token, the consumer key must be set.";
1422 | if (dfd) {
1423 | dfd.reject({ error });
1424 | return this._getPromise(dfd);
1425 | }
1426 | throw error;
1427 | }
1428 |
1429 | if (!dfd && typeof callback === "undefined") {
1430 | callback = () => {};
1431 | }
1432 |
1433 | const post_fields = "grant_type=client_credentials";
1434 | let url = this._endpoint_oauth + "oauth2/token";
1435 |
1436 | if (this._use_proxy) {
1437 | url = url.replace(this._endpoint_base, this._endpoint_proxy);
1438 | }
1439 |
1440 | const xml = this._getXmlRequestObject();
1441 | if (xml === null) {
1442 | return;
1443 | }
1444 | xml.open("POST", url, true);
1445 | xml.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
1446 | xml.setRequestHeader(
1447 | `${this._use_proxy ? "X-" : ""}Authorization`,
1448 | "Basic " +
1449 | this._base64_encode(
1450 | `${this._oauth_consumer_key}:${this._oauth_consumer_secret}`
1451 | )
1452 | );
1453 |
1454 | xml.onreadystatechange = () => {
1455 | if (xml.readyState >= 4) {
1456 | let httpstatus = 12027;
1457 | try {
1458 | httpstatus = xml.status;
1459 | } catch (e) {}
1460 | let response = "";
1461 | try {
1462 | response = xml.responseText;
1463 | } catch (e) {}
1464 | let reply = this._parseApiReply(response);
1465 | reply.httpstatus = httpstatus;
1466 | if (httpstatus === 200) {
1467 | this.setBearerToken(reply.access_token);
1468 | }
1469 | if (typeof callback === "function") {
1470 | callback(reply);
1471 | }
1472 | if (dfd) {
1473 | dfd.resolve({ reply });
1474 | }
1475 | }
1476 | };
1477 | // function called when an error occurs, including a timeout
1478 | xml.onerror = e => {
1479 | if (typeof callback === "function") {
1480 | callback(null, e);
1481 | }
1482 | if (dfd) {
1483 | dfd.reject(e);
1484 | }
1485 | };
1486 | xml.timeout = 30000; // in milliseconds
1487 |
1488 | xml.send(post_fields);
1489 | if (dfd) {
1490 | return this._getPromise(dfd);
1491 | }
1492 | }
1493 |
1494 | /**
1495 | * Calls the API using cURL
1496 | *
1497 | * @param string httpmethod The HTTP method to use for making the request
1498 | * @param string method The API method to call
1499 | * @param array optional params The parameters to send along
1500 | * @param bool optional multipart Whether to use multipart/form-data
1501 | * @param bool optional app_only_auth Whether to use app-only bearer authentication
1502 | * @param function callback The function to call with the API call result
1503 | *
1504 | * @return mixed The API reply, encoded in the set return_format
1505 | */
1506 | _callApi(
1507 | httpmethod,
1508 | method,
1509 | params = {},
1510 | multipart = false,
1511 | app_only_auth = false,
1512 | callback = () => {}
1513 | ) {
1514 | const dfd = this._getDfd();
1515 |
1516 | let url = this._getEndpoint(method),
1517 | authorization = null;
1518 |
1519 | const xml = this._getXmlRequestObject();
1520 | if (xml === null) {
1521 | return;
1522 | }
1523 | let post_fields;
1524 |
1525 | if (httpmethod === "GET") {
1526 | let url_with_params = url;
1527 | if (JSON.stringify(params) !== "{}") {
1528 | url_with_params += "?" + this._http_build_query(params);
1529 | }
1530 | if (!app_only_auth) {
1531 | authorization = this._sign(httpmethod, url, params);
1532 | }
1533 |
1534 | if (this._use_proxy) {
1535 | url_with_params = url_with_params
1536 | .replace(this._endpoint_base, this._endpoint_proxy)
1537 | .replace(this._endpoint_base_media, this._endpoint_proxy);
1538 | }
1539 | xml.open(httpmethod, url_with_params, true);
1540 | } else {
1541 | if (multipart) {
1542 | if (!app_only_auth) {
1543 | authorization = this._sign(httpmethod, url, {});
1544 | }
1545 | params = this._buildMultipart(method, params);
1546 | } else if (this._detectJsonBody(method)) {
1547 | authorization = this._sign(httpmethod, url, {});
1548 | params = JSON.stringify(params);
1549 | } else {
1550 | if (!app_only_auth) {
1551 | authorization = this._sign(httpmethod, url, params);
1552 | }
1553 | params = this._http_build_query(params);
1554 | }
1555 | post_fields = params;
1556 | if (this._use_proxy || multipart) {
1557 | // force proxy for multipart base64
1558 | url = url
1559 | .replace(this._endpoint_base, this._endpoint_proxy)
1560 | .replace(this._endpoint_base_media, this._endpoint_proxy);
1561 | }
1562 | xml.open(httpmethod, url, true);
1563 | if (multipart) {
1564 | xml.setRequestHeader(
1565 | "Content-Type",
1566 | "multipart/form-data; boundary=" +
1567 | post_fields.split("\r\n")[0].substring(2)
1568 | );
1569 | } else if (this._detectJsonBody(method)) {
1570 | xml.setRequestHeader("Content-Type", "application/json");
1571 | } else {
1572 | xml.setRequestHeader(
1573 | "Content-Type",
1574 | "application/x-www-form-urlencoded"
1575 | );
1576 | }
1577 | }
1578 | if (app_only_auth) {
1579 | if (
1580 | this._oauth_consumer_key === null &&
1581 | this._oauth_bearer_token === null
1582 | ) {
1583 | const error =
1584 | "To make an app-only auth API request, consumer key or bearer token must be set.";
1585 | if (dfd) {
1586 | dfd.reject({ error });
1587 | return this._getPromise(dfd);
1588 | }
1589 | throw error;
1590 | }
1591 | // automatically fetch bearer token, if necessary
1592 | if (this._oauth_bearer_token === null) {
1593 | if (dfd) {
1594 | return this.oauth2_token().then(() => {
1595 | return this._callApi(
1596 | httpmethod,
1597 | method,
1598 | params,
1599 | multipart,
1600 | app_only_auth,
1601 | callback
1602 | );
1603 | });
1604 | }
1605 | this.oauth2_token(() => {
1606 | this._callApi(
1607 | httpmethod,
1608 | method,
1609 | params,
1610 | multipart,
1611 | app_only_auth,
1612 | callback
1613 | );
1614 | });
1615 | return;
1616 | }
1617 | authorization = "Bearer " + this._oauth_bearer_token;
1618 | }
1619 | if (authorization !== null) {
1620 | xml.setRequestHeader(
1621 | `${this._use_proxy ? "X-" : ""}Authorization`,
1622 | authorization
1623 | );
1624 | }
1625 | xml.onreadystatechange = () => {
1626 | if (xml.readyState >= 4) {
1627 | let httpstatus = 12027;
1628 | try {
1629 | httpstatus = xml.status;
1630 | } catch (e) {}
1631 | let response = "";
1632 | try {
1633 | response = xml.responseText;
1634 | } catch (e) {}
1635 | let reply = this._parseApiReply(response);
1636 | reply.httpstatus = httpstatus;
1637 | let rate = null;
1638 | if (
1639 | typeof xml.getResponseHeader !== "undefined" &&
1640 | xml.getResponseHeader("x-rate-limit-limit") !== ""
1641 | ) {
1642 | rate = {
1643 | limit: xml.getResponseHeader("x-rate-limit-limit"),
1644 | remaining: xml.getResponseHeader("x-rate-limit-remaining"),
1645 | reset: xml.getResponseHeader("x-rate-limit-reset")
1646 | };
1647 | }
1648 | if (typeof callback === "function") {
1649 | callback(reply, rate);
1650 | }
1651 | if (dfd) {
1652 | dfd.resolve({ reply, rate });
1653 | }
1654 | }
1655 | };
1656 | // function called when an error occurs, including a timeout
1657 | xml.onerror = e => {
1658 | if (typeof callback === "function") {
1659 | callback(null, null, e);
1660 | }
1661 | if (dfd) {
1662 | dfd.reject(e);
1663 | }
1664 | };
1665 | xml.timeout = 30000; // in milliseconds
1666 |
1667 | xml.send(httpmethod === "GET" ? null : post_fields);
1668 | if (dfd) {
1669 | return this._getPromise(dfd);
1670 | }
1671 | return true;
1672 | }
1673 |
1674 | /**
1675 | * Main API handler working on any requests you issue
1676 | *
1677 | * @param string fn The member function you called
1678 | * @param array params The parameters you sent along
1679 | * @param function callback The callback to call with the reply
1680 | * @param bool app_only_auth Whether to use app-only auth
1681 | *
1682 | * @return object Promise
1683 | */
1684 | __call(fn, params = {}, callback, app_only_auth = false) {
1685 | if (typeof callback !== "function" && typeof params === "function") {
1686 | callback = params;
1687 | params = {};
1688 | if (typeof callback === "boolean") {
1689 | app_only_auth = callback;
1690 | }
1691 | } else if (typeof callback === "undefined") {
1692 | callback = () => {};
1693 | }
1694 | switch (fn) {
1695 | case "oauth_authenticate":
1696 | case "oauth_authorize":
1697 | return this[fn](params, callback);
1698 |
1699 | case "oauth2_token":
1700 | return this[fn](callback);
1701 | }
1702 |
1703 | // parse parameters
1704 | let apiparams = this._parseApiParams(params);
1705 |
1706 | // stringify null and boolean parameters
1707 | apiparams = this._stringifyNullBoolParams(apiparams);
1708 |
1709 | // reset token when requesting a new token (causes 401 for signature error on 2nd+ requests)
1710 | if (fn === "oauth_requestToken") {
1711 | this.setToken(null, null);
1712 | }
1713 |
1714 | // map function name to API method
1715 | const [method, method_template] = this._mapFnToApiMethod(fn, apiparams),
1716 | httpmethod = this._detectMethod(method_template, apiparams),
1717 | multipart = this._detectMultipart(method_template);
1718 |
1719 | return this._callApi(
1720 | httpmethod,
1721 | method,
1722 | apiparams,
1723 | multipart,
1724 | app_only_auth,
1725 | callback
1726 | );
1727 | }
1728 | }
1729 |
1730 | if (
1731 | typeof module === "object" &&
1732 | module &&
1733 | typeof module.exports === "object"
1734 | ) {
1735 | // Expose codebird as module.exports in loaders that implement the Node
1736 | // module pattern (including browserify). Do not create the global, since
1737 | // the user will be storing it themselves locally, and globals are frowned
1738 | // upon in the Node module world.
1739 | module.exports = Codebird;
1740 | } else {
1741 | // Otherwise expose codebird to the global object as usual
1742 | if (typeof window === "object" && window) {
1743 | window.Codebird = Codebird;
1744 | }
1745 |
1746 | // Register as a named AMD module, since codebird can be concatenated with other
1747 | // files that may use define, but not via a proper concatenation script that
1748 | // understands anonymous AMD modules. A named AMD is safest and most robust
1749 | // way to register. Lowercase codebird is used because AMD module names are
1750 | // derived from file names, and codebird is normally delivered in a lowercase
1751 | // file name. Do this after creating the global so that if an AMD module wants
1752 | // to call noConflict to hide this version of codebird, it will work.
1753 | if (typeof define === "function" && define.amd) {
1754 | define("codebird", [], () => Codebird);
1755 | }
1756 | }
1757 | })();
1758 |
--------------------------------------------------------------------------------
/codebird.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
4 |
5 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
6 |
7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
8 |
9 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
10 |
11 | /**
12 | * A Twitter library in JavaScript
13 | *
14 | * @package codebird
15 | * @version 3.0.0-dev
16 | * @author Jublo Limited
17 | * @copyright 2010-2018 Jublo Limited
18 | * @license http://opensource.org/licenses/GPL-3.0 GNU Public License 3.0
19 | * @link https://github.com/jublonet/codebird-php
20 | */
21 |
22 | /* global window,
23 | document,
24 | navigator,
25 | Ti,
26 | ActiveXObject,
27 | module,
28 | define,
29 | require */
30 |
31 | (function () {
32 | /**
33 | * A Twitter library in JavaScript
34 | *
35 | * @package codebird
36 | * @subpackage codebird-js
37 | */
38 | var Codebird = function () {
39 | function Codebird() {
40 | _classCallCheck(this, Codebird);
41 |
42 | /**
43 | * The OAuth consumer key of your registered app
44 | */
45 | this._oauth_consumer_key = null;
46 |
47 | /**
48 | * The corresponding consumer secret
49 | */
50 | this._oauth_consumer_secret = null;
51 |
52 | /**
53 | * The app-only bearer token. Used to authorize app-only requests
54 | */
55 | this._oauth_bearer_token = null;
56 |
57 | /**
58 | * The API endpoint base to use
59 | */
60 | this._endpoint_base = "https://api.twitter.com/";
61 |
62 | /**
63 | * The media API endpoint base to use
64 | */
65 | this._endpoint_base_media = "https://upload.twitter.com/";
66 |
67 | /**
68 | * The API endpoint to use
69 | */
70 | this._endpoint = this._endpoint_base + "1.1/";
71 |
72 | /**
73 | * The media API endpoint to use
74 | */
75 | this._endpoint_media = this._endpoint_base_media + "1.1/";
76 |
77 | /**
78 | * The publish API endpoint to use
79 | */
80 | this._endpoint_publish = "https://publish.twitter.com/";
81 |
82 | /**
83 | * The API endpoint base to use
84 | */
85 | this._endpoint_oauth = this._endpoint_base;
86 |
87 | /**
88 | * API proxy endpoint
89 | */
90 | this._endpoint_proxy = "https://api.jublo.net/codebird/";
91 |
92 | /**
93 | * Whether to access the API via a proxy that is allowed by CORS
94 | * Assume that CORS is only necessary in browsers
95 | */
96 | this._use_proxy = typeof navigator !== "undefined" && typeof navigator.userAgent !== "undefined";
97 |
98 | /**
99 | * The Request or access token. Used to sign requests
100 | */
101 | this._oauth_token = null;
102 |
103 | /**
104 | * The corresponding request or access token secret
105 | */
106 | this._oauth_token_secret = null;
107 |
108 | /**
109 | * The current Codebird version
110 | */
111 | this._version = "3.0.0-dev";
112 |
113 | this.b64_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
114 | }
115 |
116 | /**
117 | * Sets the OAuth consumer key and secret (App key)
118 | *
119 | * @param string key OAuth consumer key
120 | * @param string secret OAuth consumer secret
121 | *
122 | * @return void
123 | */
124 |
125 |
126 | _createClass(Codebird, [{
127 | key: "setConsumerKey",
128 | value: function setConsumerKey(key, secret) {
129 | this._oauth_consumer_key = key;
130 | this._oauth_consumer_secret = secret;
131 | }
132 |
133 | /**
134 | * Sets the OAuth2 app-only auth bearer token
135 | *
136 | * @param string token OAuth2 bearer token
137 | *
138 | * @return void
139 | */
140 |
141 | }, {
142 | key: "setBearerToken",
143 | value: function setBearerToken(token) {
144 | this._oauth_bearer_token = token;
145 | }
146 |
147 | /**
148 | * Gets the current Codebird version
149 | *
150 | * @return string The version number
151 | */
152 |
153 | }, {
154 | key: "getVersion",
155 | value: function getVersion() {
156 | return this._version;
157 | }
158 |
159 | /**
160 | * Sets the OAuth request or access token and secret (User key)
161 | *
162 | * @param string token OAuth request or access token
163 | * @param string secret OAuth request or access token secret
164 | *
165 | * @return void
166 | */
167 |
168 | }, {
169 | key: "setToken",
170 | value: function setToken(token, secret) {
171 | this._oauth_token = token;
172 | this._oauth_token_secret = secret;
173 | }
174 |
175 | /**
176 | * Forgets the OAuth request or access token and secret (User key)
177 | *
178 | * @return bool
179 | */
180 |
181 | }, {
182 | key: "logout",
183 | value: function logout(callback) {
184 | var _this = this;
185 |
186 | var dfd = this._getDfd();
187 |
188 | if (!dfd && typeof callback === "undefined") {
189 | callback = function callback() {};
190 | }
191 |
192 | this.__call("oauth_invalidateToken", {
193 | access_key: this._oauth_token,
194 | access_key_secret: this._oauth_token_secret
195 | }).then(function () {
196 | _this._oauth_token = _this._oauth_token_secret = null;
197 | if (typeof callback === "function") {
198 | callback(true);
199 | }
200 | if (dfd) {
201 | dfd.resolve(true);
202 | }
203 | });
204 |
205 | if (dfd) {
206 | return this._getPromise(dfd);
207 | }
208 | }
209 |
210 | /**
211 | * Enables or disables CORS proxy
212 | *
213 | * @param bool use_proxy Whether to use CORS proxy or not
214 | *
215 | * @return void
216 | */
217 |
218 | }, {
219 | key: "setUseProxy",
220 | value: function setUseProxy(use_proxy) {
221 | this._use_proxy = !!use_proxy;
222 | }
223 |
224 | /**
225 | * Sets custom CORS proxy server
226 | *
227 | * @param string proxy Address of proxy server to use
228 | *
229 | * @return void
230 | */
231 |
232 | }, {
233 | key: "setProxy",
234 | value: function setProxy(proxy) {
235 | // add trailing slash if missing
236 | if (!proxy.match(/\/$/)) {
237 | proxy += "/";
238 | }
239 | this._endpoint_proxy = proxy;
240 | }
241 |
242 | /**
243 | * Signing helpers
244 | */
245 |
246 | /**
247 | * URL-encodes the given data
248 | *
249 | * @param mixed data
250 | *
251 | * @return mixed The encoded data
252 | */
253 |
254 | }, {
255 | key: "_url",
256 | value: function _url(data) {
257 | if (/boolean|number|string/.test(typeof data === "undefined" ? "undefined" : _typeof(data))) {
258 | return encodeURIComponent(data).replace(/!/g, "%21").replace(/'/g, "%27").replace(/\(/g, "%28").replace(/\)/g, "%29").replace(/\*/g, "%2A");
259 | } else {
260 | return "";
261 | }
262 | }
263 |
264 | /**
265 | * Gets the base64-encoded SHA1 hash for the given data
266 | *
267 | * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined
268 | * in FIPS PUB 180-1
269 | * Based on version 2.1 Copyright Paul Johnston 2000 - 2002.
270 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
271 | * Distributed under the BSD License
272 | * See http://pajhome.org.uk/crypt/md5 for details.
273 | *
274 | * @param string data The data to calculate the hash from
275 | *
276 | * @return string The hash
277 | */
278 |
279 | }, {
280 | key: "_sha1",
281 | value: function _sha1(e) {
282 | function n(e, b) {
283 | e[b >> 5] |= 128 << 24 - b % 32;
284 | e[(b + 64 >> 9 << 4) + 15] = b;
285 | for (var c = new Array(80), a = 1732584193, d = -271733879, h = -1732584194, k = 271733878, g = -1009589776, p = 0; p < e.length; p += 16) {
286 | for (var o = a, q = d, r = h, s = k, t = g, f = 0; 80 > f; f++) {
287 | var m = void 0;
288 |
289 | if (f < 16) {
290 | m = e[p + f];
291 | } else {
292 | m = c[f - 3] ^ c[f - 8] ^ c[f - 14] ^ c[f - 16];
293 | m = m << 1 | m >>> 31;
294 | }
295 |
296 | c[f] = m;
297 | m = l(l(a << 5 | a >>> 27, 20 > f ? d & h | ~d & k : 40 > f ? d ^ h ^ k : 60 > f ? d & h | d & k | h & k : d ^ h ^ k), l(l(g, c[f]), 20 > f ? 1518500249 : 40 > f ? 1859775393 : 60 > f ? -1894007588 : -899497514));
298 | g = k;
299 | k = h;
300 | h = d << 30 | d >>> 2;
301 | d = a;
302 | a = m;
303 | }
304 | a = l(a, o);
305 | d = l(d, q);
306 | h = l(h, r);
307 | k = l(k, s);
308 | g = l(g, t);
309 | }
310 | return [a, d, h, k, g];
311 | }
312 |
313 | function l(e, b) {
314 | var c = (e & 65535) + (b & 65535);
315 | return (e >> 16) + (b >> 16) + (c >> 16) << 16 | c & 65535;
316 | }
317 |
318 | function q(e) {
319 | for (var b = [], c = (1 << g) - 1, a = 0; a < e.length * g; a += g) {
320 | b[a >> 5] |= (e.charCodeAt(a / g) & c) << 24 - a % 32;
321 | }
322 | return b;
323 | }
324 | var g = 8;
325 |
326 | var b = this._oauth_consumer_secret + "&" + (null !== this._oauth_token_secret ? this._oauth_token_secret : "");
327 | if (this._oauth_consumer_secret === null) {
328 | throw "To generate a hash, the consumer secret must be set.";
329 | }
330 | var c = q(b);
331 | if (c.length > 16) {
332 | c = n(c, b.length * g);
333 | }
334 | var bb = new Array(16);
335 | for (var a = new Array(16), d = 0; d < 16; d++) {
336 | a[d] = c[d] ^ 909522486;
337 | bb[d] = c[d] ^ 1549556828;
338 | }
339 | c = n(a.concat(q(e)), 512 + e.length * g);
340 | bb = n(bb.concat(c), 672);
341 | b = "";
342 | for (g = 0; g < 4 * bb.length; g += 3) {
343 | for (d = (bb[g >> 2] >> 8 * (3 - g % 4) & 255) << 16 | (bb[g + 1 >> 2] >> 8 * (3 - (g + 1) % 4) & 255) << 8 | bb[g + 2 >> 2] >> 8 * (3 - (g + 2) % 4) & 255, e = 0; 4 > e; e++) {
344 | b = 8 * g + 6 * e > 32 * bb.length ? b + "=" : b + this.b64_alphabet.charAt(d >> 6 * (3 - e) & 63);
345 | }
346 | }
347 | return b;
348 | }
349 |
350 | /*
351 | * Gets the base64 representation for the given data
352 | *
353 | * http://phpjs.org
354 | * + original by: Tyler Akins (http://rumkin.com)
355 | * + improved by: Bayron Guevara
356 | * + improved by: Thunder.m
357 | * + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
358 | * + bugfixed by: Pellentesque Malesuada
359 | * + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
360 | * + improved by: Rafał Kukawski (http://kukawski.pl)
361 | *
362 | * @param string data The data to calculate the base64 representation from
363 | *
364 | * @return string The base64 representation
365 | */
366 |
367 | }, {
368 | key: "_base64_encode",
369 | value: function _base64_encode(a) {
370 | var d = void 0,
371 | e = void 0,
372 | f = void 0,
373 | b = void 0,
374 | g = 0,
375 | h = 0,
376 | i = this.b64_alphabet,
377 | c = [];
378 | if (!a) {
379 | return a;
380 | }
381 | do {
382 | d = a.charCodeAt(g++);
383 | e = a.charCodeAt(g++);
384 | f = a.charCodeAt(g++);
385 | b = d << 16 | e << 8 | f;
386 | d = b >> 18 & 63;
387 | e = b >> 12 & 63;
388 | f = b >> 6 & 63;
389 | b &= 63;
390 | c[h++] = i.charAt(d) + i.charAt(e) + i.charAt(f) + i.charAt(b);
391 | } while (g < a.length);
392 | i = c.join("");
393 | a = a.length % 3;
394 | return (a ? i.slice(0, a - 3) : i) + "===".slice(a || 3);
395 | }
396 |
397 | /*
398 | * Builds a HTTP query string from the given data
399 | *
400 | * http://phpjs.org
401 | * + original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
402 | * + improved by: Legaev Andrey
403 | * + improved by: Michael White (http://getsprink.com)
404 | * + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
405 | * + improved by: Brett Zamir (http://brett-zamir.me)
406 | * + revised by: stag019
407 | * + input by: Dreamer
408 | * + bugfixed by: Brett Zamir (http://brett-zamir.me)
409 | * + bugfixed by: MIO_KODUKI (http://mio-koduki.blogspot.com/)
410 | *
411 | * @param string data The data to concatenate
412 | *
413 | * @return string The HTTP query
414 | */
415 |
416 | }, {
417 | key: "_http_build_query",
418 | value: function _http_build_query(e, f, b) {
419 | function g(c, a, d) {
420 | var b = void 0,
421 | e = [];
422 | if (a === true) {
423 | a = "1";
424 | } else if (a === false) {
425 | a = "0";
426 | }
427 | if (null !== a) {
428 | if ((typeof a === "undefined" ? "undefined" : _typeof(a)) === "object") {
429 | for (b in a) {
430 | if (a.hasOwnProperty(b) && a[b] !== null) {
431 | e.push(g.call(this, c + "[" + b + "]", a[b], d));
432 | }
433 | }
434 | return e.join(d);
435 | }
436 | if (typeof a !== "function") {
437 | return this._url(c) + "=" + this._url(a);
438 | }
439 | throw "There was an error processing for http_build_query().";
440 | } else {
441 | return "";
442 | }
443 | }
444 | var d,
445 | c,
446 | h = [];
447 | if (!b) {
448 | b = "&";
449 | }
450 | for (c in e) {
451 | if (!e.hasOwnProperty(c)) {
452 | continue;
453 | }
454 | d = e[c];
455 | if (f && !isNaN(c)) {
456 | c = String(f) + c;
457 | }
458 | d = g.call(this, c, d, b);
459 | if (d !== "") {
460 | h.push(d);
461 | }
462 | }
463 | return h.join(b);
464 | }
465 |
466 | /**
467 | * Generates a (hopefully) unique random string
468 | *
469 | * @param int optional length The length of the string to generate
470 | *
471 | * @return string The random string
472 | */
473 |
474 | }, {
475 | key: "_nonce",
476 | value: function _nonce() {
477 | var length = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 8;
478 |
479 | if (length < 1) {
480 | throw "Invalid nonce length.";
481 | }
482 | var nonce = "";
483 | for (var i = 0; i < length; i++) {
484 | var character = Math.floor(Math.random() * 61);
485 | nonce += this.b64_alphabet.substring(character, character + 1);
486 | }
487 | return nonce;
488 | }
489 |
490 | /**
491 | * Sort array elements by key
492 | *
493 | * @param array input_arr The array to sort
494 | *
495 | * @return array The sorted keys
496 | */
497 |
498 | }, {
499 | key: "_ksort",
500 | value: function _ksort(input_arr) {
501 | var keys = [],
502 | sorter = void 0,
503 | k = void 0;
504 |
505 | sorter = function sorter(a, b) {
506 | var a_float = parseFloat(a),
507 | b_float = parseFloat(b),
508 | a_numeric = a_float + "" === a,
509 | b_numeric = b_float + "" === b;
510 | if (a_numeric && b_numeric) {
511 | return a_float > b_float ? 1 : a_float < b_float ? -1 : 0;
512 | } else if (a_numeric && !b_numeric) {
513 | return 1;
514 | } else if (!a_numeric && b_numeric) {
515 | return -1;
516 | }
517 | return a > b ? 1 : a < b ? -1 : 0;
518 | };
519 |
520 | // Make a list of key names
521 | for (k in input_arr) {
522 | if (input_arr.hasOwnProperty(k)) {
523 | keys.push(k);
524 | }
525 | }
526 | keys.sort(sorter);
527 | return keys;
528 | }
529 |
530 | /**
531 | * Clone objects
532 | *
533 | * @param object obj The object to clone
534 | *
535 | * @return object clone The cloned object
536 | */
537 |
538 | }, {
539 | key: "_clone",
540 | value: function _clone(obj) {
541 | var clone = {};
542 | for (var i in obj) {
543 | if (_typeof(obj[i]) === "object") {
544 | clone[i] = this._clone(obj[i]);
545 | } else {
546 | clone[i] = obj[i];
547 | }
548 | }
549 | return clone;
550 | }
551 |
552 | /**
553 | * Gets the XML HTTP Request object, trying to load it in various ways
554 | *
555 | * @return object The XMLHttpRequest object instance
556 | */
557 |
558 | }, {
559 | key: "_getXmlRequestObject",
560 | value: function _getXmlRequestObject() {
561 | var xml = null;
562 | // first, try the W3-standard object
563 | if ((typeof window === "undefined" ? "undefined" : _typeof(window)) === "object" && window && typeof window.XMLHttpRequest !== "undefined") {
564 | xml = new window.XMLHttpRequest();
565 | // then, try Titanium framework object
566 | } else if ((typeof Ti === "undefined" ? "undefined" : _typeof(Ti)) === "object" && Ti && typeof Ti.Network.createHTTPClient !== "undefined") {
567 | xml = Ti.Network.createHTTPClient();
568 | // are we in an old Internet Explorer?
569 | } else if (typeof ActiveXObject !== "undefined") {
570 | try {
571 | xml = new ActiveXObject("Microsoft.XMLHTTP");
572 | } catch (e) {
573 | throw "ActiveXObject object not defined.";
574 | }
575 | // now, consider RequireJS and/or Node.js objects
576 | } else if (typeof require === "function") {
577 | var XMLHttpRequest;
578 | // look for xmlhttprequest module
579 | try {
580 | XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;
581 | xml = new XMLHttpRequest();
582 | } catch (e1) {
583 | // or maybe the user is using xhr2
584 | try {
585 | XMLHttpRequest = require("xhr2");
586 | xml = new XMLHttpRequest();
587 | } catch (e2) {
588 | throw "xhr2 object not defined, cancelling.";
589 | }
590 | }
591 | }
592 | return xml;
593 | }
594 |
595 | /**
596 | * Parse URL-style parameters into object
597 | *
598 | * version: 1109.2015
599 | * discuss at: http://phpjs.org/functions/parse_str
600 | * + original by: Cagri Ekin
601 | * + improved by: Michael White (http://getsprink.com)
602 | * + tweaked by: Jack
603 | * + bugfixed by: Onno Marsman
604 | * + reimplemented by: stag019
605 | * + bugfixed by: Brett Zamir (http://brett-zamir.me)
606 | * + bugfixed by: stag019
607 | * - depends on: urldecode
608 | * + input by: Dreamer
609 | * + bugfixed by: Brett Zamir (http://brett-zamir.me)
610 | * % note 1: When no argument is specified, will put variables in global scope.
611 | *
612 | * @param string str String to parse
613 | * @param array array to load data into
614 | *
615 | * @return object
616 | */
617 |
618 | }, {
619 | key: "_parse_str",
620 | value: function _parse_str(str, array) {
621 | var glue1 = "=",
622 | glue2 = "&",
623 | array2 = String(str).replace(/^&?([\s\S]*?)&?$/, "$1").split(glue2),
624 | i,
625 | j,
626 | chr,
627 | tmp,
628 | key,
629 | value,
630 | bracket,
631 | keys,
632 | evalStr,
633 | fixStr = function fixStr(str) {
634 | return decodeURIComponent(str).replace(/([\\"'])/g, "\\$1").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
635 | };
636 | if (!array) {
637 | array = this.window;
638 | }
639 |
640 | for (i = 0; i < array2.length; i++) {
641 | tmp = array2[i].split(glue1);
642 | if (tmp.length < 2) {
643 | tmp = [tmp, ""];
644 | }
645 | key = fixStr(tmp[0]);
646 | value = fixStr(tmp[1]);
647 | while (key.charAt(0) === " ") {
648 | key = key.substr(1);
649 | }
650 | if (key.indexOf("\0") > -1) {
651 | key = key.substr(0, key.indexOf("\0"));
652 | }
653 | if (key && key.charAt(0) !== "[") {
654 | keys = [];
655 | bracket = 0;
656 | for (j = 0; j < key.length; j++) {
657 | if (key.charAt(j) === "[" && !bracket) {
658 | bracket = j + 1;
659 | } else if (key.charAt(j) === "]") {
660 | if (bracket) {
661 | if (!keys.length) {
662 | keys.push(key.substr(0, bracket - 1));
663 | }
664 | keys.push(key.substr(bracket, j - bracket));
665 | bracket = 0;
666 | if (key.charAt(j + 1) !== "[") {
667 | break;
668 | }
669 | }
670 | }
671 | }
672 | if (!keys.length) {
673 | keys = [key];
674 | }
675 | for (j = 0; j < keys[0].length; j++) {
676 | chr = keys[0].charAt(j);
677 | if (chr === " " || chr === "." || chr === "[") {
678 | keys[0] = keys[0].substr(0, j) + "_" + keys[0].substr(j + 1);
679 | }
680 | if (chr === "[") {
681 | break;
682 | }
683 | }
684 | evalStr = "array";
685 | for (j = 0; j < keys.length; j++) {
686 | key = keys[j];
687 | if (key !== "" && key !== " " || j === 0) {
688 | key = "'" + key + "'";
689 | } else {
690 | key = eval(evalStr + ".push([]);") - 1;
691 | }
692 | evalStr += "[" + key + "]";
693 | if (j !== keys.length - 1 && eval("typeof " + evalStr) === "undefined") {
694 | eval(evalStr + " = [];");
695 | }
696 | }
697 | evalStr += " = '" + value + "';\n";
698 | eval(evalStr);
699 | }
700 | }
701 | }
702 |
703 | /**
704 | * Get allowed API methods, sorted by GET or POST
705 | * Watch out for multiple-method "account/settings"!
706 | *
707 | * @return array $apimethods
708 | */
709 |
710 | }, {
711 | key: "getApiMethods",
712 | value: function getApiMethods() {
713 | var httpmethods = {
714 | GET: ["account/settings", "account/verify_credentials", "account_activity/all/:env_name/subscriptions", "account_activity/all/:env_name/subscriptions/list", "account_activity/all/:env_name/webhooks", "account_activity/all/webhooks", "account_activity/subscriptions/count", "account_activity/webhooks", "account_activity/webhooks/:webhook_id/subscriptions/all", "account_activity/webhooks/:webhook_id/subscriptions/all/list", "application/rate_limit_status", "blocks/ids", "blocks/list", "collections/entries", "collections/list", "collections/show", "custom_profiles/:id", "custom_profiles/list", "direct_messages/events/list", "direct_messages/events/show", "direct_messages/welcome_messages/list", "direct_messages/welcome_messages/rules/list", "direct_messages/welcome_messages/rules/show", "direct_messages/welcome_messages/show", "favorites/list", "feedback/events", "feedback/show/:id", "followers/ids", "followers/list", "friends/ids", "friends/list", "friendships/incoming", "friendships/lookup", "friendships/lookup", "friendships/no_retweets/ids", "friendships/outgoing", "friendships/show", "geo/id/:place_id", "geo/reverse_geocode", "geo/search", "help/configuration", "help/languages", "help/privacy", "help/tos", "lists/list", "lists/members", "lists/members/show", "lists/memberships", "lists/ownerships", "lists/show", "lists/statuses", "lists/subscribers", "lists/subscribers/show", "lists/subscriptions", "mutes/users/ids", "mutes/users/list", "oauth/authenticate", "oauth/authorize", "saved_searches/list", "saved_searches/show/:id", "search/tweets", "statuses/home_timeline", "statuses/mentions_timeline", "statuses/oembed", "statuses/retweeters/ids", "statuses/retweets/:id", "statuses/retweets_of_me", "statuses/sample", "statuses/show/:id", "statuses/user_timeline", "trends/available", "trends/closest", "trends/place", "users/profile_banner", "users/search", "users/show", "users/suggestions", "users/suggestions/:slug", "users/suggestions/:slug/members"],
715 | POST: ["account/remove_profile_banner", "account/settings__post", "account/update_profile", "account/update_profile_banner", "account/update_profile_image", "account_activity/all/:env_name/subscriptions", "account_activity/all/:env_name/webhooks", "account_activity/webhooks", "account_activity/webhooks/:webhook_id/subscriptions/all", "blocks/create", "blocks/destroy", "collections/create", "collections/destroy", "collections/entries/add", "collections/entries/curate", "collections/entries/move", "collections/entries/remove", "collections/update", "custom_profiles/new", "direct_messages/events/new", "direct_messages/indicate_typing", "direct_messages/mark_read", "direct_messages/welcome_messages/new", "direct_messages/welcome_messages/rules/new", "favorites/create", "favorites/destroy", "feedback/create", "friendships/create", "friendships/destroy", "friendships/update", "lists/create", "lists/destroy", "lists/members/create", "lists/members/create_all", "lists/members/destroy", "lists/members/destroy_all", "lists/subscribers/create", "lists/subscribers/destroy", "lists/update", "media/metadata/create", "media/upload", "mutes/users/create", "mutes/users/destroy", "oauth/access_token", "oauth/invalidate_token", "oauth/request_token", "oauth2/invalidate_token", "oauth2/token", "saved_searches/create", "saved_searches/destroy/:id", "statuses/destroy/:id", "statuses/filter", "statuses/lookup", "statuses/retweet/:id", "statuses/unretweet/:id", "statuses/update", "users/lookup", "users/report_spam"]
716 | };
717 | return httpmethods;
718 | }
719 |
720 | /**
721 | * Promise helpers
722 | */
723 |
724 | /**
725 | * Get a deferred object
726 | */
727 |
728 | }, {
729 | key: "_getDfd",
730 | value: function _getDfd() {
731 | if (typeof window !== "undefined") {
732 | if (typeof window.jQuery !== "undefined" && window.jQuery.Deferred) {
733 | return window.jQuery.Deferred();
734 | }
735 | if (typeof window.Q !== "undefined" && window.Q.defer) {
736 | return window.Q.defer();
737 | }
738 | if (typeof window.RSVP !== "undefined" && window.RSVP.defer) {
739 | return window.RSVP.defer();
740 | }
741 | if (typeof window.when !== "undefined" && window.when.defer) {
742 | return window.when.defer();
743 | }
744 | }
745 | if (typeof require !== "undefined") {
746 | var promise_class = false;
747 | try {
748 | promise_class = require("jquery");
749 | } catch (e) {}
750 | if (promise_class) {
751 | return promise_class.Deferred();
752 | }
753 | try {
754 | promise_class = require("q");
755 | } catch (e) {}
756 | if (!promise_class) {
757 | try {
758 | promise_class = require("rsvp");
759 | } catch (e) {}
760 | }
761 | if (!promise_class) {
762 | try {
763 | promise_class = require("when");
764 | } catch (e) {}
765 | }
766 | if (promise_class) {
767 | try {
768 | return promise_class.defer();
769 | } catch (e) {}
770 | }
771 | }
772 | return false;
773 | }
774 |
775 | /**
776 | * Get a promise from the dfd object
777 | */
778 |
779 | }, {
780 | key: "_getPromise",
781 | value: function _getPromise(dfd) {
782 | if (typeof dfd.promise === "function") {
783 | return dfd.promise();
784 | }
785 | return dfd.promise; // object
786 | }
787 |
788 | /**
789 | * __call() helpers
790 | */
791 |
792 | /**
793 | * Parse given params, detect query-style params
794 | *
795 | * @param array|string params Parameters to parse
796 | *
797 | * @return array apiparams
798 | */
799 |
800 | }, {
801 | key: "_parseApiParams",
802 | value: function _parseApiParams(params) {
803 | var apiparams = {};
804 | if ((typeof params === "undefined" ? "undefined" : _typeof(params)) === "object") {
805 | apiparams = params;
806 | } else {
807 | this._parse_str(params, apiparams); //TODO
808 | }
809 |
810 | return apiparams;
811 | }
812 |
813 | /**
814 | * Replace null and boolean parameters with their string representations
815 | *
816 | * @param array apiparams Parameter array to replace in
817 | *
818 | * @return array apiparams
819 | */
820 |
821 | }, {
822 | key: "_stringifyNullBoolParams",
823 | value: function _stringifyNullBoolParams(apiparams) {
824 | for (var key in apiparams) {
825 | if (!apiparams.hasOwnProperty(key)) {
826 | continue;
827 | }
828 | var value = apiparams[key];
829 | if (value === null) {
830 | apiparams[key] = "null";
831 | } else if (value === true || value === false) {
832 | apiparams[key] = value ? "true" : "false";
833 | }
834 | }
835 |
836 | return apiparams;
837 | }
838 |
839 | /**
840 | * API method mapping: Replaces _ with / character
841 | *
842 | * @param string fn Function called
843 | *
844 | * @return string API method to call
845 | */
846 |
847 | }, {
848 | key: "_mapFnInsertSlashes",
849 | value: function _mapFnInsertSlashes(fn) {
850 | return fn.split("_").join("/");
851 | }
852 |
853 | /**
854 | * API method mapping: Restore _ character in named parameters
855 | *
856 | * @param string method API method to call
857 | *
858 | * @return string API method with restored underscores
859 | */
860 |
861 | }, {
862 | key: "_mapFnRestoreParamUnderscores",
863 | value: function _mapFnRestoreParamUnderscores(method) {
864 | var url_parameters_with_underscore = ["screen_name", "place_id"];
865 | var i = void 0,
866 | param = void 0,
867 | replacement_was = void 0;
868 | for (i = 0; i < url_parameters_with_underscore.length; i++) {
869 | param = url_parameters_with_underscore[i].toUpperCase();
870 | replacement_was = param.split("_").join("/");
871 | method = method.split(replacement_was).join(param);
872 | }
873 |
874 | return method;
875 | }
876 |
877 | /**
878 | * Maps called PHP magic method name to Twitter API method
879 | *
880 | * @param string $fn Function called
881 | * @param array $apiparams byref API parameters
882 | *
883 | * @return string[] (string method, string method_template)
884 | */
885 |
886 | }, {
887 | key: "_mapFnToApiMethod",
888 | value: function _mapFnToApiMethod(fn, apiparams) {
889 | var method = "",
890 | param = void 0,
891 | i = void 0,
892 | j = void 0;
893 |
894 | // replace _ by /
895 | method = this._mapFnInsertSlashes(fn);
896 |
897 | // undo replacement for URL parameters
898 | method = this._mapFnRestoreParamUnderscores(method);
899 |
900 | // replace AA by URL parameters
901 | var method_template = method;
902 | var match = method.match(/[A-Z_]{2,}/);
903 | if (match) {
904 | for (i = 0; i < match.length; i++) {
905 | param = match[i];
906 | var param_l = param.toLowerCase();
907 | method_template = method_template.split(param).join(":" + param_l);
908 | if (typeof apiparams[param_l] === "undefined") {
909 | for (j = 0; j < 26; j++) {
910 | method_template = method_template.split(String.fromCharCode(65 + j)).join("_" + String.fromCharCode(97 + j));
911 | }
912 | throw "To call the templated method \"" + method_template + "\", specify the parameter value for \"" + param_l + "\".";
913 | }
914 | method = method.split(param).join(apiparams[param_l]);
915 | delete apiparams[param_l];
916 | }
917 | }
918 |
919 | // replace A-Z by _a-z
920 | for (i = 0; i < 26; i++) {
921 | method = method.split(String.fromCharCode(65 + i)).join("_" + String.fromCharCode(97 + i));
922 | method_template = method_template.split(String.fromCharCode(65 + i)).join("_" + String.fromCharCode(97 + i));
923 | }
924 |
925 | return [method, method_template];
926 | }
927 |
928 | /**
929 | * Detects HTTP method to use for API call
930 | *
931 | * @param string method The API method to call
932 | * @param array params The parameters to send along
933 | *
934 | * @return string The HTTP method that should be used
935 | */
936 |
937 | }, {
938 | key: "_detectMethod",
939 | value: function _detectMethod(method, params) {
940 | if (typeof params.httpmethod !== "undefined") {
941 | var httpmethod = params.httpmethod;
942 | delete params.httpmethod;
943 | return httpmethod;
944 | }
945 |
946 | // multi-HTTP method endpoints
947 | switch (method) {
948 | case "account/settings":
949 | case "account/login_verification_enrollment":
950 | case "account/login_verification_request":
951 | method = Object.keys(params).length ? method + "__post" : method;
952 | break;
953 | }
954 |
955 | var apimethods = this.getApiMethods();
956 | for (var _httpmethod in apimethods) {
957 | if (apimethods.hasOwnProperty(_httpmethod) && apimethods[_httpmethod].indexOf(method) > -1) {
958 | return _httpmethod;
959 | }
960 | }
961 | throw "Can't find HTTP method to use for \"" + method + "\".";
962 | }
963 |
964 | /**
965 | * Detects if API call should use multipart/form-data
966 | *
967 | * @param string method The API method to call
968 | *
969 | * @return bool Whether the method should be sent as multipart
970 | */
971 |
972 | }, {
973 | key: "_detectMultipart",
974 | value: function _detectMultipart(method) {
975 | var multiparts = [
976 | // Tweets
977 | "media/upload",
978 |
979 | // Users
980 | "account/update_profile_image", "account/update_profile_banner"];
981 | return multiparts.indexOf(method) > -1;
982 | }
983 |
984 | /**
985 | * Signature helper
986 | *
987 | * @param string httpmethod Usually either 'GET' or 'POST' or 'DELETE'
988 | * @param string method The API method to call
989 | * @param array base_params The signature base parameters
990 | *
991 | * @return string signature
992 | */
993 |
994 | }, {
995 | key: "_getSignature",
996 | value: function _getSignature(httpmethod, method, keys, base_params) {
997 | // convert params to string
998 | var base_string = "",
999 | key = void 0,
1000 | value = void 0;
1001 | for (var i = 0; i < keys.length; i++) {
1002 | key = keys[i];
1003 | value = base_params[key];
1004 | base_string += key + "=" + this._url(value) + "&";
1005 | }
1006 | base_string = base_string.substring(0, base_string.length - 1);
1007 | return this._sha1(httpmethod + "&" + this._url(method) + "&" + this._url(base_string));
1008 | }
1009 |
1010 | /**
1011 | * Generates the UNIX timestamp
1012 | */
1013 |
1014 | }, {
1015 | key: "_time",
1016 | value: function _time() {
1017 | return Math.round(new Date().getTime() / 1000);
1018 | }
1019 |
1020 | /**
1021 | * Generates an OAuth signature
1022 | *
1023 | * @param string httpmethod Usually either 'GET' or 'POST' or 'DELETE'
1024 | * @param string method The API method to call
1025 | * @param array optional params The API call parameters, associative
1026 | *
1027 | * @return string Authorization HTTP header
1028 | */
1029 |
1030 | }, {
1031 | key: "_sign",
1032 | value: function _sign(httpmethod, method) {
1033 | var params = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
1034 |
1035 | if (this._oauth_consumer_key === null) {
1036 | throw "To generate a signature, the consumer key must be set.";
1037 | }
1038 | var sign_params = {
1039 | consumer_key: this._oauth_consumer_key,
1040 | version: "1.0",
1041 | timestamp: this._time(),
1042 | nonce: this._nonce(),
1043 | signature_method: "HMAC-SHA1"
1044 | };
1045 | var sign_base_params = {};
1046 | for (var key in sign_params) {
1047 | if (!sign_params.hasOwnProperty(key)) {
1048 | continue;
1049 | }
1050 | var value = sign_params[key];
1051 | sign_base_params["oauth_" + key] = this._url(value);
1052 | }
1053 | if (this._oauth_token !== null) {
1054 | sign_base_params.oauth_token = this._url(this._oauth_token);
1055 | }
1056 | var oauth_params = this._clone(sign_base_params);
1057 | for (key in params) {
1058 | if (!params.hasOwnProperty(key)) {
1059 | continue;
1060 | }
1061 | sign_base_params[key] = params[key];
1062 | }
1063 | var keys = this._ksort(sign_base_params);
1064 |
1065 | var signature = this._getSignature(httpmethod, method, keys, sign_base_params);
1066 |
1067 | params = oauth_params;
1068 | params.oauth_signature = signature;
1069 | keys = this._ksort(params);
1070 | var authorization = "OAuth ";
1071 | for (var i = 0; i < keys.length; i++) {
1072 | key = keys[i];
1073 | authorization += key + "=\"" + this._url(params[key]) + "\", ";
1074 | }
1075 | return authorization.substring(0, authorization.length - 2);
1076 | }
1077 |
1078 | /**
1079 | * Build multipart request from upload params
1080 | *
1081 | * @param string method The API method to call
1082 | * @param array params The parameters to send along
1083 | *
1084 | * @return null|string The built multipart request body
1085 | */
1086 |
1087 | }, {
1088 | key: "_buildMultipart",
1089 | value: function _buildMultipart(method, params) {
1090 | // well, files will only work in multipart methods
1091 | if (!this._detectMultipart(method)) {
1092 | return;
1093 | }
1094 |
1095 | // only check specific parameters
1096 | var possible_methods = [
1097 | // Tweets
1098 | "media/upload",
1099 | // Accounts
1100 | "account/update_profile_image", "account/update_profile_banner"];
1101 | var possible_files = {
1102 | // Tweets
1103 | "media/upload": "media",
1104 | // Accounts
1105 | "account/update_profile_image": "image",
1106 | "account/update_profile_banner": "banner"
1107 | };
1108 | // method might have files?
1109 | if (possible_methods.indexOf(method) === -1) {
1110 | return;
1111 | }
1112 |
1113 | // check for filenames
1114 | possible_files = possible_files[method].split(" ");
1115 |
1116 | var multipart_border = "--------------------" + this._nonce();
1117 | var multipart_request = "";
1118 | for (var key in params) {
1119 | if (!params.hasOwnProperty(key)) {
1120 | continue;
1121 | }
1122 | multipart_request += "--" + multipart_border + "\r\nContent-Disposition: form-data; name=\"" + key + "\"";
1123 | if (possible_files.indexOf(key) === -1) {
1124 | multipart_request += "\r\nContent-Transfer-Encoding: base64";
1125 | }
1126 | multipart_request += "\r\n\r\n" + params[key] + "\r\n";
1127 | }
1128 | multipart_request += "--" + multipart_border + "--";
1129 | return multipart_request;
1130 | }
1131 |
1132 | /**
1133 | * Detects if API call should use media endpoint
1134 | *
1135 | * @param string method The API method to call
1136 | *
1137 | * @return bool Whether the method is defined in media API
1138 | */
1139 |
1140 | }, {
1141 | key: "_detectMedia",
1142 | value: function _detectMedia(method) {
1143 | var medias = ["media/metadata/create", "media/upload"];
1144 | return medias.indexOf(method) > -1;
1145 | }
1146 |
1147 | /**
1148 | * Detects if API call should use JSON body
1149 | *
1150 | * @param string method The API method to call
1151 | *
1152 | * @return bool Whether the method is defined as accepting JSON body
1153 | */
1154 |
1155 | }, {
1156 | key: "_detectJsonBody",
1157 | value: function _detectJsonBody(method) {
1158 | var json_bodies = ["collections/entries/curate", "custom_profiles/new", "direct_messages/events/new", "direct_messages/indicate_typing", "direct_messages/mark_read", "direct_messages/welcome_messages/new", "direct_messages/welcome_messages/rules/new", "direct_messages/welcome_messages/update", "media/metadata/create"];
1159 | return json_bodies.indexOf(method) > -1;
1160 | }
1161 |
1162 | /**
1163 | * Builds the complete API endpoint url
1164 | *
1165 | * @param string method The API method to call
1166 | *
1167 | * @return string The URL to send the request to
1168 | */
1169 |
1170 | }, {
1171 | key: "_getEndpoint",
1172 | value: function _getEndpoint(method) {
1173 | var url = void 0;
1174 | if (method.substring(0, 22) === "oauth/invalidate_token") {
1175 | url = this._endpoint + method + ".json";
1176 | } else if (method.substring(0, 5) === "oauth") {
1177 | url = this._endpoint_oauth + method;
1178 | } else if (this._detectMedia(method)) {
1179 | url = this._endpoint_media + method + ".json";
1180 | } else if (method === "statuses/oembed") {
1181 | url = this._endpoint_publish + "oembed";
1182 | } else {
1183 | url = this._endpoint + method + ".json";
1184 | }
1185 | return url;
1186 | }
1187 |
1188 | /**
1189 | * Parses the API reply to encode it in the set return_format
1190 | *
1191 | * @param string reply The actual reply, JSON-encoded or URL-encoded
1192 | *
1193 | * @return array|object The parsed reply
1194 | */
1195 |
1196 | }, {
1197 | key: "_parseApiReply",
1198 | value: function _parseApiReply(reply) {
1199 | if (typeof reply !== "string" || reply === "") {
1200 | return {};
1201 | }
1202 | if (reply === "[]") {
1203 | return [];
1204 | }
1205 | var parsed = void 0;
1206 | try {
1207 | parsed = JSON.parse(reply);
1208 | } catch (e) {
1209 | parsed = {};
1210 | // assume XML
1211 | if (reply.match(/^<\?xml/)) {
1212 | var errors = void 0;
1213 | if (errors = reply.match(/(.+)<\/errors>/)) {
1214 | parsed.errors = {};
1215 | errors = errors[1].match(/(.+)/);
1218 | parsed.errors[error[1]] = error[2];
1219 | }
1220 | }
1221 | return parsed;
1222 | }
1223 | // assume query format
1224 | var elements = reply.split("&");
1225 | for (var _i = 0; _i < elements.length; _i++) {
1226 | var element = elements[_i].split("=", 2);
1227 | if (element.length > 1) {
1228 | parsed[element[0]] = decodeURIComponent(element[1]);
1229 | } else {
1230 | parsed[element[0]] = null;
1231 | }
1232 | }
1233 | }
1234 | return parsed;
1235 | }
1236 |
1237 | /**
1238 | * Uncommon API methods
1239 | */
1240 |
1241 | /**
1242 | * Gets the OAuth authenticate URL for the current request token
1243 | *
1244 | * @return object Promise
1245 | */
1246 |
1247 | }, {
1248 | key: "oauth_authenticate",
1249 | value: function oauth_authenticate() {
1250 | var params = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
1251 | var callback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined;
1252 | var type = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "authenticate";
1253 |
1254 | var dfd = this._getDfd();
1255 | if (typeof params.force_login === "undefined") {
1256 | params.force_login = null;
1257 | }
1258 | if (typeof params.screen_name === "undefined") {
1259 | params.screen_name = null;
1260 | }
1261 | if (["authenticate", "authorize"].indexOf(type) === -1) {
1262 | type = "authenticate";
1263 | }
1264 | if (this._oauth_token === null) {
1265 | var error = "To get the " + type + " URL, the OAuth token must be set.";
1266 | if (dfd) {
1267 | dfd.reject({ error: error });
1268 | return this._getPromise(dfd);
1269 | }
1270 | throw error;
1271 | }
1272 | var url = this._endpoint_oauth + "oauth/" + type + "?oauth_token=" + this._url(this._oauth_token);
1273 | if (params.force_login === true) {
1274 | url += "&force_login=1";
1275 | }
1276 | if (params.screen_name !== null) {
1277 | url += "&screen_name=" + params.screen_name;
1278 | }
1279 | if (typeof callback === "function") {
1280 | callback(url);
1281 | }
1282 | if (dfd) {
1283 | dfd.resolve({ reply: url });
1284 | return this._getPromise(dfd);
1285 | }
1286 | // no promises
1287 | return true;
1288 | }
1289 |
1290 | /**
1291 | * Gets the OAuth authorize URL for the current request token
1292 | *
1293 | * @return string The OAuth authorize URL
1294 | */
1295 |
1296 | }, {
1297 | key: "oauth_authorize",
1298 | value: function oauth_authorize(params, callback) {
1299 | return this.oauth_authenticate(params, callback, "authorize");
1300 | }
1301 |
1302 | /**
1303 | * Gets the OAuth bearer token
1304 | *
1305 | * @return object Promise
1306 | */
1307 |
1308 | }, {
1309 | key: "oauth2_token",
1310 | value: function oauth2_token(callback) {
1311 | var _this2 = this;
1312 |
1313 | var dfd = this._getDfd();
1314 |
1315 | if (this._oauth_consumer_key === null) {
1316 | var error = "To obtain a bearer token, the consumer key must be set.";
1317 | if (dfd) {
1318 | dfd.reject({ error: error });
1319 | return this._getPromise(dfd);
1320 | }
1321 | throw error;
1322 | }
1323 |
1324 | if (!dfd && typeof callback === "undefined") {
1325 | callback = function callback() {};
1326 | }
1327 |
1328 | var post_fields = "grant_type=client_credentials";
1329 | var url = this._endpoint_oauth + "oauth2/token";
1330 |
1331 | if (this._use_proxy) {
1332 | url = url.replace(this._endpoint_base, this._endpoint_proxy);
1333 | }
1334 |
1335 | var xml = this._getXmlRequestObject();
1336 | if (xml === null) {
1337 | return;
1338 | }
1339 | xml.open("POST", url, true);
1340 | xml.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
1341 | xml.setRequestHeader((this._use_proxy ? "X-" : "") + "Authorization", "Basic " + this._base64_encode(this._oauth_consumer_key + ":" + this._oauth_consumer_secret));
1342 |
1343 | xml.onreadystatechange = function () {
1344 | if (xml.readyState >= 4) {
1345 | var httpstatus = 12027;
1346 | try {
1347 | httpstatus = xml.status;
1348 | } catch (e) {}
1349 | var response = "";
1350 | try {
1351 | response = xml.responseText;
1352 | } catch (e) {}
1353 | var reply = _this2._parseApiReply(response);
1354 | reply.httpstatus = httpstatus;
1355 | if (httpstatus === 200) {
1356 | _this2.setBearerToken(reply.access_token);
1357 | }
1358 | if (typeof callback === "function") {
1359 | callback(reply);
1360 | }
1361 | if (dfd) {
1362 | dfd.resolve({ reply: reply });
1363 | }
1364 | }
1365 | };
1366 | // function called when an error occurs, including a timeout
1367 | xml.onerror = function (e) {
1368 | if (typeof callback === "function") {
1369 | callback(null, e);
1370 | }
1371 | if (dfd) {
1372 | dfd.reject(e);
1373 | }
1374 | };
1375 | xml.timeout = 30000; // in milliseconds
1376 |
1377 | xml.send(post_fields);
1378 | if (dfd) {
1379 | return this._getPromise(dfd);
1380 | }
1381 | }
1382 |
1383 | /**
1384 | * Calls the API using cURL
1385 | *
1386 | * @param string httpmethod The HTTP method to use for making the request
1387 | * @param string method The API method to call
1388 | * @param array optional params The parameters to send along
1389 | * @param bool optional multipart Whether to use multipart/form-data
1390 | * @param bool optional app_only_auth Whether to use app-only bearer authentication
1391 | * @param function callback The function to call with the API call result
1392 | *
1393 | * @return mixed The API reply, encoded in the set return_format
1394 | */
1395 |
1396 | }, {
1397 | key: "_callApi",
1398 | value: function _callApi(httpmethod, method) {
1399 | var params = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
1400 | var multipart = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
1401 |
1402 | var _this3 = this;
1403 |
1404 | var app_only_auth = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
1405 | var callback = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : function () {};
1406 |
1407 | var dfd = this._getDfd();
1408 |
1409 | var url = this._getEndpoint(method),
1410 | authorization = null;
1411 |
1412 | var xml = this._getXmlRequestObject();
1413 | if (xml === null) {
1414 | return;
1415 | }
1416 | var post_fields = void 0;
1417 |
1418 | if (httpmethod === "GET") {
1419 | var url_with_params = url;
1420 | if (JSON.stringify(params) !== "{}") {
1421 | url_with_params += "?" + this._http_build_query(params);
1422 | }
1423 | if (!app_only_auth) {
1424 | authorization = this._sign(httpmethod, url, params);
1425 | }
1426 |
1427 | if (this._use_proxy) {
1428 | url_with_params = url_with_params.replace(this._endpoint_base, this._endpoint_proxy).replace(this._endpoint_base_media, this._endpoint_proxy);
1429 | }
1430 | xml.open(httpmethod, url_with_params, true);
1431 | } else {
1432 | if (multipart) {
1433 | if (!app_only_auth) {
1434 | authorization = this._sign(httpmethod, url, {});
1435 | }
1436 | params = this._buildMultipart(method, params);
1437 | } else if (this._detectJsonBody(method)) {
1438 | authorization = this._sign(httpmethod, url, {});
1439 | params = JSON.stringify(params);
1440 | } else {
1441 | if (!app_only_auth) {
1442 | authorization = this._sign(httpmethod, url, params);
1443 | }
1444 | params = this._http_build_query(params);
1445 | }
1446 | post_fields = params;
1447 | if (this._use_proxy || multipart) {
1448 | // force proxy for multipart base64
1449 | url = url.replace(this._endpoint_base, this._endpoint_proxy).replace(this._endpoint_base_media, this._endpoint_proxy);
1450 | }
1451 | xml.open(httpmethod, url, true);
1452 | if (multipart) {
1453 | xml.setRequestHeader("Content-Type", "multipart/form-data; boundary=" + post_fields.split("\r\n")[0].substring(2));
1454 | } else if (this._detectJsonBody(method)) {
1455 | xml.setRequestHeader("Content-Type", "application/json");
1456 | } else {
1457 | xml.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
1458 | }
1459 | }
1460 | if (app_only_auth) {
1461 | if (this._oauth_consumer_key === null && this._oauth_bearer_token === null) {
1462 | var error = "To make an app-only auth API request, consumer key or bearer token must be set.";
1463 | if (dfd) {
1464 | dfd.reject({ error: error });
1465 | return this._getPromise(dfd);
1466 | }
1467 | throw error;
1468 | }
1469 | // automatically fetch bearer token, if necessary
1470 | if (this._oauth_bearer_token === null) {
1471 | if (dfd) {
1472 | return this.oauth2_token().then(function () {
1473 | return _this3._callApi(httpmethod, method, params, multipart, app_only_auth, callback);
1474 | });
1475 | }
1476 | this.oauth2_token(function () {
1477 | _this3._callApi(httpmethod, method, params, multipart, app_only_auth, callback);
1478 | });
1479 | return;
1480 | }
1481 | authorization = "Bearer " + this._oauth_bearer_token;
1482 | }
1483 | if (authorization !== null) {
1484 | xml.setRequestHeader((this._use_proxy ? "X-" : "") + "Authorization", authorization);
1485 | }
1486 | xml.onreadystatechange = function () {
1487 | if (xml.readyState >= 4) {
1488 | var httpstatus = 12027;
1489 | try {
1490 | httpstatus = xml.status;
1491 | } catch (e) {}
1492 | var response = "";
1493 | try {
1494 | response = xml.responseText;
1495 | } catch (e) {}
1496 | var reply = _this3._parseApiReply(response);
1497 | reply.httpstatus = httpstatus;
1498 | var rate = null;
1499 | if (typeof xml.getResponseHeader !== "undefined" && xml.getResponseHeader("x-rate-limit-limit") !== "") {
1500 | rate = {
1501 | limit: xml.getResponseHeader("x-rate-limit-limit"),
1502 | remaining: xml.getResponseHeader("x-rate-limit-remaining"),
1503 | reset: xml.getResponseHeader("x-rate-limit-reset")
1504 | };
1505 | }
1506 | if (typeof callback === "function") {
1507 | callback(reply, rate);
1508 | }
1509 | if (dfd) {
1510 | dfd.resolve({ reply: reply, rate: rate });
1511 | }
1512 | }
1513 | };
1514 | // function called when an error occurs, including a timeout
1515 | xml.onerror = function (e) {
1516 | if (typeof callback === "function") {
1517 | callback(null, null, e);
1518 | }
1519 | if (dfd) {
1520 | dfd.reject(e);
1521 | }
1522 | };
1523 | xml.timeout = 30000; // in milliseconds
1524 |
1525 | xml.send(httpmethod === "GET" ? null : post_fields);
1526 | if (dfd) {
1527 | return this._getPromise(dfd);
1528 | }
1529 | return true;
1530 | }
1531 |
1532 | /**
1533 | * Main API handler working on any requests you issue
1534 | *
1535 | * @param string fn The member function you called
1536 | * @param array params The parameters you sent along
1537 | * @param function callback The callback to call with the reply
1538 | * @param bool app_only_auth Whether to use app-only auth
1539 | *
1540 | * @return object Promise
1541 | */
1542 |
1543 | }, {
1544 | key: "__call",
1545 | value: function __call(fn) {
1546 | var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
1547 | var callback = arguments[2];
1548 | var app_only_auth = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
1549 |
1550 | if (typeof callback !== "function" && typeof params === "function") {
1551 | callback = params;
1552 | params = {};
1553 | if (typeof callback === "boolean") {
1554 | app_only_auth = callback;
1555 | }
1556 | } else if (typeof callback === "undefined") {
1557 | callback = function callback() {};
1558 | }
1559 | switch (fn) {
1560 | case "oauth_authenticate":
1561 | case "oauth_authorize":
1562 | return this[fn](params, callback);
1563 |
1564 | case "oauth2_token":
1565 | return this[fn](callback);
1566 | }
1567 |
1568 | // parse parameters
1569 | var apiparams = this._parseApiParams(params);
1570 |
1571 | // stringify null and boolean parameters
1572 | apiparams = this._stringifyNullBoolParams(apiparams);
1573 |
1574 | // reset token when requesting a new token (causes 401 for signature error on 2nd+ requests)
1575 | if (fn === "oauth_requestToken") {
1576 | this.setToken(null, null);
1577 | }
1578 |
1579 | // map function name to API method
1580 |
1581 | var _mapFnToApiMethod2 = this._mapFnToApiMethod(fn, apiparams),
1582 | _mapFnToApiMethod3 = _slicedToArray(_mapFnToApiMethod2, 2),
1583 | method = _mapFnToApiMethod3[0],
1584 | method_template = _mapFnToApiMethod3[1],
1585 | httpmethod = this._detectMethod(method_template, apiparams),
1586 | multipart = this._detectMultipart(method_template);
1587 |
1588 | return this._callApi(httpmethod, method, apiparams, multipart, app_only_auth, callback);
1589 | }
1590 | }]);
1591 |
1592 | return Codebird;
1593 | }();
1594 |
1595 | if ((typeof module === "undefined" ? "undefined" : _typeof(module)) === "object" && module && _typeof(module.exports) === "object") {
1596 | // Expose codebird as module.exports in loaders that implement the Node
1597 | // module pattern (including browserify). Do not create the global, since
1598 | // the user will be storing it themselves locally, and globals are frowned
1599 | // upon in the Node module world.
1600 | module.exports = Codebird;
1601 | } else {
1602 | // Otherwise expose codebird to the global object as usual
1603 | if ((typeof window === "undefined" ? "undefined" : _typeof(window)) === "object" && window) {
1604 | window.Codebird = Codebird;
1605 | }
1606 |
1607 | // Register as a named AMD module, since codebird can be concatenated with other
1608 | // files that may use define, but not via a proper concatenation script that
1609 | // understands anonymous AMD modules. A named AMD is safest and most robust
1610 | // way to register. Lowercase codebird is used because AMD module names are
1611 | // derived from file names, and codebird is normally delivered in a lowercase
1612 | // file name. Do this after creating the global so that if an AMD module wants
1613 | // to call noConflict to hide this version of codebird, it will work.
1614 | if (typeof define === "function" && define.amd) {
1615 | define("codebird", [], function () {
1616 | return Codebird;
1617 | });
1618 | }
1619 | }
1620 | })();
1621 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "codebird",
3 | "version": "3.0.0-dev",
4 | "description": "A Twitter library in JavaScript.",
5 | "keywords": [
6 | "Twitter",
7 | "API",
8 | "networking",
9 | "CORS"
10 | ],
11 | "homepage": "http://www.jublo.net/projects/codebird/js",
12 | "bugs": "https://github.com/jublonet/codebird-js/issues",
13 | "license": "GPL-3.0+",
14 | "author": "Jublo Limited (http://jublo.net/)",
15 | "contributors": [
16 | "Joshua Atkins (http://atkins.im/)",
17 | "J.M. (http://mynetx.net/)"
18 | ],
19 | "main": "codebird.es7.js",
20 | "directories": {
21 | "example": "examples"
22 | },
23 | "repository": {
24 | "type": "git",
25 | "url": "https://github.com/jublonet/codebird-js.git"
26 | },
27 | "scripts": {
28 | "build-cb": "babel codebird.es7.js -o codebird.js",
29 | "build-cbt": "babel test/codebirdt.es7.js -o test/codebirdt.js",
30 | "build-cbm": "babel test/codebirdm.es7.js -o test/codebirdm.js",
31 | "build": "npm run build-cb && npm run build-cbt && npm run build-cbm",
32 | "test-run": "tape test/*_tests.js | faucet",
33 | "test": "npm run build && npm run test-run"
34 | },
35 | "devDependencies": {
36 | "babel-cli": "^6.26.0",
37 | "babel-preset-es2015": "^6.24.1",
38 | "eslint": "^5.6.0",
39 | "faucet": "0.0.1",
40 | "q": "^1.5.1",
41 | "tape": "^4.9.1",
42 | "tape-promise": "^1.1.0",
43 | "xmlhttprequest": "^1.8.0"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/test/README:
--------------------------------------------------------------------------------
1 | The following methods are covered by unit tests:
2 |
3 | _detectMethod
4 | _detectMultipart
5 | _detectMedia
6 | _getEndpoint
7 | oauth_authenticate
8 | oauth_authorize
9 | oauth2_token
10 |
--------------------------------------------------------------------------------
/test/codebirdm.es7.js:
--------------------------------------------------------------------------------
1 | import CodebirdT from "./codebirdt";
2 |
3 | /**
4 | * A Twitter library in JavaScript
5 | *
6 | * @package codebird-test
7 | * @version 3.0.0-dev
8 | * @author Jublo Limited
9 | * @copyright 2010-2018 Jublo Limited
10 | * @license http://opensource.org/licenses/GPL-3.0 GNU Public License 3.0
11 | * @link https://github.com/jublonet/codebird-php
12 | */
13 |
14 | /**
15 | * A Twitter library in JavaScript
16 | *
17 | * @package codebird-test
18 | */
19 | export default class CodebirdM extends CodebirdT {
20 | constructor() {
21 | super();
22 | /**
23 | * Mock API replies
24 | */
25 | this._mock_replies = {
26 | default: {
27 | httpstatus: 404,
28 | reply:
29 | '{"errors":[{"message":"Sorry, that page does not exist","code":34}]}'
30 | },
31 | "GET https://api.twitter.com/1.1/users/show.json?screen_name=TwitterAPI": {
32 | httpstatus: 200,
33 | reply:
34 | '{"id":6253282,"id_str":"6253282","name":"Twitter API","screen_name":"twitterapi","location":"San Francisco, CA","profile_location":null,"description":"The Real Twitter API. I tweet about API changes, service issues and happily answer questions about Twitter and our API. Don\'t get an answer? It\'s on my website.","url":"http://t.co/78pYTvWfJd","entities":{"url":{"urls":[{"url":"http://t.co/78pYTvWfJd","expanded_url":"http://dev.twitter.com","display_url":"dev.twitter.com","indices":[0,22]}]},"description":{"urls":[]}},"protected":false,"followers_count":4993679,"friends_count":48,"listed_count":13001,"created_at":"Wed May 23 06:01:13 +0000 2007","favourites_count":27,"utc_offset":-28800,"time_zone":"Pacific Time (US & Canada)","geo_enabled":true,"verified":true,"statuses_count":3553,"lang":"en","status":{"created_at":"Tue Nov 24 08:56:07 +0000 2015","id":669077021138493440,"id_str":"669077021138493440","text":"Additional 64-bit entity ID migration coming in Feb 2016 https://t.co/eQIGvw1rsJ","source":"\u003ca href=\\"https://about.twitter.com/products/tweetdeck\\" rel=\\"nofollow\\"\u003eTweetDeck\u003c/a\u003e","truncated":false,"in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":null,"in_reply_to_user_id_str":null,"in_reply_to_screen_name":null,"geo":null,"coordinates":null,"place":null,"contributors":null,"retweet_count":67,"favorite_count":79,"entities":{"hashtags":[],"symbols":[],"user_mentions":[],"urls":[{"url":"https://t.co/eQIGvw1rsJ","expanded_url":"https://twittercommunity.com/t/migration-of-twitter-core-entities-to-64-bit-ids/56881","display_url":"twittercommunity.com/t/migration-of\u2026","indices":[57,80]}]},"favorited":false,"retweeted":false,"possibly_sensitive":false,"lang":"en"},"contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"C0DEED","profile_background_image_url":"http://pbs.twimg.com/profile_background_images/656927849/miyt9dpjz77sc0w3d4vj.png","profile_background_image_url_https":"https://pbs.twimg.com/profile_background_images/656927849/miyt9dpjz77sc0w3d4vj.png","profile_background_tile":true,"profile_image_url":"http://pbs.twimg.com/profile_images/2284174872/7df3h38zabcvjylnyfe3_normal.png","profile_image_url_https":"https://pbs.twimg.com/profile_images/2284174872/7df3h38zabcvjylnyfe3_normal.png","profile_banner_url":"https://pbs.twimg.com/profile_banners/6253282/1431474710","profile_link_color":"0084B4","profile_sidebar_border_color":"C0DEED","profile_sidebar_fill_color":"DDEEF6","profile_text_color":"333333","profile_use_background_image":true,"has_extended_profile":false,"default_profile":false,"default_profile_image":false,"following":true,"follow_request_sent":false,"notifications":false}'
35 | },
36 | "POST https://api.twitter.com/oauth2/token": {
37 | httpstatus: 200,
38 | reply: '{"token_type":"bearer","access_token":"VqiO0n2HrKE"}'
39 | },
40 | "POST https://api.twitter.com/oauth/request_token": {
41 | httpstatus: 401,
42 | reply:
43 | 'Callback URL not approved for this client application. Approved callback URLs can be adjusted in your application settings'
44 | }
45 | };
46 |
47 | this.xml = {
48 | readyState: 4,
49 | open: (httpmethod, url) => {
50 | this.xml.httpmethod = httpmethod;
51 | this.xml.url = url;
52 | const key = `${httpmethod} ${url}`;
53 | if (this._mock_replies.hasOwnProperty(key)) {
54 | this.xml.status = this._mock_replies[key].httpstatus;
55 | this.xml.responseText = this._mock_replies[key].reply;
56 | } else {
57 | this.xml.status = this._mock_replies.default.httpstatus;
58 | this.xml.responseText = this._mock_replies.default.reply;
59 | }
60 | },
61 | setRequestHeader: () => true,
62 | onreadystatechange: () => false,
63 | send: function() {
64 | setTimeout(this.onreadystatechange, 200);
65 | }
66 | };
67 | }
68 |
69 | _getXmlRequestObject() {
70 | return this.xml;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/test/codebirdt.es7.js:
--------------------------------------------------------------------------------
1 | import Codebird from "../codebird";
2 |
3 | /**
4 | * A Twitter library in JavaScript
5 | *
6 | * @package codebird-test
7 | * @version 3.0.0-dev
8 | * @author Jublo Limited
9 | * @copyright 2010-2018 Jublo Limited
10 | * @license http://opensource.org/licenses/GPL-3.0 GNU Public License 3.0
11 | * @link https://github.com/jublonet/codebird-php
12 | */
13 |
14 | /**
15 | * A Twitter library in JavaScript
16 | *
17 | * @package codebird-test
18 | */
19 | export default class CodebirdT extends Codebird {
20 | /**
21 | * Returns properties
22 | *
23 | * @param string property The property to get
24 | *
25 | * @return mixed Property
26 | */
27 | get(property) {
28 | if (typeof this[property] !== "undefined") {
29 | return this[property];
30 | }
31 | throw `Property ${property} is not defined.`;
32 | }
33 |
34 | /**
35 | * Returns static properties
36 | *
37 | * @param string property The property to get
38 | *
39 | * @return mixed Property
40 | */
41 | getStatic(property) {
42 | if (typeof CodebirdT[property] !== "undefined") {
43 | return CodebirdT[property];
44 | }
45 | throw `Static property ${property} is not defined.`;
46 | }
47 |
48 | /**
49 | * Calls methods
50 | *
51 | * @param string method The method to call
52 | * @param mixed params The parameters to send along
53 | *
54 | * @return mixed Return value
55 | */
56 | call(method, params = []) {
57 | if (typeof this[method] === "function") {
58 | return this[method].apply(this, params);
59 | }
60 | throw `Method ${method} is not defined.`;
61 | }
62 |
63 | /**
64 | * Calls static methods
65 | *
66 | * @param string method The method to call
67 | * @param mixed params The parameters to send along
68 | *
69 | * @return mixed Return value
70 | */
71 | callStatic(method, params = []) {
72 | if (typeof CodebirdT[method] === "function") {
73 | return CodebirdT[method].apply(this, params);
74 | }
75 | throw `Static method ${method} is not defined.`;
76 | }
77 |
78 | /*
79 | // Unit testing code
80 | this.__test = {
81 | call: (name, params = []) => this[name].apply(this, params),
82 | get: name => this[name],
83 | mock: methods_to_mock => {
84 | for (let name in methods_to_mock) {
85 | if (methods_to_mock.hasOwnProperty(name)) {
86 | this[name] = methods_to_mock[name];
87 | }
88 | }
89 | }
90 | };
91 | */
92 | }
93 |
--------------------------------------------------------------------------------
/test/detection_tests.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | _test = require("tape-promise"),
3 | test = _test(tape), // decorate tape
4 | CodebirdT = require("./codebirdt");
5 |
6 | function getCB() {
7 | return new CodebirdT.default();
8 | }
9 |
10 | test("Tests _detectMethod", function(t) {
11 | const cb = getCB();
12 |
13 | t.throws(function() {
14 | cb.call("_detectMethod", ["non-existent", {}]);
15 | }, 'Can\'t find HTTP method to use for "non-existent".');
16 |
17 | // forced httpmethod
18 | t.equal(
19 | cb.call("_detectMethod", ["doesnt-matter", { httpmethod: "DELETE" }]),
20 | "DELETE"
21 | );
22 |
23 | // normal detection
24 | t.equal(
25 | cb.call("_detectMethod", [
26 | "account_activity/all/:env_name/subscriptions",
27 | {}
28 | ]),
29 | "GET"
30 | );
31 | t.equal(cb.call("_detectMethod", ["search/tweets", {}]), "GET");
32 | t.equal(cb.call("_detectMethod", ["statuses/update", {}]), "POST");
33 | t.equal(cb.call("_detectMethod", ["statuses/destroy/:id", {}]), "POST");
34 |
35 | // parameter-based detection
36 | t.equal(cb.call("_detectMethod", ["account/settings", {}]), "GET");
37 | t.equal(cb.call("_detectMethod", ["account/settings", { test: 12 }]), "POST");
38 |
39 | t.end();
40 | });
41 |
42 | test("Tests _detectMultipart", function(t) {
43 | const cb = getCB();
44 |
45 | t.false(cb.call("_detectMultipart", ["statuses/update"]));
46 | t.true(cb.call("_detectMultipart", ["media/upload"]));
47 |
48 | t.end();
49 | });
50 |
51 | test("Tests _detectMedia", function(t) {
52 | const cb = getCB();
53 |
54 | t.false(cb.call("_detectMedia", ["statuses/update"]));
55 | t.true(cb.call("_detectMedia", ["media/upload"]));
56 |
57 | t.end();
58 | });
59 |
60 | test("Tests _getEndpoint", function(t) {
61 | const cb = getCB();
62 |
63 | t.equal(
64 | cb.call("_getEndpoint", ["statuses/update", "statuses/update"]),
65 | "https://api.twitter.com/1.1/statuses/update.json"
66 | );
67 | t.equal(
68 | cb.call("_getEndpoint", ["oauth/authenticate", "oauth/authenticate"]),
69 | "https://api.twitter.com/oauth/authenticate"
70 | );
71 | t.equal(
72 | cb.call("_getEndpoint", [
73 | "oauth/invalidate_token",
74 | "oauth/invalidate_token"
75 | ]),
76 | "https://api.twitter.com/1.1/oauth/invalidate_token.json"
77 | );
78 | t.equal(
79 | cb.call("_getEndpoint", ["oauth2/token", "oauth2/token"]),
80 | "https://api.twitter.com/oauth2/token"
81 | );
82 | t.equal(
83 | cb.call("_getEndpoint", ["media/upload", "media/upload"]),
84 | "https://upload.twitter.com/1.1/media/upload.json"
85 | );
86 | t.equal(
87 | cb.call("_getEndpoint", ["statuses/oembed", "statuses/oembed"]),
88 | "https://publish.twitter.com/oembed"
89 | );
90 |
91 | t.end();
92 | });
93 |
--------------------------------------------------------------------------------
/test/functional_tests.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | _test = require("tape-promise"),
3 | test = _test(tape), // decorate tape
4 | CodebirdT = require("./codebirdt"),
5 | CodebirdM = require("./codebirdm");
6 |
7 | function getCB(mock) {
8 | if (typeof mock === "undefined") {
9 | var mock = false;
10 | }
11 | var cb = mock ? new CodebirdM.default() : new CodebirdT.default();
12 | cb.setConsumerKey("123", "456");
13 |
14 | return cb;
15 | }
16 |
17 | test("Tests statuses/update", function(t) {
18 | const cb = getCB();
19 | t.plan(1);
20 |
21 | cb.setToken("123", "456");
22 | return cb
23 | .__call("statuses_update", { status: "Whohoo, I just tweeted!" })
24 | .then(function(reply, rate, err) {
25 | t.deepEqual(reply, {
26 | rate: {
27 | limit: null,
28 | remaining: null,
29 | reset: null
30 | },
31 | reply: {
32 | errors: [{ code: 89, message: "Invalid or expired token." }],
33 | httpstatus: 401
34 | }
35 | });
36 | });
37 | });
38 |
39 | test("Tests reply XML detection", function(t) {
40 | const cb = getCB(true);
41 | t.plan(1);
42 |
43 | cb.setToken("123", "456");
44 | return cb
45 | .__call("oauth_requestToken", {
46 | oauth_callback: "http://www.example.com/#xml_detection"
47 | })
48 | .then(function(reply, rate, err) {
49 | t.deepEqual(reply, {
50 | reply: {
51 | errors: {
52 | "415":
53 | "Callback URL not approved for this client application. Approved callback URLs can be adjusted in your application settings"
54 | },
55 | httpstatus: 401
56 | },
57 | rate: null
58 | });
59 | });
60 | });
61 |
62 | test("Tests logout method", function(t) {
63 | const cb = getCB(true);
64 | t.plan(4);
65 |
66 | cb.setToken("123", "456");
67 |
68 | t.equal(cb.get("_oauth_token"), "123");
69 | t.equal(cb.get("_oauth_token_secret"), "456");
70 |
71 | return cb.logout().then(function() {
72 | t.equal(cb.get("_oauth_token"), null);
73 | t.equal(cb.get("_oauth_token_secret"), null);
74 | });
75 | });
76 |
--------------------------------------------------------------------------------
/test/media_tests.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | _test = require("tape-promise"),
3 | test = _test(tape), // decorate tape
4 | CodebirdT = require("./codebirdt");
5 |
6 | function getCB() {
7 | var cb = new CodebirdT.default;
8 | cb.setConsumerKey("123", "456");
9 |
10 | return cb;
11 | }
12 |
13 | test("Tests media/upload base64", function (t) {
14 | const cb = getCB();
15 | t.plan(1);
16 |
17 | cb.setToken("123", "456");
18 | return cb.__call(
19 | "media_upload",
20 | {"media_data": "R0lGODlhPQBEAPeoAJosM//AwO/AwHVYZ/z595kzAP/s7P+goOXMv8+fhw/v739/f+8PD98fH/8mJl+fn/9ZWb8/PzWlwv///6wWGbImAPgTEMImIN9gUFCEm/gDALULDN8PAD6atYdCTX9gUNKlj8wZAKUsAOzZz+UMAOsJAP/Z2ccMDA8PD/95eX5NWvsJCOVNQPtfX/8zM8+QePLl38MGBr8JCP+zs9myn/8GBqwpAP/GxgwJCPny78lzYLgjAJ8vAP9fX/+MjMUcAN8zM/9wcM8ZGcATEL+QePdZWf/29uc/P9cmJu9MTDImIN+/r7+/vz8/P8VNQGNugV8AAF9fX8swMNgTAFlDOICAgPNSUnNWSMQ5MBAQEJE3QPIGAM9AQMqGcG9vb6MhJsEdGM8vLx8fH98AANIWAMuQeL8fABkTEPPQ0OM5OSYdGFl5jo+Pj/+pqcsTE78wMFNGQLYmID4dGPvd3UBAQJmTkP+8vH9QUK+vr8ZWSHpzcJMmILdwcLOGcHRQUHxwcK9PT9DQ0O/v70w5MLypoG8wKOuwsP/g4P/Q0IcwKEswKMl8aJ9fX2xjdOtGRs/Pz+Dg4GImIP8gIH0sKEAwKKmTiKZ8aB/f39Wsl+LFt8dgUE9PT5x5aHBwcP+AgP+WltdgYMyZfyywz78AAAAAAAD///8AAP9mZv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAKgALAAAAAA9AEQAAAj/AFEJHEiwoMGDCBMqXMiwocAbBww4nEhxoYkUpzJGrMixogkfGUNqlNixJEIDB0SqHGmyJSojM1bKZOmyop0gM3Oe2liTISKMOoPy7GnwY9CjIYcSRYm0aVKSLmE6nfq05QycVLPuhDrxBlCtYJUqNAq2bNWEBj6ZXRuyxZyDRtqwnXvkhACDV+euTeJm1Ki7A73qNWtFiF+/gA95Gly2CJLDhwEHMOUAAuOpLYDEgBxZ4GRTlC1fDnpkM+fOqD6DDj1aZpITp0dtGCDhr+fVuCu3zlg49ijaokTZTo27uG7Gjn2P+hI8+PDPERoUB318bWbfAJ5sUNFcuGRTYUqV/3ogfXp1rWlMc6awJjiAAd2fm4ogXjz56aypOoIde4OE5u/F9x199dlXnnGiHZWEYbGpsAEA3QXYnHwEFliKAgswgJ8LPeiUXGwedCAKABACCN+EA1pYIIYaFlcDhytd51sGAJbo3onOpajiihlO92KHGaUXGwWjUBChjSPiWJuOO/LYIm4v1tXfE6J4gCSJEZ7YgRYUNrkji9P55sF/ogxw5ZkSqIDaZBV6aSGYq/lGZplndkckZ98xoICbTcIJGQAZcNmdmUc210hs35nCyJ58fgmIKX5RQGOZowxaZwYA+JaoKQwswGijBV4C6SiTUmpphMspJx9unX4KaimjDv9aaXOEBteBqmuuxgEHoLX6Kqx+yXqqBANsgCtit4FWQAEkrNbpq7HSOmtwag5w57GrmlJBASEU18ADjUYb3ADTinIttsgSB1oJFfA63bduimuqKB1keqwUhoCSK374wbujvOSu4QG6UvxBRydcpKsav++Ca6G8A6Pr1x2kVMyHwsVxUALDq/krnrhPSOzXG1lUTIoffqGR7Goi2MAxbv6O2kEG56I7CSlRsEFKFVyovDJoIRTg7sugNRDGqCJzJgcKE0ywc0ELm6KBCCJo8DIPFeCWNGcyqNFE06ToAfV0HBRgxsvLThHn1oddQMrXj5DyAQgjEHSAJMWZwS3HPxT/QMbabI/iBCliMLEJKX2EEkomBAUCxRi42VDADxyTYDVogV+wSChqmKxEKCDAYFDFj4OmwbY7bDGdBhtrnTQYOigeChUmc1K3QTnAUfEgGFgAWt88hKA6aCRIXhxnQ1yg3BCayK44EWdkUQcBByEQChFXfCB776aQsG0BIlQgQgE8qO26X1h8cEUep8ngRBnOy74E9QgRgEAC8SvOfQkh7FDBDmS43PmGoIiKUUEGkMEC/PJHgxw0xH74yx/3XnaYRJgMB8obxQW6kL9QYEJ0FIFgByfIL7/IQAlvQwEpnAC7DtLNJCKUoO/w45c44GwCXiAFB/OXAATQryUxdN4LfFiwgjCNYg+kYMIEFkCKDs6PKAIJouyGWMS1FSKJOMRB/BoIxYJIUXFUxNwoIkEKPAgCBZSQHQ1A2EWDfDEUVLyADj5AChSIQW6gu10bE/JG2VnCZGfo4R4d0sdQoBAHhPjhIB94v/wRoRKQWGRHgrhGSQJxCS+0pCZbEhAAOw=="}
21 | ).then(function (reply, rate, err) {
22 | t.deepEqual(
23 | reply,
24 | {
25 | rate: {
26 | limit: null, remaining: null, reset: null
27 | },
28 | reply: {
29 | errors: [
30 | { code: 89, message: "Invalid or expired token." }
31 | ],
32 | httpstatus: 200
33 | }
34 | }
35 | )
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/test/oauth_tests.js:
--------------------------------------------------------------------------------
1 | require("events").EventEmitter.defaultMaxListeners = 50;
2 |
3 | const tape = require("tape"),
4 | _test = require("tape-promise"),
5 | test = _test(tape), // decorate tape
6 | CodebirdM = require("./codebirdm");
7 |
8 | function getCB() {
9 | var cb = new CodebirdM.default();
10 | cb.setConsumerKey("123", "456");
11 |
12 | return cb;
13 | }
14 |
15 | test("Tests oauth_authenticate Promise", function(t) {
16 | const cb = getCB();
17 | t.plan(1);
18 |
19 | cb.setToken("123", "456");
20 | return cb.oauth_authenticate().then(function(a) {
21 | t.deepEqual(a, {
22 | reply: "https://api.twitter.com/oauth/authenticate?oauth_token=123"
23 | });
24 | });
25 | });
26 |
27 | test("Tests oauth_authenticate callback", function(t) {
28 | const cb = getCB();
29 | t.plan(4);
30 |
31 | cb.setToken("123", "456");
32 | cb.oauth_authenticate({}, function(a) {
33 | t.equal(a, "https://api.twitter.com/oauth/authenticate?oauth_token=123");
34 | });
35 | cb.oauth_authenticate({ force_login: true }, function(a) {
36 | t.equal(
37 | a,
38 | "https://api.twitter.com/oauth/authenticate?oauth_token=123&force_login=1"
39 | );
40 | });
41 | cb.oauth_authenticate(
42 | { force_login: true, screen_name: "TwitterAPI" },
43 | function(a) {
44 | t.equal(
45 | a,
46 | "https://api.twitter.com/oauth/authenticate?oauth_token=123&force_login=1&screen_name=TwitterAPI"
47 | );
48 | }
49 | );
50 | cb.oauth_authenticate({ screen_name: "TwitterAPI" }, function(a) {
51 | t.equal(
52 | a,
53 | "https://api.twitter.com/oauth/authenticate?oauth_token=123&screen_name=TwitterAPI"
54 | );
55 | });
56 | });
57 |
58 | test("Tests oauth_authorize callback", function(t) {
59 | const cb = getCB();
60 | t.plan(4);
61 |
62 | cb.setToken("123", "456");
63 | cb.oauth_authorize({}, function(a) {
64 | t.equal(a, "https://api.twitter.com/oauth/authorize?oauth_token=123");
65 | });
66 | cb.oauth_authorize({ force_login: true }, function(a) {
67 | t.equal(
68 | a,
69 | "https://api.twitter.com/oauth/authorize?oauth_token=123&force_login=1"
70 | );
71 | });
72 | cb.oauth_authorize({ force_login: true, screen_name: "TwitterAPI" }, function(
73 | a
74 | ) {
75 | t.equal(
76 | a,
77 | "https://api.twitter.com/oauth/authorize?oauth_token=123&force_login=1&screen_name=TwitterAPI"
78 | );
79 | });
80 | cb.oauth_authorize({ screen_name: "TwitterAPI" }, function(a) {
81 | t.equal(
82 | a,
83 | "https://api.twitter.com/oauth/authorize?oauth_token=123&screen_name=TwitterAPI"
84 | );
85 | });
86 | });
87 |
88 | test("Tests oauth2_token", function(t) {
89 | const cb = getCB();
90 | t.plan(1);
91 |
92 | cb.oauth2_token(function(a) {
93 | t.deepEqual(a, {
94 | token_type: "bearer",
95 | access_token: "VqiO0n2HrKE",
96 | httpstatus: 200
97 | });
98 | });
99 | });
100 |
101 | test("Tests signing of boolean parameters", function(t) {
102 | const cb = getCB();
103 |
104 | t.equal(
105 | cb.call("_getSignature", ["GET", "friends/ids", ["stringify_ids"], [true]]),
106 | "OFNuMTEnE82pfI0cAdJPgtO4xzY="
107 | );
108 |
109 | t.end();
110 | });
111 |
--------------------------------------------------------------------------------