├── .gitignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── AUTHORS ├── History.md ├── LICENSE.txt ├── Makefile ├── README.md ├── api.md ├── examples ├── browser │ ├── bundle.js │ ├── entry.js │ └── index.html └── connect_with_oauth_middleware.js ├── index.js ├── lib ├── base64.js ├── emotional.js ├── github.js ├── instapaper.js ├── oauth.js ├── oauth_middleware.js ├── sha1.js ├── tapi.js ├── tbase.js ├── tbase_oauth_v2.js ├── tqq.js ├── tsina.js ├── tsohu.js ├── twitter.js ├── urllib.js ├── utils.js ├── weibo.js └── weibo_util.js ├── logo.png ├── package.json └── test ├── base64.js ├── browser ├── bundle.js ├── entry.js ├── jquery-1.6.min.js └── weibo_browser_test.html ├── config.js ├── mk2.jpg ├── mocha.opts ├── oauth.js ├── proxy.js ├── snake.jpg ├── tapi.js ├── tqq_text_process.js ├── tsina_emotions.json ├── tsina_public_timeline.json ├── utils.js ├── utils └── check.js └── weibo_text_process.js /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | .settings 3 | node_modules/ 4 | lib-cov 5 | coverage.html 6 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "phantom", 4 | "module", 5 | "require", 6 | "__dirname", 7 | "process", 8 | "console", 9 | "it", 10 | "describe", 11 | "before", 12 | "after", 13 | "jQuery", 14 | "window", 15 | "weibo" 16 | ], 17 | 18 | "node" : true, 19 | "es5": true, 20 | "bitwise": true, 21 | "curly": true, 22 | "eqeqeq": true, 23 | "forin": false, 24 | "immed": true, 25 | "latedef": true, 26 | "newcap": true, 27 | "noarg": true, 28 | "noempty": true, 29 | "nonew": true, 30 | "plusplus": false, 31 | "undef": true, 32 | "strict": false, 33 | "trailing": false, 34 | "globalstrict": true, 35 | "nonstandard": true, 36 | "white": false, 37 | "indent": 2, 38 | "expr": true, 39 | "multistr": true, 40 | "onevar": false 41 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git* 2 | logo.png 3 | lib-cov/ 4 | examples/ 5 | test/ 6 | support/ 7 | .settings/ 8 | .project 9 | mkdoc.sh 10 | Makefile 11 | gtap 12 | node_modules 13 | .travis.yml 14 | coverage.html 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | before_install: 5 | - 'npm install --registry=http://registry.cnpmjs.org --cache=${HOME}/.npm/.cache/cnpm' 6 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Total 4 contributors. 2 | # Ordered by date of first contribution. 3 | 4 | fengmk2 (https://github.com/fengmk2) 5 | QLeelulu (https://github.com/QLeelulu) 6 | hpf1908 (https://github.com/hpf1908) 7 | xydudu (https://github.com/xydudu) 8 | iwillwen (https://github.com/iwillwen) 9 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 0.6.11 / 2014-02-08 3 | ================== 4 | 5 | * support authorize scope (@iwillwen) 6 | 7 | 0.6.10 / 2013-12-13 8 | ================== 9 | 10 | * add user agent #38 (@chemzqm) 11 | * fixed #31 README.md 中的 entry.js 无法正常运行 12 | 13 | 0.6.9 / 2013-05-18 14 | ================== 15 | 16 | * fix redirect_url bug: #32 (@hbbalfred) 17 | 18 | 0.6.8 / 2013-02-04 19 | ================== 20 | 21 | * Merge pull request #28 from im007boy/master 22 | * Update lib/tapi.js 23 | 24 | 0.6.7 / 2012-11-17 25 | ================== 26 | 27 | * fixed #26 support direct_message_create and destroy 28 | * npm ignore logo.png 29 | * add logo 30 | 31 | 0.6.6 / 2012-10-08 32 | ================== 33 | 34 | * let tqq support comments_to_me. 35 | * let TQQAPI.prototype.comments_to_me equal comments_timeline on tqq. 36 | * only test with tqq now. 37 | * fixed ep 0.1.3 not export EventProxy problem. 38 | * let tqq support user_search and at user suggestions. 39 | 40 | 0.6.5 / 2012-10-07 41 | ================== 42 | 43 | * add search_suggestions_at_users() 44 | * remove _blank for alink 45 | 46 | 0.6.4 / 2012-10-05 47 | ================== 48 | 49 | * fixed direct_messages_both duplite bug. 50 | * add direct_messages() apis 51 | * add tqq friendship_show() 52 | * follow and unfollow 53 | * add friend ship 54 | * add emotions and @user text process. 55 | * fixed since_id include in timeline api; fixed count() dont not suppport many ids problem. 56 | * fixed tqq timeline pagging problem. 57 | * upload support progress callback. 58 | 59 | 0.6.3 / 2012-10-01 60 | ================== 61 | 62 | * add favorites apis: favorites(), favorite_create(), favorite_destroy(), favorite_show(). 63 | * add jscoverage result to readme.md. 64 | * add count() for status. 65 | * add text process helpers. 66 | 67 | 0.6.2 / 2012-09-29 68 | ================== 69 | 70 | * remove mime dependency. 71 | * add browser env demo on examples/browser. 72 | * add browser test on test/browser/weibo_browser_test.html. 73 | 74 | 0.6.1 / 2012-09-29 75 | ================== 76 | 77 | * Refactor core code. 78 | * All TAPI inherits from `TBase`. 79 | * `WeiboAPI` support weibo api 2.0 now. 80 | * only support node >= 0.8.0. 81 | * all support apis test pass. 82 | * use browserify to support browser env. 83 | 84 | 0.5.1 / 2012-09-26 85 | ================== 86 | 87 | * let oauth middleware support login callback and logout callback. 88 | 89 | 0.5.0 / 2012-07-22 90 | ================== 91 | 92 | * support github oauth. 93 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | This software is licensed under the MIT License. 2 | 3 | Copyright (C) 2011-2012 by fengmk2 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TESTS = test/*.js 2 | REPORTER = spec 3 | TIMEOUT = 10000 4 | MOCHA_OPTS = 5 | G = 6 | JSCOVERAGE = ./node_modules/jscover/bin/jscover 7 | 8 | install: 9 | @npm install --registry=http://registry.cnpmjs.org --cache=${HOME}/.npm/.cache/cnpm 10 | 11 | test: install 12 | @NODE_ENV=test ./node_modules/mocha/bin/mocha \ 13 | --reporter $(REPORTER) \ 14 | --timeout $(TIMEOUT) $(MOCHA_OPTS) \ 15 | $(TESTS) 16 | 17 | test-g: 18 | @NODE_ENV=test ./node_modules/mocha/bin/mocha \ 19 | --reporter $(REPORTER) \ 20 | --timeout $(TIMEOUT) -g "$(G)" \ 21 | $(TESTS) 22 | 23 | test-cov: lib-cov 24 | @WEIBO_COV=1 $(MAKE) test REPORTER=dot 25 | @WEIBO_COV=1 $(MAKE) test REPORTER=html-cov > coverage.html 26 | 27 | lib-cov: install 28 | @rm -rf $@ 29 | @$(JSCOVERAGE) lib $@ 30 | 31 | build: 32 | ./node_modules/browserify/bin/cmd.js examples/browser/entry.js -o examples/browser/bundle.js 33 | ./node_modules/browserify/bin/cmd.js test/browser/entry.js -o test/browser/bundle.js 34 | 35 | publish: build 36 | npm publish 37 | 38 | contributors: install 39 | @./node_modules/.bin/contributors -f plain -o AUTHORS 40 | 41 | .PHONY: test 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-weibo [![Build Status](https://secure.travis-ci.org/fengmk2/node-weibo.png)](http://travis-ci.org/fengmk2/node-weibo) 2 | 3 | [![NPM](https://nodei.co/npm/weibo.png?downloads=true&stars=true)](https://nodei.co/npm/weibo/) 4 | 5 | ![logo](https://raw.github.com/fengmk2/node-weibo/master/logo.png) 6 | 7 | A [weibo](http://weibo.com)(like [twitter](http://twitter.com)) API SDK, use on browser client and nodejs server. 8 | 9 | Please see the [API Documents](https://github.com/fengmk2/node-weibo/blob/master/api.md) first. 10 | 11 | ## Supports APIs 12 | 13 | * weibo: [http://t.sina.com.cn/](http://weibo.com/) 14 | * tqq: [http://t.qq.com/](http://t.qq.com/) 15 | * github: [http://github.com](http://github.com), only `oauth` for now. 16 | * twitter(unavailable): [http://twitter.com/](http://twitter.com/) 17 | * facebook(unavailable): [http://facebook.com/](http://facebook.com/) 18 | * fanfou(unavailable): [http://fanfou.com/](http://fanfou.com/) 19 | * digu(unavailable): [http://digu.com/](http://digu.com/) 20 | * tsohu(unavailable): [http://t.sohu.com/](http://t.sohu.com/) 21 | * t163(unavailable): [http://t.163.com/](http://t.163.com/) 22 | * plurk(unavailable): [http://plurk.com/](http://plurk.com/) 23 | 24 | ## Nodejs Install 25 | 26 | ```bash 27 | $ npm install weibo 28 | ``` 29 | 30 | ## How to use 31 | 32 | `entry.js` 33 | 34 | ```js 35 | var weibo = require('weibo'); 36 | 37 | // change appkey to yours 38 | var appkey = 'your appkey'; 39 | var secret = 'your app secret'; 40 | var oauth_callback_url = 'your callback url'; 41 | weibo.init('weibo', appkey, secret, oauth_callback_url); 42 | 43 | var user = { blogtype: 'weibo' }; 44 | var cursor = {count: 20}; 45 | weibo.public_timeline(user, cursor, function (err, statuses) { 46 | if (err) { 47 | console.error(err); 48 | } else { 49 | console.log(statuses); 50 | } 51 | }); 52 | ``` 53 | 54 | Demo on nodejs and browser just the same code. 55 | 56 | Thanks for [browserify](https://github.com/substack/node-browserify), 57 | let us to use the same code on nodejs and browser. 58 | 59 | ### Browser: `Phonegap`, `Chrome extension` or [node-webkit](https://github.com/rogerwang/node-webkit). 60 | 61 | NOTICE: browser must enable **cross-domain** request. 62 | 63 | browserify to `bundle.js` 64 | 65 | ```bash 66 | $ browserify entry.js -o bundle.js 67 | ``` 68 | 69 | Include `bundle.js` to your html. 70 | 71 | ```html 72 | 73 | 74 | Weibo Hello world 75 | 76 | 77 | 78 | Hello world. 79 | 80 | 81 | ``` 82 | 83 | ### Use `weibo.oauth` middleware 84 | 85 | handler oauth login middleware, use on connect, express. 86 | 87 | ```js 88 | /** 89 | * oauth middleware for connect 90 | * 91 | * example: 92 | * 93 | * connect( 94 | * connect.query(), 95 | * connect.cookieParser('I\'m cookie secret.'), 96 | * connect.session({ secret: "oh year a secret" }), 97 | * weibo.oauth() 98 | * ); 99 | * 100 | * @param {Object} [options] 101 | * - {String} [homeUrl], use to create login success oauth_callback url with referer header, 102 | * default is `'http://' + req.headers.host`; 103 | * - {String} [loginPath], login url, default is '/oauth' 104 | * - {String} [logoutPath], default is '/oauth/logout' 105 | * - {String} [callbackPath], default is login_path + '/callback' 106 | * - {String} [blogtypeField], default is 'type', 107 | * if you want to connect weibo, login url should be '/oauth?type=weibo' 108 | * - {Function(req, res, callback)} [afterLogin], when oauth login success, will call this function. 109 | * - {Function(req, res, callback)} [beforeLogout], will call this function before user logout. 110 | */ 111 | ``` 112 | 113 | Example: A simple web with oauth login. 114 | 115 | ```js 116 | var connect = require('connect'); 117 | var weibo = require('../'); 118 | 119 | /** 120 | * init weibo api settings 121 | */ 122 | 123 | weibo.init('weibo', '$appkey', '$secret'); 124 | weibo.init('tqq', '$appkey', '$secret'); 125 | weibo.init('github', '$ClientID', '$ClientSecret'); 126 | 127 | /** 128 | * Create a web application. 129 | */ 130 | 131 | var app = connect( 132 | connect.query(), 133 | connect.cookieParser('oh year a cookie secret'), 134 | connect.session({ secret: "oh year a secret" }), 135 | // using weibo.oauth middleware for use login 136 | // will auto save user in req.session.oauthUser 137 | weibo.oauth({ 138 | loginPath: '/login', 139 | logoutPath: '/logout', 140 | blogtypeField: 'type', 141 | afterLogin: function (req, res, callback) { 142 | console.log(req.session.oauthUser.screen_name, 'login success'); 143 | process.nextTick(callback); 144 | }, 145 | beforeLogout: function (req, res, callback) { 146 | console.log(req.session.oauthUser.screen_name, 'loging out'); 147 | process.nextTick(callback); 148 | } 149 | }), 150 | connect.errorHandler({ stack: true, dump: true }) 151 | ); 152 | 153 | app.use('/', function (req, res, next) { 154 | var user = req.session.oauthUser; 155 | res.writeHeader(200, { 'Content-Type': 'text/html' }); 156 | if (!user) { 157 | res.end('Login with Weibo | \ 158 | QQ | \ 159 | Github'); 160 | return; 161 | } 162 | res.end('Hello, \ 163 | @' + user.screen_name + '. ' + 165 | 'Logout'); 166 | }); 167 | 168 | app.listen(8088); 169 | console.log('Server start on http://localhost:8088/'); 170 | ``` 171 | 172 | ## Test 173 | 174 | ```bash 175 | $ npm install 176 | $ npm test 177 | ``` 178 | 179 | jscoverage: [79%](http://fengmk2.github.com/coverage/node-weibo.html) 180 | 181 | ## Authors 182 | 183 | Below is the output from `git-summary`. 184 | 185 | ```bash 186 | $ git summary 187 | 188 | project : node-weibo 189 | repo age : 3 years 190 | active : 73 days 191 | commits : 173 192 | files : 53 193 | authors : 194 | 156 fengmk2 90.2% 195 | 7 hpf1908 4.0% 196 | 3 chemzqm 1.7% 197 | 2 QLeelulu 1.2% 198 | 1 hbbalfred 0.6% 199 | 1 im007boy 0.6% 200 | 1 iwillwen 0.6% 201 | 1 mk2 0.6% 202 | 1 xydudu 0.6% 203 | ``` 204 | 205 | ## License 206 | 207 | (The MIT License) 208 | 209 | Copyright (c) 2011-2014 fengmk2 <fengmk2@gmail.com> 210 | 211 | Permission is hereby granted, free of charge, to any person obtaining 212 | a copy of this software and associated documentation files (the 213 | 'Software'), to deal in the Software without restriction, including 214 | without limitation the rights to use, copy, modify, merge, publish, 215 | distribute, sublicense, and/or sell copies of the Software, and to 216 | permit persons to whom the Software is furnished to do so, subject to 217 | the following conditions: 218 | 219 | The above copyright notice and this permission notice shall be 220 | included in all copies or substantial portions of the Software. 221 | 222 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 223 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 224 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 225 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 226 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 227 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 228 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 229 | -------------------------------------------------------------------------------- /examples/browser/entry.js: -------------------------------------------------------------------------------- 1 | var weibo = require('../../'); 2 | 3 | // change appkey to yours 4 | var appkey = 'your appkey'; 5 | var secret = 'your app secret'; 6 | var oauth_callback_url = 'your callback url'; 7 | weibo.init('weibo', appkey, secret, oauth_callback_url); 8 | 9 | var user = { blogtype: 'weibo' }; 10 | var cursor = {count: 20, source: appkey}; 11 | weibo.public_timeline(user, cursor, function (err, statuses) { 12 | if (err) { 13 | console.error(err); 14 | } else { 15 | console.log(statuses); 16 | } 17 | }); -------------------------------------------------------------------------------- /examples/browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Weibo Hello world 4 | 5 | 6 | 7 | Hello world. 8 | 9 | -------------------------------------------------------------------------------- /examples/connect_with_oauth_middleware.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * node-weibo - demo for using oauth_middleware in connect 3 | * Copyright(c) 2012 fengmk2 4 | * MIT Licensed 5 | */ 6 | 7 | /** 8 | * Module dependencies. 9 | */ 10 | 11 | var connect = require('connect'); 12 | var weibo = require('../'); 13 | 14 | /** 15 | * init weibo api settings 16 | */ 17 | 18 | weibo.init('weibo', '1122960051', 'e678e06f627ffe0e60e2ba48abe3a1e3'); 19 | weibo.init('github', '8e14edfda73a71f1f226', '1796ac639a8ada0dff6acfee2d63390440ca0f3b'); 20 | weibo.init('tqq', '801196838', '9f1a88caa8709de7dccbe3cae4bdc962'); 21 | 22 | /** 23 | * Create a web application. 24 | */ 25 | 26 | var app = connect( 27 | connect.query(), 28 | connect.cookieParser('oh year a cookie secret'), 29 | connect.session({ secret: "oh year a secret" }), 30 | // using weibo.oauth middleware for use login 31 | // will auto save user in req.session.oauthUser 32 | weibo.oauth({ 33 | loginPath: '/login', 34 | logoutPath: '/logout', 35 | callbackPath: '/oauth/callback', 36 | blogtypeField: 'type', 37 | afterLogin: function (req, res, callback) { 38 | console.log(req.session.oauthUser && req.session.oauthUser.screen_name, 'login success'); 39 | process.nextTick(callback); 40 | }, 41 | beforeLogout: function (req, res, callback) { 42 | console.log(req.session.oauthUser && req.session.oauthUser.screen_name, 'loging out'); 43 | process.nextTick(callback); 44 | } 45 | }) 46 | ); 47 | 48 | app.use('/', function (req, res, next) { 49 | var user = req.session.oauthUser; 50 | res.writeHeader(200, { 'Content-Type': 'text/html' }); 51 | if (!user) { 52 | res.end('Login with Weibo | \ 53 | QQ | \ 54 | Github'); 55 | return; 56 | } 57 | res.end('Hello, \ 58 | @' + user.screen_name + '. ' + 60 | 'Logout
' + JSON.stringify(user, null, '  ') + '
'); 61 | }); 62 | 63 | app.listen(8088); 64 | console.log('Server start on http://localhost.nodeweibo.com:8088/'); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * node-weibo - index.js 3 | * Copyright(c) 2012 fengmk2 4 | * MIT Licensed 5 | */ 6 | 7 | /** 8 | * Module dependencies. 9 | */ 10 | 11 | var weibo = require('./lib/tapi'); 12 | weibo.oauth = require('./lib/oauth_middleware'); 13 | 14 | module.exports = weibo; -------------------------------------------------------------------------------- /lib/base64.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Base64 encode / decode 4 | * http://www.webtoolkit.info/ 5 | * 6 | **/ 7 | 8 | // support atob and btoa native method in browser 9 | 10 | (function () { 11 | 12 | var Base64 = { 13 | 14 | // private property 15 | _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", 16 | 17 | // public method for encoding 18 | encode: function (input) { 19 | var output = ""; 20 | var chr1, chr2, chr3, enc1, enc2, enc3, enc4; 21 | var i = 0; 22 | 23 | input = Base64._utf8_encode(input); 24 | 25 | while (i < input.length) { 26 | 27 | chr1 = input.charCodeAt(i++); 28 | chr2 = input.charCodeAt(i++); 29 | chr3 = input.charCodeAt(i++); 30 | 31 | enc1 = chr1 >> 2; 32 | enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); 33 | enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); 34 | enc4 = chr3 & 63; 35 | 36 | if (isNaN(chr2)) { 37 | enc3 = enc4 = 64; 38 | } else if (isNaN(chr3)) { 39 | enc4 = 64; 40 | } 41 | 42 | output = output + 43 | this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) + 44 | this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4); 45 | 46 | } 47 | 48 | return output; 49 | }, 50 | 51 | // public method for decoding 52 | decode: function (input) { 53 | var output = ""; 54 | var chr1, chr2, chr3; 55 | var enc1, enc2, enc3, enc4; 56 | var i = 0; 57 | 58 | input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); 59 | 60 | while (i < input.length) { 61 | 62 | enc1 = this._keyStr.indexOf(input.charAt(i++)); 63 | enc2 = this._keyStr.indexOf(input.charAt(i++)); 64 | enc3 = this._keyStr.indexOf(input.charAt(i++)); 65 | enc4 = this._keyStr.indexOf(input.charAt(i++)); 66 | 67 | chr1 = (enc1 << 2) | (enc2 >> 4); 68 | chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); 69 | chr3 = ((enc3 & 3) << 6) | enc4; 70 | 71 | output = output + String.fromCharCode(chr1); 72 | 73 | if (enc3 != 64) { 74 | output = output + String.fromCharCode(chr2); 75 | } 76 | if (enc4 != 64) { 77 | output = output + String.fromCharCode(chr3); 78 | } 79 | 80 | } 81 | 82 | output = Base64._utf8_decode(output); 83 | 84 | return output; 85 | 86 | }, 87 | 88 | // private method for UTF-8 encoding 89 | _utf8_encode : function (string) { 90 | string = string.replace(/\r\n/g,"\n"); 91 | var utftext = ""; 92 | 93 | for (var n = 0; n < string.length; n++) { 94 | 95 | var c = string.charCodeAt(n); 96 | 97 | if (c < 128) { 98 | utftext += String.fromCharCode(c); 99 | } 100 | else if ((c > 127) && (c < 2048)) { 101 | utftext += String.fromCharCode((c >> 6) | 192); 102 | utftext += String.fromCharCode((c & 63) | 128); 103 | } 104 | else { 105 | utftext += String.fromCharCode((c >> 12) | 224); 106 | utftext += String.fromCharCode(((c >> 6) & 63) | 128); 107 | utftext += String.fromCharCode((c & 63) | 128); 108 | } 109 | 110 | } 111 | 112 | return utftext; 113 | }, 114 | 115 | // private method for UTF-8 decoding 116 | _utf8_decode : function (utftext) { 117 | var string = ""; 118 | var i = 0; 119 | var c = 0, c1 = 0, c2 = 0; 120 | 121 | while ( i < utftext.length ) { 122 | 123 | c = utftext.charCodeAt(i); 124 | 125 | if (c < 128) { 126 | string += String.fromCharCode(c); 127 | i++; 128 | } 129 | else if((c > 191) && (c < 224)) { 130 | c2 = utftext.charCodeAt(i+1); 131 | string += String.fromCharCode(((c & 31) << 6) | (c2 & 63)); 132 | i += 2; 133 | } 134 | else { 135 | c2 = utftext.charCodeAt(i+1); 136 | c3 = utftext.charCodeAt(i+2); 137 | string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63)); 138 | i += 3; 139 | } 140 | } 141 | return string; 142 | }, 143 | /** 144 | * A str encode and decode use on phpwind 145 | * 146 | * e.g.: var my_key = 'awejfosjdlxldfjlsdfwerwljxoasldf!@##@' 147 | * // encode 148 | * var encode_str = Base64.strcode('fawave发威', my_key); 149 | * // decode 150 | * var source_str = Base64.strcode(encode_str, my_key, true) 151 | * 152 | * @param {String} str 153 | * @param {String} key 154 | * @param {Boolen} decode, default is `false` 155 | * @return {String} encode or decode string 156 | * @api public 157 | */ 158 | strcode: function (str, key, decode) { 159 | var keybuffer = this.utf8_encode(key); 160 | var key_length = keybuffer.length; 161 | var buffer = null, encoding = 'base64'; 162 | if(decode) { 163 | buffer = this.decode(str); 164 | } else { 165 | buffer = this.utf8_encode(str); 166 | } 167 | var buf = ''; 168 | for (var i = 0, len = buffer.length; i < len; i++) { 169 | var k = i % key_length; 170 | buf += String.fromCharCode(buffer.charCodeAt(i) ^ keybuffer.charCodeAt(k)); 171 | } 172 | if (decode) { 173 | return this.utf8_decode(buffer); 174 | } else { 175 | return this.encode(buffer); 176 | } 177 | } 178 | }; 179 | 180 | Base64.utf8_encode = Base64._utf8_encode; 181 | Base64.utf8_decode = Base64._utf8_decode; 182 | 183 | var root = this; // window on browser 184 | if (typeof module === 'undefined') { 185 | root.weibo = root.weibo || {}; 186 | root.weibo.base64 = Base64; 187 | } else { 188 | module.exports = Base64; 189 | } 190 | 191 | })(); -------------------------------------------------------------------------------- /lib/github.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * node-weibo - lib/github.js 3 | * Copyright(c) 2012 fengmk2 4 | * MIT Licensed 5 | */ 6 | 7 | "use strict"; 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var TBaseOauthV2 = require('./tbase_oauth_v2'); 14 | var inherits = require('util').inherits; 15 | var TSinaAPI = require('./tsina'); 16 | var utils = require('./utils'); 17 | var querystring = require('querystring'); 18 | 19 | 20 | function GithubAPI(options) { 21 | GithubAPI.super_.call(this); 22 | var config = utils.extend({}, options, { 23 | host: 'https://api.github.com', 24 | result_format: '', 25 | oauth_key: '', 26 | oauth_secret: '', 27 | oauth_host: 'https://github.com', 28 | oauth_authorize: '/login/oauth/authorize', 29 | oauth_access_token: '/login/oauth/access_token', 30 | 31 | verify_credentials: '/user', 32 | user_show: '/users/{{uid}}', 33 | 34 | support_search: false, 35 | support_user_search: false, 36 | support_search_suggestions_at_users: false, 37 | support_favorites: false, 38 | support_favorite_show: false, 39 | support_favorite_create: false, 40 | support_favorite_destroy: false, 41 | support_direct_messages_both: false, 42 | support_direct_messages: false, 43 | support_direct_messages_sent: false, 44 | support_direct_message_create: false, 45 | support_direct_message_destroy: false, 46 | 47 | }); 48 | this.init(config); 49 | } 50 | 51 | inherits(GithubAPI, TBaseOauthV2); 52 | module.exports = GithubAPI; 53 | 54 | /** 55 | * Utils methods 56 | */ 57 | 58 | GithubAPI.prototype.url_encode = function (text) { 59 | return text; 60 | }; 61 | 62 | /** 63 | * OAuth 64 | */ 65 | 66 | GithubAPI.prototype.convert_token = function (user) { 67 | var data = GithubAPI.super_.prototype.convert_token.call(this, user); 68 | data.state = Date.now(); 69 | return data; 70 | }; 71 | 72 | /** 73 | * Result formatters 74 | */ 75 | 76 | GithubAPI.prototype.format_access_token = function (token) { 77 | token = querystring.parse(token); 78 | return token; 79 | }; 80 | 81 | /** 82 | * 83 | { 84 | public_repos: 67, 85 | following: 84, 86 | created_at: '2009-11-21T08:07:35Z', 87 | type: 'User', 88 | email: 'fengmk2@gmail.com', 89 | bio: 'nodejs', 90 | blog: 'http://fengmk2.github.com', 91 | location: 'Hangzhou, China', 92 | gravatar_id: '95b9d41231617a05ced5604d242c9670', 93 | avatar_url: 'https://secure.gravatar.com/avatar/95b9d41231617a05ced5604d242c9670?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-user-420.png', 94 | public_gists: 21, 95 | followers: 293, 96 | login: 'fengmk2', 97 | name: 'fengmk2', 98 | company: 'http://www.taobao.com/', 99 | id: 156269, 100 | html_url: 'https://github.com/fengmk2', 101 | hireable: false, 102 | url: 'https://api.github.com/users/fengmk2' 103 | } 104 | */ 105 | GithubAPI.prototype.format_user = function (data) { 106 | var user = { 107 | id: data.login, 108 | t_url: data.html_url, 109 | screen_name: data.name, 110 | name: data.login, 111 | location: data.location, 112 | url: data.blog || data.url, 113 | profile_image_url: data.avatar_url + '&s=50', 114 | avatar_large: data.avatar_url + '&s=180', 115 | gender: 'n', 116 | following: false, 117 | verified: false, 118 | follow_me: false, 119 | followers_count: data.followers, 120 | friends_count: data.following, 121 | statuses_count: data.public_repos, 122 | favourites_count: 0, 123 | created_at: new Date(data.created_at), 124 | email: data.email, 125 | }; 126 | return user; 127 | }; 128 | 129 | 130 | -------------------------------------------------------------------------------- /lib/instapaper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * http://www.instapaper.com/api/simple 3 | * 4 | * @type Object 5 | */ 6 | 7 | (function(exports){ 8 | 9 | var urllib = require('./urllib'); 10 | 11 | Object.extend(exports, { 12 | 13 | request: function(user, url, data, callback, context){ 14 | var args = { 15 | data: data, 16 | type: 'post', 17 | headers: { 18 | Authorization: urllib.make_base_auth_header(user.username, user.password), 19 | 'Content-Type': 'application/x-www-form-urlencoded' 20 | } 21 | }; 22 | urllib.request(url, args, function(text, error, response){ 23 | var success = (text == '201' || text == '200'); 24 | callback.call(context, error, success, response); 25 | }, this); 26 | }, 27 | 28 | // ajax_request: function(user, url, data, callback, context){ 29 | // var headers = {Authorization: urllib.make_base_auth_header(user.username, user.password)}; 30 | // $.ajax({ 31 | // url: url, 32 | // data: data, 33 | // timeout: 60000, 34 | // type: 'post', 35 | // beforeSend: function(req) { 36 | // for(var k in headers) { 37 | // req.setRequestHeader(k, headers[k]); 38 | // } 39 | // }, 40 | // success: function(data, text_status, xhr){ 41 | // callback.call(context, text_status == 'success', text_status, xhr); 42 | // }, 43 | // error: function(xhr, text_status, err){ 44 | // callback.call(context, false, text_status, xhr); 45 | // } 46 | // }); 47 | // }, 48 | // 49 | authenticate: function(user, callback, context) { 50 | var api = 'https://www.instapaper.com/api/authenticate'; 51 | this.request(user, api, {}, callback, context); 52 | }, 53 | 54 | // url, title, selection 55 | add: function(user, data, callback, context){ 56 | var api = 'https://www.instapaper.com/api/add'; 57 | this.request(user, api, data, callback, context); 58 | } 59 | }); 60 | 61 | })( (function(){ 62 | if(typeof exports === 'undefined') { 63 | window.instapaper = {}; 64 | return window.instapaper; 65 | } else { 66 | return exports; 67 | } 68 | })() ); -------------------------------------------------------------------------------- /lib/oauth.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2008 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // Here's some JavaScript software that's useful for implementing OAuth. 17 | // The HMAC-SHA1 signature method calls b64_hmac_sha1, defined by 18 | // http://pajhome.org.uk/crypt/md5/sha1.js 19 | /* An OAuth message is represented as an object like this: 20 | {method: "GET", action: "http://server.com/path", parameters: ...} 21 | The parameters may be either a map {name: value, name2: value2} 22 | or an Array of name-value pairs [[name, value], [name2, value2]]. 23 | The latter representation is more powerful: it supports parameters 24 | in a specific sequence, or several parameters with the same name; 25 | for example [["a", 1], ["b", 2], ["a", 3]]. 26 | Parameter names and values are NOT percent-encoded in an object. 27 | They must be encoded before transmission and decoded after reception. 28 | For example, this message object: 29 | {method: "GET", action: "http://server/path", parameters: {p: "x y"}} 30 | ... can be transmitted as an HTTP request that begins: 31 | GET /path?p=x%20y HTTP/1.0 32 | (This isn't a valid OAuth request, since it lacks a signature etc.) 33 | Note that the object "x y" is transmitted as x%20y. To encode 34 | parameters, you can call OAuth.addToURL, OAuth.formEncode or 35 | OAuth.getAuthorization. 36 | This message object model harmonizes with the browser object model for 37 | input elements of an form, whose value property isn't percent encoded. 38 | The browser encodes each value before transmitting it. For example, 39 | see consumer.setInputs in example/consumer.js. 40 | */ 41 | 42 | (function () { 43 | 44 | var utils; 45 | if (typeof require !== 'undefined') { 46 | utils = require('./utils'); 47 | } else { 48 | utils = weibo.utils; 49 | } 50 | 51 | var OAuth = {}; 52 | 53 | OAuth.setProperties = function setProperties(into, from) { 54 | if (into && from) { 55 | for (var key in from) { 56 | into[key] = from[key]; 57 | } 58 | } 59 | return into; 60 | }; 61 | 62 | // utility functions 63 | OAuth.setProperties(OAuth, { 64 | percentEncode: function percentEncode(s) { 65 | if (!s) { 66 | return ""; 67 | } 68 | if (s instanceof Array) { 69 | var e = ""; 70 | for (var i = 0; i < s.length; ++s) { 71 | if (e) { 72 | e += '&'; 73 | } 74 | e += percentEncode(s[i]); 75 | } 76 | return e; 77 | } 78 | s = encodeURIComponent(s); 79 | // Now replace the values which encodeURIComponent doesn't do 80 | // encodeURIComponent ignores: - _ . ! ~ * ' ( ) 81 | // OAuth dictates the only ones you can ignore are: - _ . ~ 82 | // Source: http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Functions:encodeURIComponent 83 | s = s.replace(/\!/g, "%21"); 84 | s = s.replace(/\*/g, "%2A"); 85 | s = s.replace(/\'/g, "%27"); 86 | // s = s.replace("(", "%28", "g"); 87 | s = s.replace(/\(/g, "%28"); 88 | s = s.replace(/\)/g, "%29"); 89 | return s; 90 | }, 91 | decodePercent: decodeURIComponent, 92 | /** Convert the given parameters to an Array of name-value pairs. */ 93 | getParameterList: function getParameterList(parameters) { 94 | if (!parameters) { 95 | return []; 96 | } 97 | if (typeof parameters !== "object") { 98 | return this.decodeForm(parameters + ""); 99 | } 100 | if (parameters instanceof Array) { 101 | return parameters; 102 | } 103 | var list = []; 104 | for (var p in parameters) { 105 | list.push([ p, parameters[p] ]); 106 | } 107 | return list; 108 | }, 109 | /** Convert the given parameters to a map from name to value. */ 110 | getParameterMap: function getParameterMap(parameters) { 111 | if (!parameters) { 112 | return {}; 113 | } 114 | if (typeof parameters !== "object") { 115 | return this.getParameterMap(this.decodeForm(parameters + "")); 116 | } 117 | if (parameters instanceof Array) { 118 | var map = {}; 119 | for (var p = 0; p < parameters.length; ++p) { 120 | var key = parameters[p][0]; 121 | if (map[key] === undefined) { // first value wins 122 | map[key] = parameters[p][1]; 123 | } 124 | } 125 | return map; 126 | } 127 | return parameters; 128 | }, 129 | formEncode: function formEncode(parameters) { 130 | var form = ""; 131 | var list = OAuth.getParameterList(parameters); 132 | for (var p = 0, l = list.length; p < l; p++) { 133 | var pair = list[p]; 134 | var value = pair[1]; 135 | if (!value) { 136 | value = ""; 137 | } 138 | if (form) { 139 | form += '&'; 140 | } 141 | form += OAuth.percentEncode(pair[0]) + '=' + OAuth.percentEncode(value); 142 | } 143 | return form; 144 | }, 145 | decodeForm: function decodeForm(form) { 146 | var list = []; 147 | var nvps = form.split('&'); 148 | for (var n = 0; n < nvps.length; ++n) { 149 | var nvp = nvps[n]; 150 | if (!nvp) { 151 | continue; 152 | } 153 | var equals = nvp.indexOf('='); 154 | var name; 155 | var value; 156 | if (equals < 0) { 157 | name = OAuth.decodePercent(nvp); 158 | value = null; 159 | } else { 160 | name = OAuth.decodePercent(nvp.substring(0, equals)); 161 | value = OAuth.decodePercent(nvp.substring(equals + 1)); 162 | } 163 | list.push([name, value]); 164 | } 165 | return list; 166 | }, 167 | setParameter: function setParameter(message, name, value) { 168 | var parameters = message.parameters; 169 | if (parameters instanceof Array) { 170 | for (var p = 0; p < parameters.length; ++p) { 171 | if (parameters[p][0] === name) { 172 | if (value === undefined) { 173 | parameters.splice(p, 1); 174 | } else { 175 | parameters[p][1] = value; 176 | value = undefined; 177 | } 178 | } 179 | } 180 | if (value !== undefined) { 181 | parameters.push([name, value]); 182 | } 183 | } else { 184 | parameters = OAuth.getParameterMap(parameters); 185 | parameters[name] = value; 186 | message.parameters = parameters; 187 | } 188 | }, 189 | setParameters: function setParameters(message, parameters) { 190 | var list = OAuth.getParameterList(parameters); 191 | for (var i = 0; i < list.length; ++i) { 192 | OAuth.setParameter(message, list[i][0], list[i][1]); 193 | } 194 | }, 195 | setTimestampAndNonce: function setTimestampAndNonce(message) { 196 | OAuth.setParameter(message, "oauth_timestamp", OAuth.timestamp()); 197 | OAuth.setParameter(message, "oauth_nonce", OAuth.nonce(32)); 198 | }, 199 | addToURL: function addToURL(url, parameters) { 200 | if (parameters) { 201 | var toAdd = OAuth.formEncode(parameters); 202 | if (toAdd) { 203 | if (url.indexOf('?') < 0) { 204 | url += '?'; 205 | } else { 206 | url += '&'; 207 | } 208 | url += toAdd; 209 | } 210 | } 211 | return url; 212 | }, 213 | /** Construct the value of the Authorization header for an HTTP request. */ 214 | getAuthorizationHeader: function getAuthorizationHeader(realm, parameters) { 215 | var header = ''; 216 | if (realm) { 217 | header += ', realm="' + OAuth.percentEncode(realm) + '"'; 218 | } 219 | var list = OAuth.getParameterList(parameters); 220 | for (var p = 0; p < list.length; ++p) { 221 | var parameter = list[p]; 222 | var name = parameter[0]; 223 | if (name.indexOf("oauth_") === 0) { 224 | header += ', ' + OAuth.percentEncode(name) + '="' + OAuth.percentEncode(parameter[1]) + '"'; 225 | } 226 | } 227 | return 'OAuth ' + header.substring(2); 228 | }, 229 | 230 | timestamp: function timestamp() { 231 | return Math.floor(new Date().getTime() / 1000); 232 | }, 233 | 234 | nonce: function nonce(length) { 235 | if (!length) { 236 | return ''; 237 | } 238 | var chars = OAuth.nonce.CHARS; 239 | var result = ""; 240 | for (var i = 0; i < length; ++i) { 241 | var rnum = Math.floor(Math.random() * chars.length); 242 | result += chars.substring(rnum, rnum + 1); 243 | } 244 | return result; 245 | } 246 | }); 247 | 248 | OAuth.nonce.CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz"; 249 | /** Define a constructor function, 250 | without causing trouble to anyone who was using it as a namespace. 251 | That is, if parent[name] already existed and had properties, 252 | copy those properties into the new constructor. 253 | */ 254 | OAuth.declareClass = function declareClass(parent, name, newConstructor) { 255 | var previous = parent[name]; 256 | parent[name] = newConstructor; 257 | if (newConstructor && previous) { 258 | for (var key in previous) { 259 | if (key !== "prototype") { 260 | newConstructor[key] = previous[key]; 261 | } 262 | } 263 | } 264 | return newConstructor; 265 | }; 266 | 267 | /** An abstract algorithm for signing messages. */ 268 | OAuth.declareClass(OAuth, "SignatureMethod", function OAuthSignatureMethod() {}); 269 | 270 | // instance members 271 | OAuth.setProperties(OAuth.SignatureMethod.prototype, { 272 | /** Add a signature to the message. */ 273 | sign: function sign(message) { 274 | var baseString = OAuth.SignatureMethod.getBaseString(message); 275 | var signature = this.getSignature(baseString); 276 | // console.log(baseString, this.key, signature) 277 | OAuth.setParameter(message, "oauth_signature", signature); 278 | return signature; // just in case someone's interested 279 | }, 280 | /** Set the key string for signing. */ 281 | initialize: function initialize(name, accessor) { 282 | var consumerSecret; 283 | if (accessor.accessorSecret && name.length > 9 && name.substring(name.length-9) === "-Accessor") { 284 | consumerSecret = accessor.accessorSecret; 285 | } else { 286 | consumerSecret = accessor.consumerSecret; 287 | } 288 | this.key = OAuth.percentEncode(consumerSecret) + "&" + OAuth.percentEncode(accessor.tokenSecret); 289 | } 290 | }); 291 | 292 | /* SignatureMethod expects an accessor object to be like this: 293 | {tokenSecret: "lakjsdflkj...", consumerSecret: "QOUEWRI..", accessorSecret: "xcmvzc..."} 294 | The accessorSecret property is optional. 295 | */ 296 | // Class members: 297 | OAuth.setProperties(OAuth.SignatureMethod, { 298 | sign: function sign(message, accessor) { 299 | var name = OAuth.getParameterMap(message.parameters).oauth_signature_method; 300 | if (!name) { 301 | name = 'HMAC-SHA1'; 302 | OAuth.setParameter(message, 'oauth_signature_method', name); 303 | } 304 | OAuth.SignatureMethod.newMethod(name, accessor).sign(message); 305 | }, 306 | 307 | /** Instantiate a SignatureMethod for the given method name. */ 308 | newMethod: function newMethod(name, accessor) { 309 | var Impl = OAuth.SignatureMethod.REGISTERED[name]; 310 | if (typeof Impl === 'function') { 311 | var method = new Impl(); 312 | method.initialize(name, accessor); 313 | return method; 314 | } 315 | var err = new Error("signature_method_rejected"); 316 | var acceptable = ""; 317 | for (var r in OAuth.SignatureMethod.REGISTERED) { 318 | if (acceptable) { 319 | acceptable += '&'; 320 | } 321 | acceptable += OAuth.percentEncode(r); 322 | } 323 | err.oauth_acceptable_signature_methods = acceptable; 324 | throw err; 325 | }, 326 | /** A map from signature method name to constructor. */ 327 | REGISTERED: {}, 328 | /** Subsequently, the given constructor will be used for the named methods. 329 | The constructor will be called with no parameters. 330 | The resulting object should usually implement getSignature(baseString). 331 | You can easily define such a constructor by calling makeSubclass, below. 332 | */ 333 | registerMethodClass: function registerMethodClass(names, classConstructor) { 334 | for (var n = 0, l = names.length; n < l; ++n) { 335 | OAuth.SignatureMethod.REGISTERED[names[n]] = classConstructor; 336 | } 337 | }, 338 | /** Create a subclass of OAuth.SignatureMethod, with the given getSignature function. */ 339 | makeSubclass: function makeSubclass(getSignatureFunction) { 340 | var SuperClass = OAuth.SignatureMethod; 341 | var subClass = function() { 342 | SuperClass.call(this); 343 | }; 344 | subClass.prototype = new SuperClass(); 345 | // Delete instance variables from prototype: 346 | // delete subclass.prototype... There aren't any. 347 | subClass.prototype.getSignature = getSignatureFunction; 348 | subClass.prototype.constructor = subClass; 349 | return subClass; 350 | }, 351 | getBaseString: function getBaseString(message) { 352 | var URL = message.action; 353 | var q = URL.indexOf('?'); 354 | var parameters; 355 | if (q < 0) { 356 | parameters = message.parameters; 357 | } else { 358 | // Combine the URL query string with the other parameters: 359 | parameters = OAuth.decodeForm(URL.substring(q + 1)); 360 | var toAdd = OAuth.getParameterList(message.parameters); 361 | for (var a = 0, l = toAdd.length; a < l; ++a) { 362 | parameters.push(toAdd[a]); 363 | } 364 | } 365 | return OAuth.percentEncode(message.method.toUpperCase()) + '&' + 366 | OAuth.percentEncode(OAuth.SignatureMethod.normalizeUrl(URL)) + '&' + 367 | OAuth.percentEncode(OAuth.SignatureMethod.normalizeParameters(parameters)); 368 | }, 369 | normalizeUrl: function normalizeUrl(url) { 370 | var uri = OAuth.SignatureMethod.parseUri(url); 371 | var scheme = uri.protocol.toLowerCase(); 372 | var authority = uri.authority.toLowerCase(); 373 | var dropPort = (scheme === "http" && uri.port === 80) || (scheme === "https" && uri.port === 443); 374 | if (dropPort) { 375 | // find the last : in the authority 376 | var index = authority.lastIndexOf(":"); 377 | if (index >= 0) { 378 | authority = authority.substring(0, index); 379 | } 380 | } 381 | var path = uri.path; 382 | // if (!path) { 383 | // path = "/"; // conforms to RFC 2616 section 3.2.2 384 | // } 385 | // we know that there is no query and no fragment here. 386 | return scheme + "://" + authority + path; 387 | }, 388 | parseUri: function parseUri(str) { 389 | /* This function was adapted from parseUri 1.2.1 390 | http://stevenlevithan.com/demo/parseuri/js/assets/parseuri.js 391 | */ 392 | var o = { 393 | key: [ "source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], 394 | parser: { strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/ }}; 395 | var m = o.parser.strict.exec(str); 396 | var uri = {}; 397 | var i = 14; 398 | while (i--) { 399 | uri[o.key[i]] = m[i] || ""; 400 | } 401 | return uri; 402 | }, 403 | normalizeParameters: function normalizeParameters(parameters) { 404 | if (!parameters) { 405 | return ""; 406 | } 407 | var norm = []; 408 | var list = OAuth.getParameterList(parameters); 409 | for (var p = 0; p < list.length; ++p) { 410 | var nvp = list[p]; 411 | if (nvp[0] !== "oauth_signature") { 412 | norm.push(nvp); 413 | } 414 | } 415 | norm.sort(function (a, b) { 416 | if (a[0] < b[0]) { return -1; } 417 | if (a[0] > b[0]) { return 1; } 418 | if (a[1] < b[1]) { return -1; } 419 | if (a[1] > b[1]) { return 1; } 420 | return 0; 421 | }); 422 | return OAuth.formEncode(norm); 423 | } 424 | }); 425 | 426 | OAuth.SignatureMethod.registerMethodClass(["PLAINTEXT", "PLAINTEXT-Accessor"], 427 | OAuth.SignatureMethod.makeSubclass( 428 | function getSignature(baseString) { 429 | return this.key; 430 | } 431 | )); 432 | 433 | OAuth.SignatureMethod.registerMethodClass(["HMAC-SHA1", "HMAC-SHA1-Accessor"], 434 | OAuth.SignatureMethod.makeSubclass( 435 | function getSignature(baseString) { 436 | return utils.base64HmacSha1(baseString, this.key); 437 | } 438 | )); 439 | 440 | var root = this; // window on browser 441 | if (typeof module === 'undefined') { 442 | root.weibo = root.weibo || {}; 443 | root.weibo.OAuth = OAuth; 444 | } else { 445 | module.exports = OAuth; 446 | } 447 | 448 | })(); 449 | -------------------------------------------------------------------------------- /lib/oauth_middleware.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * node-weibo - oauth_middleware for connect 3 | * Copyright(c) 2012 fengmk2 4 | * MIT Licensed 5 | */ 6 | 7 | /** 8 | * Module dependencies. 9 | */ 10 | 11 | var tapi = require('./tapi'); 12 | 13 | function getReferer(req, options) { 14 | var referer = req.headers.referer || '/'; 15 | if (referer.indexOf(options.loginPath) === 0 || referer.indexOf(options.logoutPath) === 0) { 16 | referer = '/'; 17 | } 18 | return referer; 19 | } 20 | 21 | function redirect(res, url) { 22 | res.writeHead(302, { 23 | Location: url 24 | }); 25 | res.end(); 26 | } 27 | 28 | function getAuthCallback(options) { 29 | return options.homeUrl + options.callbackPath; 30 | } 31 | 32 | function login(req, res, next, options) { 33 | var blogtypeField = options.blogtypeField; 34 | var blogtype = req.query[blogtypeField]; 35 | var referer = getReferer(req, options); 36 | 37 | options.homeUrl = options._customeHomeUrl || 'http://' + req.headers.host; 38 | var authCallback = getAuthCallback(options); 39 | var user = { 40 | blogtype: blogtype, 41 | oauth_callback: authCallback, 42 | scope: options.scope 43 | }; 44 | tapi.get_authorization_url(user, function (err, authInfo) { 45 | if (err) { 46 | return next(err); 47 | } 48 | authInfo.blogtype = blogtype; 49 | authInfo.referer = referer; 50 | req.session.oauthInfo = authInfo; 51 | redirect(res, authInfo.auth_url); 52 | }); 53 | } 54 | 55 | function logout(req, res, next, options) { 56 | options.beforeLogout(req, res, function (err) { 57 | if (err) { 58 | return next(err); 59 | } 60 | var referer = getReferer(req, options); 61 | req.session.oauthUser = null; 62 | redirect(res, referer); 63 | }); 64 | } 65 | 66 | function oauthCallback(req, res, next, options) { 67 | var oauthInfo = req.session.oauthInfo || {}; 68 | var blogtype = req.query[options.blogtypeField] || oauthInfo.blogtype; 69 | req.session.oauthInfo = null; 70 | var token = req.query; 71 | token.blogtype = blogtype; 72 | token.oauth_callback = getAuthCallback(options); 73 | if (oauthInfo.oauth_token_secret) { 74 | token.oauth_token_secret = oauthInfo.oauth_token_secret; 75 | } 76 | var referer = oauthInfo.referer; 77 | tapi.get_access_token(token, function (err, accessToken) { 78 | if (err) { 79 | return next(err); 80 | } 81 | // get user info 82 | tapi.verify_credentials(accessToken, function (err, user) { 83 | if (err) { 84 | return next(err); 85 | } 86 | for (var k in accessToken) { 87 | user[k] = accessToken[k]; 88 | } 89 | req.session.oauthUser = user; 90 | options.afterLogin(req, res, function (err) { 91 | if (err) { 92 | return next(err); 93 | } 94 | redirect(res, referer); 95 | }); 96 | }); 97 | }); 98 | } 99 | 100 | function defaultCallback(req, res, callback) { 101 | callback(); 102 | } 103 | 104 | /** 105 | * oauth middleware for connect 106 | * 107 | * example: 108 | * 109 | * connect( 110 | * connect.query(), 111 | * connect.cookieParser('I\'m cookie secret.'), 112 | * connect.session({ secret: "oh year a secret" }), 113 | * weibo.oauth() 114 | * ); 115 | * 116 | * @param {Object} [options] 117 | * - {String} [homeUrl], use to create login success oauth_callback url with referer header, 118 | * default is `'http://' + req.headers.host`; 119 | * - {String} [loginPath], login url, default is '/oauth' 120 | * - {String} [logoutPath], default is '/oauth/logout' 121 | * - {String} [callbackPath], default is login_path + '/callback' 122 | * - {String} [blogtypeField], default is 'type', 123 | * if you want to connect weibo, login url should be '/oauth?type=weibo' 124 | * - {Function(req, res, callback)} [afterLogin], when oauth login success, will call this function. 125 | * - {Function(req, res, callback)} [beforeLogout], will call this function before user logout. 126 | */ 127 | 128 | module.exports = function oauth(options) { 129 | options = options || {}; 130 | if (options.homeUrl) { 131 | options.homeUrl = options.homeUrl.replace(/\/+$/, ''); 132 | options._customeHomeUrl = options.homeUrl; 133 | } 134 | options.loginPath = options.loginPath || '/oauth'; 135 | options.logoutPath = options.logoutPath || '/oauth/logout'; 136 | options.callbackPath = options.callbackPath || (options.loginPath + '/callback'); 137 | options.blogtypeField = options.blogtypeField || 'type'; 138 | options.afterLogin = options.afterLogin || defaultCallback; 139 | options.beforeLogout = options.beforeLogout || defaultCallback; 140 | options.scope = options.scope || false; 141 | return function (req, res, next) { 142 | if (req.url.indexOf(options.callbackPath) === 0) { 143 | oauthCallback(req, res, next, options); 144 | } else if (req.url.indexOf(options.loginPath) === 0) { 145 | login(req, res, next, options); 146 | } else if (req.url.indexOf(options.logoutPath) === 0) { 147 | logout(req, res, next, options); 148 | } else { 149 | next(); 150 | } 151 | }; 152 | }; 153 | -------------------------------------------------------------------------------- /lib/sha1.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined 3 | * in FIPS PUB 180-1 4 | * Version 2.1a Copyright Paul Johnston 2000 - 2002. 5 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet 6 | * Distributed under the BSD License 7 | * See http://pajhome.org.uk/crypt/md5 for details. 8 | */ 9 | 10 | (function () { 11 | 12 | var root = this; // window on browser 13 | var exports; 14 | var crypto; 15 | if (typeof module === 'undefined') { 16 | root.weibo = root.weibo || {}; 17 | exports = root.weibo.sha1 = {}; 18 | } else { 19 | exports = module.exports; 20 | } 21 | 22 | /* 23 | * Configurable variables. You may need to tweak these to be compatible with 24 | * the server-side, but the defaults work in most cases. 25 | */ 26 | var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ 27 | var b64pad = "="; /* base-64 pad character. "=" for strict RFC compliance */ 28 | var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ 29 | 30 | /* 31 | * These are the functions you'll usually want to call 32 | * They take string arguments and return either hex or base-64 encoded strings 33 | */ 34 | function hex_sha1(s) { 35 | return binb2hex(core_sha1(str2binb(s), s.length * chrsz)); 36 | } 37 | exports.hex_sha1 = hex_sha1; 38 | 39 | function b64_sha1(s) { 40 | return binb2b64(core_sha1(str2binb(s), s.length * chrsz)); 41 | } 42 | exports.b64_sha1 = b64_sha1; 43 | 44 | function str_sha1(s) { 45 | return binb2str(core_sha1(str2binb(s), s.length * chrsz)); 46 | } 47 | exports.str_sha1 = str_sha1; 48 | 49 | function hex_hmac_sha1(key, data) { 50 | return binb2hex(core_hmac_sha1(key, data)); 51 | } 52 | exports.hex_hmac_sha1 = hex_hmac_sha1; 53 | 54 | function b64_hmac_sha1(key, data) { 55 | return binb2b64(core_hmac_sha1(key, data)); 56 | } 57 | exports.b64_hmac_sha1 = b64_hmac_sha1; 58 | 59 | function str_hmac_sha1(key, data) { 60 | return binb2str(core_hmac_sha1(key, data)); 61 | } 62 | exports.str_hmac_sha1 = str_hmac_sha1; 63 | 64 | /* 65 | * Perform a simple self-test to see if the VM is working 66 | */ 67 | function sha1_vm_test() { 68 | return hex_sha1("abc") === "a9993e364706816aba3e25717850c26c9cd0d89d"; 69 | } 70 | 71 | /* 72 | * Calculate the SHA-1 of an array of big-endian words, and a bit length 73 | */ 74 | function core_sha1(x, len) { 75 | /* append padding */ 76 | x[len >> 5] |= 0x80 << (24 - len % 32); 77 | x[((len + 64 >> 9) << 4) + 15] = len; 78 | 79 | var w = new Array(80); 80 | var a = 1732584193; 81 | var b = -271733879; 82 | var c = -1732584194; 83 | var d = 271733878; 84 | var e = -1009589776; 85 | 86 | for (var i = 0; i < x.length; i += 16) { 87 | var olda = a; 88 | var oldb = b; 89 | var oldc = c; 90 | var oldd = d; 91 | var olde = e; 92 | 93 | for (var j = 0; j < 80; j++) { 94 | if (j < 16) { 95 | w[j] = x[i + j]; 96 | } 97 | else { 98 | w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1); 99 | } 100 | var t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)), 101 | safe_add(safe_add(e, w[j]), sha1_kt(j))); 102 | e = d; 103 | d = c; 104 | c = rol(b, 30); 105 | b = a; 106 | a = t; 107 | } 108 | 109 | a = safe_add(a, olda); 110 | b = safe_add(b, oldb); 111 | c = safe_add(c, oldc); 112 | d = safe_add(d, oldd); 113 | e = safe_add(e, olde); 114 | } 115 | return [ a, b, c, d, e ]; 116 | 117 | } 118 | 119 | /* 120 | * Perform the appropriate triplet combination function for the current 121 | * iteration 122 | */ 123 | function sha1_ft(t, b, c, d) { 124 | if (t < 20) { 125 | return (b & c) | ((~b) & d); 126 | } 127 | if (t < 40) { 128 | return b ^ c ^ d; 129 | } 130 | if (t < 60) { 131 | return (b & c) | (b & d) | (c & d); 132 | } 133 | return b ^ c ^ d; 134 | } 135 | 136 | /* 137 | * Determine the appropriate additive constant for the current iteration 138 | */ 139 | function sha1_kt(t) { 140 | return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 : 141 | (t < 60) ? -1894007588 : -899497514; 142 | } 143 | 144 | /* 145 | * Calculate the HMAC-SHA1 of a key and some data 146 | */ 147 | function core_hmac_sha1(key, data) { 148 | var bkey = str2binb(key); 149 | if (bkey.length > 16) { 150 | bkey = core_sha1(bkey, key.length * chrsz); 151 | } 152 | 153 | var ipad = new Array(16), opad = new Array(16); 154 | for(var i = 0; i < 16; i++) { 155 | ipad[i] = bkey[i] ^ 0x36363636; 156 | opad[i] = bkey[i] ^ 0x5C5C5C5C; 157 | } 158 | 159 | var hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * chrsz); 160 | return core_sha1(opad.concat(hash), 512 + 160); 161 | } 162 | 163 | /* 164 | * Add integers, wrapping at 2^32. This uses 16-bit operations internally 165 | * to work around bugs in some JS interpreters. 166 | */ 167 | function safe_add(x, y) { 168 | var lsw = (x & 0xFFFF) + (y & 0xFFFF); 169 | var msw = (x >> 16) + (y >> 16) + (lsw >> 16); 170 | return (msw << 16) | (lsw & 0xFFFF); 171 | } 172 | 173 | /* 174 | * Bitwise rotate a 32-bit number to the left. 175 | */ 176 | function rol(num, cnt) { 177 | return (num << cnt) | (num >>> (32 - cnt)); 178 | } 179 | 180 | /* 181 | * Convert an 8-bit or 16-bit string to an array of big-endian words 182 | * In 8-bit function, characters >255 have their hi-byte silently ignored. 183 | */ 184 | function str2binb(str) { 185 | var bin = Array(); 186 | var mask = (1 << chrsz) - 1; 187 | for(var i = 0; i < str.length * chrsz; i += chrsz) 188 | bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (32 - chrsz - i%32); 189 | return bin; 190 | } 191 | exports.str2binb = str2binb; 192 | 193 | /* 194 | * Convert an array of big-endian words to a string 195 | */ 196 | function binb2str(bin) { 197 | var str = ""; 198 | var mask = (1 << chrsz) - 1; 199 | for(var i = 0; i < bin.length * 32; i += chrsz) 200 | str += String.fromCharCode((bin[i>>5] >>> (32 - chrsz - i%32)) & mask); 201 | return str; 202 | } 203 | exports.binb2str = binb2str; 204 | 205 | /* 206 | * Convert an array of big-endian words to a hex string. 207 | */ 208 | function binb2hex(binarray) { 209 | var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; 210 | var str = ""; 211 | for(var i = 0; i < binarray.length * 4; i++) { 212 | str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) + 213 | hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF); 214 | } 215 | return str; 216 | } 217 | exports.binb2hex = binb2hex; 218 | 219 | /* 220 | * Convert an array of big-endian words to a base-64 string 221 | */ 222 | function binb2b64(binarray) { 223 | var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 224 | var str = ""; 225 | for(var i = 0; i < binarray.length * 4; i += 3) { 226 | var triplet = (((binarray[i >> 2] >> 8 * (3 - i %4)) & 0xFF) << 16) 227 | | (((binarray[i+1 >> 2] >> 8 * (3 - (i+1)%4)) & 0xFF) << 8 ) 228 | | ((binarray[i+2 >> 2] >> 8 * (3 - (i+2)%4)) & 0xFF); 229 | for(var j = 0; j < 4; j++) 230 | { 231 | if(i * 8 + j * 6 > binarray.length * 32) str += b64pad; 232 | else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); 233 | } 234 | } 235 | return str; 236 | } 237 | exports.binb2b64 = binb2b64; 238 | 239 | })(); -------------------------------------------------------------------------------- /lib/tapi.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * node-weibo - lib/tapi.js 3 | * Copyright(c) 2012 fengmk2 4 | * MIT Licensed 5 | */ 6 | 7 | "use strict"; 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var EventProxy = require('eventproxy'); 14 | var utils = require('./utils'); 15 | var TSinaAPI = require('./tsina'); 16 | var TQQAPI = require('./tqq'); 17 | var WeiboAPI = require('./weibo'); 18 | var GithubAPI = require('./github'); 19 | 20 | var TAPI = module.exports = { 21 | TYPES: { 22 | weibo: WeiboAPI, // api v2.0 23 | github: GithubAPI, 24 | tsina: TSinaAPI, // api v1.0 25 | // twitter: TwitterAPI, 26 | tqq: TQQAPI, 27 | // tsohu: TSOHUAPI 28 | }, 29 | 30 | enables: {}, 31 | 32 | /** 33 | * Init API options, must init before use it. 34 | * 35 | * @param {String} blogtype, blog api type, e.g.: 'weibo', 'tqq', 'github' and so on. 36 | * @param {String} appkey 37 | * @param {String} secret 38 | * @param {String|Object} [oauth_callback] or [oauth_options] 39 | * - {String} [oauth_callback], oauth callback redirect uri. 40 | * - {String} [oauth_scope], comma separated list of scopes. e.g.: `status, user` 41 | * @return {[type]} [description] 42 | */ 43 | init: function (blogtype, appkey, secret, oauth_options) { 44 | if (!appkey) { 45 | throw new TypeError('appkey must be set'); 46 | } 47 | if (!secret) { 48 | throw new TypeError('secret must be set'); 49 | } 50 | if (typeof oauth_options === 'string') { 51 | oauth_options = { 52 | oauth_callback: oauth_options 53 | }; 54 | } 55 | var TypeAPI = this.TYPES[blogtype]; 56 | if (!TypeAPI) { 57 | throw new TypeError(blogtype + ' api not exists'); 58 | } 59 | var options = { 60 | appkey: appkey, 61 | secret: secret 62 | }; 63 | options = utils.extend(options, oauth_options); 64 | var instance = new TypeAPI(options); 65 | this.enables[blogtype] = instance; 66 | }, 67 | 68 | /** 69 | * Auto detech which API instance to use by user. 70 | * 71 | * @param {User} user 72 | * @return {API} api instance 73 | */ 74 | api_dispatch: function (user) { 75 | var apiType = user.blogtype || user.blogType; 76 | return this.enables[apiType]; 77 | }, 78 | 79 | /** 80 | * Get api instance config by user 81 | * 82 | * @param {User} user 83 | * @return {Object} config 84 | */ 85 | get_config: function (user) { 86 | return this.api_dispatch(user).config; 87 | }, 88 | 89 | /** 90 | * Check api support the method or not. 91 | * 92 | * @param {User} user 93 | * @param {String} method 94 | * @return {Boolean} true or false 95 | */ 96 | support: function (user, method) { 97 | return this.get_config(user)['support_' + method] !== false; 98 | }, 99 | 100 | /** 101 | * Process text to display format. 102 | * 103 | * @param {User} user 104 | * @param {Status} status 105 | * @return {String} 106 | */ 107 | process_text: function (user, status) { 108 | return this.api_dispatch(user).process_text(status); 109 | }, 110 | 111 | /** 112 | * Utils methods 113 | */ 114 | 115 | _timeline: function (method, user, cursor, callback) { 116 | if (typeof cursor === 'function') { 117 | callback = cursor; 118 | cursor = null; 119 | } 120 | cursor = cursor || {}; 121 | cursor.count = cursor.count || 20; 122 | var max_id = cursor.max_id; 123 | var since_id = cursor.since_id; 124 | var self = this; 125 | return self.api_dispatch(user)[method](user, cursor, function (err, result) { 126 | if (err) { 127 | return callback(err); 128 | } 129 | if (!max_id && !since_id) { 130 | return callback(null, result); 131 | } 132 | var testId = String(max_id || since_id); 133 | // ignore the max_id status 134 | var needs = []; 135 | var statuses = result.items || []; 136 | for (var i = 0, l = statuses.length; i < l; i++) { 137 | var status = statuses[i]; 138 | if (status.id === testId) { 139 | continue; 140 | } 141 | needs.push(status); 142 | } 143 | result.items = needs; 144 | callback(null, result); 145 | }); 146 | }, 147 | 148 | /** 149 | * Status 150 | */ 151 | 152 | /** 153 | * Post a status 154 | * 155 | * @param {User} user, oauth user. 156 | * @param {String|Object} status 157 | * - {String} status, content text. 158 | * - {Number} [lat], latitude. 159 | * - {Number} [long], longitude. 160 | * - {String} [annotations], addtional information. 161 | * @param {Function(Error, Status)} callback 162 | * @return {Context} this 163 | */ 164 | update: function (user, status, callback) { 165 | if (typeof status === 'string') { 166 | status = {status: status}; 167 | } 168 | return this.api_dispatch(user).update(user, status, callback); 169 | }, 170 | 171 | /** 172 | * Post a status contain an image. 173 | * 174 | * @param {User} user, oauth user. 175 | * @param {String|Object} status 176 | * - {String} status, content text. 177 | * - {Number} [lat], latitude. 178 | * - {Number} [long], longitude. 179 | * - {String} [annotations], addtional information. 180 | * @param {Object} pic 181 | * - {Buffer|ReadStream} data 182 | * - {String} [name], image file name 183 | * - {String} [content_type], data content type 184 | * - {Function(info)} [progress], upload progress callback. 185 | * - {Object} info: {total: total Size, loaded: upload Size}. 186 | * @param {Function(Error, Status)} callback 187 | * @return {Context} this 188 | */ 189 | upload: function (user, status, pic, callback) { 190 | if (typeof status === 'string') { 191 | status = {status: status}; 192 | } 193 | return this.api_dispatch(user).upload(user, status, pic, callback); 194 | }, 195 | 196 | /** 197 | * Repost a status. 198 | * 199 | * @param {User} user 200 | * @param {String|Number} id, need to repost status id. 201 | * @param {String|Object} status 202 | * - {String} status, content text 203 | * - {Number} [lat], latitude. 204 | * - {Number} [long], longitude. 205 | * - {Boolean} isComment, is comment or not, default is `false`. 206 | * @param {Function(Error, Status)} callback 207 | * @return {Context} this 208 | */ 209 | repost: function (user, id, status, callback) { 210 | if (typeof status === 'string') { 211 | status = {status: status}; 212 | } 213 | id = String(id); 214 | return this.api_dispatch(user).repost(user, id, status, callback); 215 | }, 216 | 217 | /** 218 | * Remove a status by id. 219 | * 220 | * @param {User} user 221 | * @param {String|Number} id 222 | * @param {Function(Error, Status)} callback 223 | * @return {Context} this 224 | */ 225 | destroy: function (user, id, callback) { 226 | id = String(id); 227 | return this.api_dispatch(user).destroy(user, id, callback); 228 | }, 229 | 230 | // upload_pic_url: function (data, pic, callback, context) { 231 | // return this.api_dispatch(data).upload_pic_url(data, pic, callback, context); 232 | // }, 233 | 234 | // // id 瑞推 235 | // retweet: function (data, callback, context) { 236 | // return this.api_dispatch(data).retweet(data, callback, context); 237 | // }, 238 | 239 | /** 240 | * Get a status by id. 241 | * 242 | * @param {User} user 243 | * @param {String|Number} id 244 | * @param {Function(Error, Status)} callback 245 | * @return {Context} this 246 | */ 247 | show: function (user, id, callback) { 248 | return this.api_dispatch(user).show(user, String(id), callback); 249 | }, 250 | 251 | /** 252 | * Get statuses comment count and repost count by ids. 253 | * 254 | * @param {User} user 255 | * @param {String|Array} ids, separate by comma. 256 | * @param {Function(err, counts)} callback 257 | * - {String} id 258 | * - {Number} comments 259 | * - {Number} reposts 260 | * @return {Context} this 261 | */ 262 | count: function (user, ids, callback) { 263 | if (Array.isArray(ids)) { 264 | ids = ids.join(','); 265 | } 266 | return this.api_dispatch(user).count(user, ids, callback); 267 | }, 268 | 269 | /** 270 | * List home timeline statuses. 271 | * 272 | * @param {User} user 273 | * @param {Cursor} [cursor] 274 | * - {String} since_id 275 | * - {String} max_id 276 | * - {String} [since_time], only for tqq, status.timestamp in seconds. 277 | * - {String} [max_time], only for tqq, status.timestamp in seconds. 278 | * - {Number} count, default is `20` 279 | * - {Number} page 280 | * @param {Function(err, result)} callback 281 | * {Object} result: 282 | * - {Array} items, [Status, ...] 283 | * - {Cursor} cursor 284 | * - ... 285 | * @return {Context} this 286 | */ 287 | home_timeline: function (user, cursor, callback) { 288 | return this._timeline('home_timeline', user, cursor, callback); 289 | }, 290 | 291 | /** 292 | * List home timeline statuses. 293 | * 294 | * @param {User} user 295 | * @param {Cursor} [cursor] 296 | * - {String} since_id 297 | * - {String} max_id 298 | * - {String} [since_time], only for tqq 299 | * - {String} [max_time], only for tqq 300 | * - {Number} count, default is `20` 301 | * - {Number} page 302 | * @param {Function(err, result)} callback 303 | * {Object} result: 304 | * - {Array} items, [Status, ...] 305 | * - {Cursor} cursor 306 | * - ... 307 | * @return {Context} this 308 | */ 309 | public_timeline: function (user, cursor, callback) { 310 | return this._timeline('public_timeline', user, cursor, callback); 311 | }, 312 | 313 | /** 314 | * List user personal timeline statuses. 315 | * 316 | * @param {User} user 317 | * @param {Cursor} [cursor] 318 | * - {String} [uid], user id 319 | * - {String} [screen_name], `user.screen_name`, screen_name or uid must be set at least one. 320 | * - {String} [since_id] 321 | * - {String} [max_id] 322 | * - {String} [since_time], only for tqq 323 | * - {String} [max_time], only for tqq 324 | * - {Number} count, default is `20` 325 | * - {Number} page 326 | * @param {Function(err, result)} callback 327 | * {Object} result: 328 | * - {Array} items, [Status, ...] 329 | * - {Cursor} cursor 330 | * - ... 331 | * @return {Context} this 332 | */ 333 | user_timeline: function (user, cursor, callback) { 334 | return this._timeline('user_timeline', user, cursor, callback); 335 | }, 336 | 337 | /** 338 | * List @me statuses. 339 | * 340 | * @param {User} user 341 | * @param {Cursor} [cursor] 342 | * - {String} since_id 343 | * - {String} max_id 344 | * - {String} [since_time], only for tqq 345 | * - {String} [max_time], only for tqq 346 | * - {Number} count, default is `20` 347 | * - {Number} page 348 | * @param {Function(err, result)} callback 349 | * {Object} result: 350 | * - {Array} items, [Status, ...] 351 | * - {Cursor} cursor 352 | * - ... 353 | * @return {Context} this 354 | */ 355 | mentions: function (user, cursor, callback) { 356 | return this._timeline('mentions', user, cursor, callback); 357 | }, 358 | 359 | /** 360 | * List one status's reposted statuses 361 | * 362 | * @param {User} user 363 | * @param {String} id, status's id 364 | * @param {Cursor} [cursor] 365 | * - {String} since_id 366 | * - {String} max_id 367 | * - {String} [since_time], only for tqq 368 | * - {String} [max_time], only for tqq 369 | * - {Number} count, default is `20` 370 | * - {Number} page 371 | * - {Number} [filter_by_author], only support by `weibo`; 372 | * Filter statuses by author type, 0: all, 1: only I following、2: stranger, default is `0`. 373 | * @param {Function(err, result)} callback 374 | * {Object} result: 375 | * - {Array} items, [Status, ...] 376 | * - {Cursor} cursor 377 | * - ... 378 | * @return {Context} this 379 | */ 380 | repost_timeline: function (user, id, cursor, callback) { 381 | if (typeof cursor === 'function') { 382 | callback = cursor; 383 | cursor = null; 384 | } 385 | cursor = cursor || {}; 386 | cursor.id = id; 387 | return this._timeline('repost_timeline', user, cursor, callback); 388 | }, 389 | 390 | /** 391 | * Favorite 392 | */ 393 | 394 | /** 395 | * List favorites. 396 | * 397 | * @param {User} user 398 | * @param {Cursor} [cursor] 399 | * - {String} since_id 400 | * - {String} max_id 401 | * - {String} [since_time], only for tqq 402 | * - {String} [max_time], only for tqq 403 | * - {Number} count, default is `20` 404 | * - {Number} page 405 | * @param {Function(err, result)} callback 406 | * {Object} result: 407 | * - {Array} items, [Favorite, ...] 408 | * - {Cursor} cursor 409 | * - ... 410 | * @return {Context} this 411 | */ 412 | favorites: function (user, cursor, callback) { 413 | return this._timeline('favorites', user, cursor, callback); 414 | }, 415 | 416 | /** 417 | * Show a favorite item by item id. 418 | * 419 | * @param {User} user 420 | * @param {String} id, favorite item's id. 421 | * @param {Function(err, favorite)} callback 422 | * @return {Context} this 423 | */ 424 | favorite_show: function (user, id, callback) { 425 | return this.api_dispatch(user).favorite_show(user, id, callback); 426 | }, 427 | 428 | /** 429 | * Add a status to favorites. 430 | * 431 | * @param {User} user 432 | * @param {String} id, status's id. 433 | * @param {Function(err, result)} callback 434 | * - {Object} result 435 | * - {String} id, relation item's id. 436 | * - addtional infomation maybe. 437 | * @return {Context} this 438 | */ 439 | favorite_create: function (user, id, callback) { 440 | return this.api_dispatch(user).favorite_create(user, id, callback); 441 | }, 442 | 443 | /** 444 | * Remove the status from favorites. 445 | * 446 | * @param {User} user 447 | * @param {String} id, the favorite item's id. 448 | * @param {Function(err, result)} callback 449 | * - {Object} result 450 | * - {String} id, relation item's id. 451 | * - addtional infomation maybe. 452 | * @return {Context} this 453 | */ 454 | favorite_destroy: function (user, id, callback) { 455 | return this.api_dispatch(user).favorite_destroy(user, id, callback); 456 | }, 457 | 458 | /** 459 | * Comment 460 | */ 461 | 462 | /** 463 | * List comments to my statues 464 | * 465 | * @param {User} user 466 | * @param {Cursor} [cursor] 467 | * - {String} since_id 468 | * - {String} max_id 469 | * - {String} [since_time], only for tqq 470 | * - {String} [max_time], only for tqq 471 | * - {Number} count, default is `20` 472 | * - {Number} page 473 | * @param {Function(err, result)} callback 474 | * {Object} result: 475 | * - {Array} items, [Comment, ...] 476 | * - {Cursor} cursor 477 | * - ... 478 | * @return {Context} this 479 | */ 480 | comments_timeline: function (user, cursor, callback) { 481 | return this._timeline('comments_timeline', user, cursor, callback); 482 | }, 483 | 484 | /** 485 | * List @me comments 486 | * 487 | * @param {User} user 488 | * @param {Cursor} [cursor] 489 | * - {String} since_id 490 | * - {String} max_id 491 | * - {Number} count, default is `20` 492 | * - {Number} page 493 | * @param {Function(err, result)} callback 494 | * {Object} result: 495 | * - {Array} items, [Comment, ...] 496 | * - {Cursor} cursor 497 | * - ... 498 | * @return {Context} this 499 | */ 500 | comments_mentions: function (user, cursor, callback) { 501 | return this._timeline('comments_mentions', user, cursor, callback); 502 | }, 503 | 504 | /** 505 | * List comments post by me 506 | * 507 | * @param {User} user 508 | * @param {Cursor} [cursor] 509 | * - {String} since_id 510 | * - {String} max_id 511 | * - {Number} count, default is `20` 512 | * - {Number} page 513 | * - {Number} [filter_by_source], only support by `weibo`; 514 | * Filter comments by source type, 0: all, 1: come from weibo, 2: come from weiqun, default is `0`. 515 | * @param {Function(err, result)} callback 516 | * {Object} result: 517 | * - {Array} items, [Comment, ...] 518 | * - {Cursor} cursor 519 | * - ... 520 | * @return {Context} this 521 | */ 522 | comments_by_me: function (user, cursor, callback) { 523 | return this._timeline('comments_by_me', user, cursor, callback); 524 | }, 525 | 526 | /** 527 | * List comments to me 528 | * 529 | * @param {User} user 530 | * @param {Cursor} [cursor] 531 | * - {String} [since_id] 532 | * - {String} [max_id] 533 | * - {Number} [count], default is `20` 534 | * - {Number} [page] 535 | * - {Number} [filter_by_author], only support by `weibo`; 536 | * Filter comments by author type, 0: all, 1: I following, 2: stranger, default is `0`. 537 | * - {Number} [filter_by_source], only support by `weibo`; 538 | * Filter comments by source type, 0: all, 1: come from weibo, 2: come from weiqun, default is `0`. 539 | * @param {Function(err, result)} callback 540 | * {Object} result: 541 | * - {Array} items, [Comment, ...] 542 | * - {Cursor} cursor 543 | * - ... 544 | * @return {Context} this 545 | */ 546 | comments_to_me: function (user, cursor, callback) { 547 | return this._timeline('comments_to_me', user, cursor, callback); 548 | }, 549 | 550 | /** 551 | * List one status's comments 552 | * 553 | * @param {User} user 554 | * @param {String} id, status's id 555 | * @param {Cursor} [cursor] 556 | * - {String} since_id 557 | * - {String} max_id 558 | * - {String} [since_time], only for tqq 559 | * - {String} [max_time], only for tqq 560 | * - {Number} count, default is `20` 561 | * - {Number} page 562 | * - {Number} [filter_by_author], only support by `weibo`; 563 | * Filter comments by author type, 0: all, 1: only I following、2: stranger, default is `0`. 564 | * @param {Function(err, result)} callback 565 | * {Object} result: 566 | * - {Array} items, [Comment, ...] 567 | * - {Cursor} cursor 568 | * - ... 569 | * @return {Context} this 570 | */ 571 | comments: function (user, id, cursor, callback) { 572 | if (typeof cursor === 'function') { 573 | callback = cursor; 574 | cursor = null; 575 | } 576 | cursor = cursor || {}; 577 | cursor.id = id; 578 | return this._timeline('comments', user, cursor, callback); 579 | }, 580 | 581 | /** 582 | * post a comment to a status 583 | * 584 | * @param {AccessToken} user 585 | * @param {String} id, status's id 586 | * @param {String|Object} comment 587 | * - {String} comment 588 | * - {Number} [comment_ori], same comment to the original status when comment on a repost status, 589 | * 0: no, 1: yes, default is `0`. 590 | * @param {Function(err, result)} callback 591 | * - {Object} result 592 | * - {String} id, the comment id 593 | * @return {Context} this 594 | */ 595 | comment_create: function (user, id, comment, callback) { 596 | if (typeof comment === 'string') { 597 | comment = {comment: comment}; 598 | } 599 | return this.api_dispatch(user).comment_create(user, id, comment, callback); 600 | }, 601 | 602 | /** 603 | * reply to a comment 604 | * @param {AccessToken} user 605 | * @param {String} cid, comment's id 606 | * @param {String} id, status's id 607 | * @param {String|Object} comment 608 | * - {String} comment 609 | * - {Number} [without_mention], auto add `'reply@username'` to comment text or not, 610 | * 0: yes, 1: no, default is `1`, won't auto add. 611 | * - {Number} [comment_ori], same comment to the original status when comment on a repost status, 612 | * 0: no, 1: yes, default is `0`. 613 | * @param {Function(err, result)} callback 614 | * @return {Context} this 615 | */ 616 | comment_reply: function (user, cid, id, comment, callback) { 617 | if (typeof comment === 'string') { 618 | comment = {comment: comment}; 619 | } 620 | return this.api_dispatch(user).comment_reply(user, cid, id, comment, callback); 621 | }, 622 | 623 | /** 624 | * remove a comment 625 | * @param {AccessToken} user 626 | * @param {String} cid, comment's id 627 | * @param {Function(err, result)} callback 628 | * @return {Context} this 629 | */ 630 | comment_destroy: function (user, cid, callback) { 631 | return this.api_dispatch(user).comment_destroy(user, cid, callback); 632 | }, 633 | 634 | /** 635 | * OAuth 636 | */ 637 | 638 | /** 639 | * Get authorization token and login url. 640 | * 641 | * @param {Object} user 642 | * - {String} blogtype, 'weibo' or other blog type, 643 | * - {String} oauth_callback, 'login callback url' or 'oob' 644 | * @param {Function(err, auth_info)} callback 645 | * - {Object} auth_info 646 | * - {String} auth_url: 'http://xxxx/auth?xxx', 647 | * - {String} oauth_token: $oauth_token, 648 | * - {String} oauth_token_secret: $oauth_token_secret 649 | * @return {Context} this, blogType api. 650 | */ 651 | get_authorization_url: function (user, callback) { 652 | return this.api_dispatch(user).get_authorization_url(user, callback); 653 | }, 654 | 655 | /** 656 | * Get access token. 657 | * 658 | * @param {Object} user 659 | * - {String} blogtype 660 | * - {String} oauth_token, authorization `oauth_token` 661 | * - {String} oauth_verifier, authorization `oauth_verifier` 662 | * - {String} oauth_token_secret, request token secret 663 | * @param {Function(err, token)} callback 664 | * - {Object} token 665 | * - {String} oauth_token 666 | * - {String} oauth_token_secret 667 | * @return {Context} this 668 | */ 669 | get_access_token: function (user, callback) { 670 | return this.api_dispatch(user).get_access_token(user, callback); 671 | }, 672 | 673 | /** 674 | * User 675 | */ 676 | 677 | /** 678 | * Get user profile infomation by access token. 679 | * 680 | * @param {Object} user 681 | * - {String} blogtype 682 | * - {String} oauth_token, access oauth token 683 | * - {String} [oauth_token_secret], access oauth token secret, oauth v2 don't need this param. 684 | * @param {Function(err, User)} callback 685 | * @return {Context} this 686 | */ 687 | verify_credentials: function (user, callback) { 688 | return this.api_dispatch(user).verify_credentials(user, callback); 689 | }, 690 | 691 | /** 692 | * Get user profile infomation by uid. 693 | * @param {Object} user 694 | * - {String} blogtype 695 | * - {String} oauth_token, access token 696 | * - {String} [oauth_token_secret], access oauth token secret, oauth v2 don't need this param. 697 | * @param {String} [uid], user id 698 | * @param {String} [screen_name], user screen_name 699 | * uid and screen_name MUST set one. 700 | * @param {Function(err, User)} callback 701 | * @return {Context} this 702 | */ 703 | user_show: function (user, uid, screen_name, callback) { 704 | if (typeof screen_name === 'function') { 705 | callback = screen_name; 706 | screen_name = null; 707 | } 708 | var self = this; 709 | return self.api_dispatch(user).user_show(user, uid, screen_name, function (err, info) { 710 | if (err) { 711 | return callback(err); 712 | } 713 | // need to get friendship info 714 | var data = { 715 | source_id: user.id, 716 | target_id: info.id 717 | }; 718 | self.friendship_show(user, data, function (err, friendship) { 719 | if (err) { 720 | return callback(err); 721 | } 722 | if (friendship.target.following) { 723 | info.follow_me = friendship.target.following; 724 | } 725 | if (friendship.target.followed_by) { 726 | info.following = friendship.target.followed_by; 727 | } 728 | callback(null, info); 729 | }); 730 | }); 731 | }, 732 | 733 | /** 734 | * Get relation between two users. 735 | * 736 | * @param {User} user 737 | * @param {Object} data, source and target. 738 | * id and screen_name must set one and only one. 739 | * tqq only support source_id and target_id. 740 | * - {String} [source_id], set source to current user when source not set. 741 | * - {String} [source_screen_name] 742 | * - {String} [target_id] 743 | * - {String} [target_screen_name] 744 | * @param {Function(err, relation)} callback 745 | * @return {Context} this 746 | */ 747 | friendship_show: function (user, data, callback) { 748 | if (data.source_id && data.source_screen_name) { 749 | delete data.source_screen_name; 750 | } 751 | if (!data.source_id && !data.source_screen_name) { 752 | data.source_id = user.uid; 753 | } 754 | if (data.target_id && data.target_screen_name) { 755 | delete data.target_screen_name; 756 | } 757 | return this.api_dispatch(user).friendship_show(user, data, callback); 758 | }, 759 | 760 | /** 761 | * Follow a user. 762 | * @param {User} user 763 | * @param {String} uid, user's id which you want to follow. 764 | * @param {String} [screen_name] 765 | * @param {Function(err, result)} callback 766 | */ 767 | friendship_create: function (user, uid, screen_name, callback) { 768 | if (typeof screen_name === 'function') { 769 | callback = screen_name; 770 | screen_name = null; 771 | } 772 | return this.api_dispatch(user).friendship_create(user, uid, screen_name, callback); 773 | }, 774 | 775 | /** 776 | * Unfollow a user. 777 | * @param {User} user 778 | * @param {String} uid, user's id which you want to unfollow. 779 | * @param {String} [screen_name] 780 | * @param {Function(err, result)} callback 781 | */ 782 | friendship_destroy: function (user, uid, screen_name, callback) { 783 | if (typeof screen_name === 'function') { 784 | callback = screen_name; 785 | screen_name = null; 786 | } 787 | return this.api_dispatch(user).friendship_destroy(user, uid, screen_name, callback); 788 | }, 789 | 790 | /** 791 | * Message 792 | */ 793 | 794 | /** 795 | * Returns the direct messages, sent to and sent by the authenticating user. 796 | * 797 | * @param {User} user 798 | * @param {Object} cursor, pagging params. 799 | * - {Number} [count], Specifies the number of records to retrieve. 800 | * - {String} [since_id], Returns results with an ID greater than (that is, more recent than) the specified ID. 801 | * - {String} [since_time], only for tqq 802 | * - {String} [max_id], Returns results with an ID less than (that is, older than) the specified ID. 803 | * - {String} [max_time], only for tqq 804 | * - {Number} [page], Specifies the page of results to retrieve. 805 | * - {Boolean} [include_entities], The entities node will not be included when set to `false`. 806 | * - {Boolean} [skip_status], When set to either true, t or 1 statuses will not be included in the returned user objects. 807 | * @param {Function(err, result)} callback 808 | */ 809 | direct_messages_both: function (user, cursor, callback) { 810 | if (typeof cursor === 'function') { 811 | callback = cursor; 812 | cursor = null; 813 | } 814 | cursor = cursor || {}; 815 | var ep = EventProxy.create('received', 'sent', function (received, sent) { 816 | var messages = received.items.concat(sent.items); 817 | messages.sort(function (a, b) { 818 | return a.created_at > b.created_at ? -1 : 1; 819 | }); 820 | callback(null, { 821 | items: messages, 822 | received_cursor: received.cursor, 823 | sent_cursor: sent.cursor 824 | }); 825 | }); 826 | ep.once('error', function (err) { 827 | ep.unbind(); 828 | callback(err); 829 | }); 830 | this.direct_messages(user, cursor, function (err, result) { 831 | if (err) { 832 | return ep.emit('error', err); 833 | } 834 | ep.emit('received', result); 835 | }); 836 | this.direct_messages_sent(user, cursor, function (err, result) { 837 | if (err) { 838 | return ep.emit('error', err); 839 | } 840 | ep.emit('sent', result); 841 | }); 842 | }, 843 | 844 | /** 845 | * Returns the 20 most recent direct messages sent to the authenticating user. 846 | * 847 | * @param {User} user 848 | * @param {Object} cursor, pagging params. 849 | * - {Number} [count], Specifies the number of records to retrieve. 850 | * - {String} [since_id], Returns results with an ID greater than (that is, more recent than) the specified ID. 851 | * - {String} [since_time], only for tqq 852 | * - {String} [max_id], Returns results with an ID less than (that is, older than) the specified ID. 853 | * - {String} [max_time], only for tqq 854 | * - {Number} [page], Specifies the page of results to retrieve. 855 | * - {Boolean} [include_entities], The entities node will not be included when set to `false`. 856 | * - {Boolean} [skip_status], When set to either true, t or 1 statuses will not be included in the returned user objects. 857 | * @param {Function(err, result)} callback 858 | */ 859 | direct_messages: function (user, cursor, callback) { 860 | return this._timeline('direct_messages', user, cursor, callback); 861 | }, 862 | 863 | /** 864 | * Returns the 20 most recent direct messages sent by the authenticating user. 865 | * 866 | * @param {User} user 867 | * @param {Object} cursor, pagging params. 868 | * - {Number} [count], Specifies the number of records to retrieve. 869 | * - {String} [since_id], Returns results with an ID greater than (that is, more recent than) the specified ID. 870 | * - {String} [since_time], only for tqq 871 | * - {String} [max_id], Returns results with an ID less than (that is, older than) the specified ID. 872 | * - {String} [max_time], only for tqq 873 | * - {Number} [page], Specifies the page of results to retrieve. 874 | * - {Boolean} [include_entities], The entities node will not be included when set to `false`. 875 | * @param {Function(err, result)} callback 876 | */ 877 | direct_messages_sent: function (user, cursor, callback) { 878 | return this._timeline('direct_messages_sent', user, cursor, callback); 879 | }, 880 | 881 | /** 882 | * Returns a single direct message, specified by an id parameter. 883 | * @param {User} user 884 | * @param {String} id, The ID of the direct message. 885 | * @param {Function(err, message)} callback 886 | */ 887 | direct_message_show: function (user, id, callback) { 888 | return this.api_dispatch(user).direct_message_show(user, id, callback); 889 | }, 890 | 891 | /** 892 | * Sends a new direct message to the specified user from the authenticating user. 893 | * @param {User} user 894 | * @param {Object} toUser, One of uid or screen_name are required. 895 | * - {String} uid, The ID of the user who should receive the direct message. 896 | * - {String} screen_name, The screen name of the user who should receive the direct message. 897 | * @param {String} text, The text of your direct message. Be sure to URL encode as necessary. 898 | * @param {Function(err, result)} callback 899 | */ 900 | direct_message_create: function (user, toUser, text, callback) { 901 | return this.api_dispatch(user).direct_message_create(user, toUser, text, callback); 902 | }, 903 | 904 | /** 905 | * Destroys the direct message specified in the required ID parameter. 906 | * @param {User} user 907 | * @param {String} id, The ID of the direct message to delete. 908 | * @param {Function(err, result)} callback 909 | */ 910 | direct_message_destroy: function (user, id, callback) { 911 | return this.api_dispatch(user).direct_message_destroy(user, id, callback); 912 | }, 913 | 914 | /** 915 | * Search Statuses and Users 916 | */ 917 | 918 | /** 919 | * Search suggestion users when @somebody. 920 | * 921 | * @param {User} user 922 | * @param {String} q, search keyword 923 | * @param {Object} [cursor] 924 | * - {Number} [count], return records number, default is `10`. 925 | * - {Number} [type], suggestion type, 0: I following, 1: My followers. default is `0`. 926 | * - {Number} [range], suggestion search range, 0: only screen_name, 1: only remark, 2: both. default is `2`. 927 | * @param {Function(err, result)} callback 928 | * - {Object} result: 929 | * - {Array} items: [ SuggetionUser, ... ] 930 | * - {SuggetionUser} { id: '123123', screen_name: 'QLeeLulu', remark: '' } 931 | */ 932 | search_suggestions_at_users: function (user, q, cursor, callback) { 933 | if (typeof cursor === 'function') { 934 | callback = cursor; 935 | cursor = null; 936 | } 937 | cursor = cursor || {}; 938 | cursor.count = cursor.count || 10; 939 | cursor.type = String(cursor.count || 0); 940 | cursor.q = q; 941 | return this.api_dispatch(user).search_suggestions_at_users(user, cursor, callback); 942 | }, 943 | 944 | /** 945 | * Search statuses by query. 946 | * 947 | * @param {AccessToken} user 948 | * @param {String|Object} query 949 | * - {String} q, query keyword 950 | * - {String} [long], longitude 951 | * - {String} [lat], latitude 952 | * - {String} [radius], radius for longitude and latitude. 953 | * @param {Cursor} [cursor] 954 | * - {Number} [count], default is `20` 955 | * - {Number} [page], default is the first page. 956 | * @param {Function(err, result)} callback 957 | * @return {Context} this 958 | */ 959 | search: function (user, query, cursor, callback) { 960 | if (typeof query === 'string') { 961 | query = { 962 | q: query 963 | }; 964 | } 965 | if (typeof cursor === 'function') { 966 | callback = cursor; 967 | cursor = null; 968 | } 969 | return this.api_dispatch(user).search(user, query, cursor, callback); 970 | }, 971 | 972 | /** 973 | * Search users by query. 974 | * @param {User} user 975 | * @param {String} query 976 | * @param {Object} cursor 977 | * @param {Function(err, result)} callback 978 | */ 979 | user_search: function (user, query, cursor, callback) { 980 | if (typeof cursor === 'function') { 981 | callback = cursor; 982 | cursor = null; 983 | } 984 | return this.api_dispatch(user).user_search(user, query, cursor, callback); 985 | }, 986 | 987 | }; 988 | 989 | TAPI.friends_timeline = TAPI.home_timeline; 990 | -------------------------------------------------------------------------------- /lib/tbase_oauth_v2.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * node-weibo - lib/tbase_oauth_v2.js 3 | * Copyright(c) 2012 fengmk2 4 | * MIT Licensed 5 | */ 6 | 7 | "use strict"; 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var TBase = require('./tbase'); 14 | var inherits = require('util').inherits; 15 | var utils = require('./utils'); 16 | var querystring = require('querystring'); 17 | 18 | /** 19 | * TAPI Base class, support OAuth v2.0 20 | */ 21 | function TBaseOauthV2() { 22 | TBaseOauthV2.super_.call(this); 23 | this.config.oauth_version = '2.0'; 24 | } 25 | 26 | inherits(TBaseOauthV2, TBase); 27 | module.exports = TBaseOauthV2; 28 | 29 | /** 30 | * Result formatters 31 | */ 32 | 33 | TBaseOauthV2.prototype.format_access_token = function (token) { 34 | token = JSON.parse(token); 35 | return token; 36 | }; 37 | 38 | /** 39 | * OAuth 40 | */ 41 | 42 | TBaseOauthV2.prototype.convert_token = function (user) { 43 | var params = { 44 | redirect_uri: user.oauth_callback || this.config.oauth_callback, 45 | client_id: this.config.appkey, 46 | response_type: 'code', 47 | }; 48 | var oauth_scope = user.oauth_scope || this.config.oauth_scope; 49 | if (oauth_scope) { 50 | params.oauth_scope = oauth_scope; 51 | } 52 | if (user.state) { 53 | // An unguessable random string. It is used to protect against cross-site request forgery attacks. 54 | params.state = user.state; 55 | } 56 | if (user.forcelogin) { 57 | params.forcelogin = user.forcelogin; 58 | } 59 | return params; 60 | }; 61 | 62 | TBaseOauthV2.prototype.get_authorization_url = function (user, callback) { 63 | var data = this.convert_token(user); 64 | data.response_type = 'code'; 65 | var info = { 66 | blogtype: user.blogtype, 67 | auth_url: this.format_authorization_url(data) 68 | }; 69 | process.nextTick(function () { 70 | callback(null, info); 71 | }); 72 | return this; 73 | }; 74 | 75 | TBaseOauthV2.prototype.get_access_token = function (user, callback) { 76 | var params = { 77 | type: 'POST', 78 | user: user, 79 | playload: 'string', 80 | api_host: this.config.oauth_host, 81 | request_method: 'get_access_token' 82 | }; 83 | var data = this.convert_token(user); 84 | data.grant_type = 'authorization_code'; 85 | data.client_secret = this.config.secret; 86 | var code = user.code || user.oauth_verifier || user.oauth_pin; 87 | if (code) { 88 | data.code = code; 89 | } 90 | 91 | params.data = data; 92 | var self = this; 93 | var url = self.config.oauth_access_token; 94 | self.send_request(url, params, function (err, token) { 95 | if (err) { 96 | return callback(err); 97 | } 98 | // { access_token: '2.00EkofzBtMpzNBb9bc3108d8MwDTTE', 99 | // remind_in: '633971', 100 | // expires_in: 633971, 101 | // uid: '1827455832' } 102 | token = self.format_access_token(token); 103 | if (!token.access_token) { 104 | var message = token.error || JSON.stringify(token); 105 | err = new Error(message); 106 | err.data = token; 107 | err.name = self.errorname('get_access_token'); 108 | return callback(err); 109 | } 110 | token.blogtype = user.blogtype; 111 | callback(null, token); 112 | }); 113 | return this; 114 | }; 115 | 116 | TBaseOauthV2.prototype.apply_auth = function (url, args, user) { 117 | args.data = args.data || {}; 118 | args.data.access_token = user.access_token; 119 | }; 120 | 121 | -------------------------------------------------------------------------------- /lib/tqq.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * node-weibo - lib/tqq.js 3 | * Copyright(c) 2012 fengmk2 4 | * MIT Licensed 5 | */ 6 | 7 | "use strict"; 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var inherits = require('util').inherits; 14 | var EventProxy = require('eventproxy'); 15 | var TBase = require('./tbase'); 16 | var utils = require('./utils'); 17 | 18 | function TQQAPI(options) { 19 | TQQAPI.super_.call(this); 20 | 21 | this.blogtype = 'tqq'; 22 | 23 | var config = utils.extend({}, options, { 24 | host: 'http://open.t.qq.com/api', 25 | user_home_url: 'http://t.qq.com/', 26 | result_format: '', 27 | oauth_host: 'https://open.t.qq.com', 28 | oauth_authorize: '/cgi-bin/authorize', 29 | oauth_request_token: '/cgi-bin/request_token', 30 | oauth_access_token: '/cgi-bin/access_token', 31 | 32 | count_max_number: 30, 33 | 34 | // 竟然是通过get传递 35 | oauth_params_by_get: true, 36 | support_comment: false, // 不支持comment_timeline 37 | support_do_comment: true, 38 | support_repost_timeline: true, // 支持查看转发列表 39 | support_favorites_max_id: true, 40 | reply_dont_need_at_screen_name: true, // @回复某条微博 无需填充@screen_name 41 | rt_at_name: true, // RT的@name而不是@screen_name 42 | repost_delimiter: ' || ', //转发时的分隔符 43 | support_counts: false, // 只有rt_count这个,不过貌似有问题,总是404。暂时隐藏 44 | 45 | home_timeline: '/statuses/home_timeline', 46 | mentions: '/statuses/mentions_timeline', 47 | comments_timeline: '/statuses/mentions_timeline', 48 | comments_mentions: '/statuses/mentions_timeline', 49 | 50 | repost_timeline: '/t/re_list', 51 | 52 | followers: '/friends/user_fanslist', 53 | friends: '/friends/user_idollist', 54 | favorites: '/fav/list_t', 55 | favorite_show: null, // use show() to mock this api 56 | favorite_create: '/fav/addt', 57 | favorite_destroy: '/fav/delt', 58 | count: '/t/re_count', //仅仅是转播数 59 | show: '/t/show', 60 | update: '/t/add', 61 | upload: '/t/add_pic', 62 | repost: '/t/re_add', 63 | comment_create: '/t/comment', 64 | comment_reply: '/t/comment', 65 | comments: '/t/re_list', 66 | destroy: '/t/del', 67 | 68 | direct_messages: '/private/recv', 69 | direct_messages_sent: '/private/send', 70 | direct_message_create: '/private/add', 71 | direct_message_destroy: '/private/del', 72 | 73 | rate_limit_status: '/account/rate_limit_status', 74 | friendship_create: '/friends/add', 75 | friendship_destroy: '/friends/del', 76 | friendship_show: '/friends/check', 77 | reset_count: '/statuses/reset_count', 78 | user_show: '/user/other_info', 79 | 80 | // 用户标签 81 | tags: '/tags', 82 | create_tag: '/tags/create', 83 | destroy_tag: '/tags/destroy', 84 | tags_suggestions: '/tags/suggestions', 85 | 86 | // 搜索 87 | search: '/search/t', 88 | user_search: '/search/user', 89 | verify_credentials: '/user/info', 90 | 91 | gender_map: {0: 'n', 1: 'm', 2: 'f'}, 92 | 93 | // support apis 94 | support_comment_destroy: false, 95 | support_comments_mentions: false, 96 | support_comments_by_me: false, 97 | // support_search_suggestions_at_users: false, 98 | // support_user_search: false, 99 | 100 | }); 101 | 102 | this.init(config); 103 | } 104 | 105 | inherits(TQQAPI, TBase); 106 | module.exports = TQQAPI; 107 | 108 | /** 109 | * Utils methods 110 | */ 111 | 112 | TQQAPI.prototype.detect_error = function (method, res, playload, data) { 113 | var headers = res.headers; 114 | var err; 115 | if (res.statusCode === 200 && headers.status) { 116 | err = new Error(headers.status); 117 | } else if (data.errcode && data.msg) { 118 | err = new Error(data.msg); 119 | } else if (!data.data && data.msg && data.errcode !== 0) { 120 | err = new Error(data.msg); 121 | } 122 | if (err) { 123 | err.name = this.errorname(method); 124 | err.data = data; 125 | return err; 126 | } 127 | return TQQAPI.super_.prototype.detect_error.call(this, method, res, playload, data); 128 | }; 129 | 130 | TQQAPI.prototype.url_encode = function (text) { 131 | return text; 132 | }; 133 | 134 | /** 135 | * Emotions 136 | */ 137 | 138 | var _VIDEO_PADDING = '!!!{{status.video.shorturl}}!!!'; 139 | var _EMOTION_MAP = { 140 | 1: '狂喜', 141 | 2: '偷乐', 142 | 3: '无感', 143 | 4: '伤心', 144 | 5: '咆哮' 145 | }; 146 | 147 | TQQAPI.prototype.process_text = function (status) { 148 | var text = status.text; 149 | var hasVideo = false; 150 | if (status.video && status.video.picurl && text) { 151 | // 添加视频链接 152 | if (text.indexOf(status.video.shorturl) < 0) { 153 | text += ' ' + status.video.shorturl; 154 | } 155 | text = text.replace(status.video.shorturl, this._VIDEO_PADDING); 156 | hasVideo = true; 157 | } 158 | 159 | text = utils.escape(text); 160 | text = text.replace(this.URL_RE, this._replace_url); 161 | 162 | text = this.process_at(text, status.users || {}); //@*** 163 | 164 | text = this.process_emotional(text); 165 | 166 | text = this.process_search(text); //#xxXX# 167 | 168 | text = this.process_emoji(text); 169 | 170 | if (hasVideo) { 171 | var video_html = '' + status.video.shorturl + ''; 173 | text = text.replace(this._VIDEO_PADDING, video_html); 174 | text += '
'; 175 | } 176 | 177 | if (status.emotionurl) { 178 | var title = _EMOTION_MAP[status.emotiontype] || ('未知心情:' + status.emotiontype); 179 | text = '' + title + '' + text; 180 | } 181 | return text || ' '; 182 | }; 183 | 184 | var _SHUOSHUO_EMOTION_RE = /\[em\](\w+)\[\/em\]/g; 185 | var _EMOTION_RE = null; 186 | 187 | TQQAPI.prototype.process_emotional = function (text) { 188 | // show shuoshuo faces : http://code.google.com/p/falang/issues/detail?id=318 189 | text = text.replace(_SHUOSHUO_EMOTION_RE, function (m, g1) { 190 | if (g1) { 191 | return ''; 192 | } 193 | }); 194 | var emotions = this.load_emotions(); 195 | var map = emotions[1]; 196 | if (!_EMOTION_RE) { 197 | _EMOTION_RE = new RegExp('\/(' + Object.keys(map).join('|') + ')', 'g'); 198 | } 199 | var tpl = ''; 200 | return text.replace(_EMOTION_RE, function (m, g1) { 201 | var face = map[g1]; 202 | if (face) { 203 | return utils.format(tpl, {title: g1, face: emotions[0] + face}); 204 | } 205 | return m; 206 | }); 207 | }; 208 | 209 | var AT_USER_RE = /([^#])?@([\w\-\_]+)/g; 210 | var ONLY_AT_USER_RE = /@([\w\-\_]+)/g; 211 | 212 | TQQAPI.prototype.process_at = function (text, users) { //@*** 213 | var tpl = '{{pre}}@{{screen_name}}'; 215 | var homeurl = this.config.user_home_url; 216 | return text.replace(AT_USER_RE, function (match, m1, m2) { 217 | var uid = m2; 218 | var username = users[uid]; 219 | if (username) { 220 | username += '(@' + uid + ')'; 221 | } else { 222 | username = uid; 223 | } 224 | var data = { 225 | pre: m1 || '', 226 | url: homeurl + uid, 227 | uid: uid, 228 | screen_name: username 229 | }; 230 | return utils.format(tpl, data); 231 | }); 232 | }; 233 | 234 | /** 235 | * Result getters 236 | */ 237 | 238 | TQQAPI.prototype.get_result_items = function (data, playload, args) { 239 | if (playload === 'count') { 240 | var counts = []; 241 | for (var id in data) { 242 | var item = data[id]; 243 | counts.push({id: id, reposts: item.count, comments: item.mcount}); 244 | } 245 | return counts; 246 | } 247 | var items = data && data.info; 248 | return items || []; 249 | }; 250 | 251 | /** 252 | * { hasnext: 0, 253 | info: 254 | [ [Object], 255 | [Object], 256 | [Object], 257 | [Object], 258 | [Object], 259 | [Object], 260 | [Object], 261 | [Object], 262 | [Object] ], 263 | timestamp: 1348753615, 264 | user: { GreenMango: '青芒', 'node-weibo': 'node-weibo' } }, 265 | */ 266 | // TQQAPI.prototype.get_pagging_cursor = function (data, playload, args) { 267 | // return {}; 268 | // }; 269 | 270 | /** 271 | * Result formatters 272 | */ 273 | 274 | TQQAPI.prototype.format_result = function (data, playload, args) { 275 | data = data.data; 276 | var result = TQQAPI.super_.prototype.format_result.call(this, data, playload, args); 277 | if (data && data.user) { 278 | if (Array.isArray(result.items)) { 279 | var items = result.items; 280 | for (var i = 0; i < items.length; i++) { 281 | items[i].users = data.user; 282 | } 283 | } else { 284 | result.users = data.user; 285 | } 286 | } 287 | return result; 288 | }; 289 | 290 | TQQAPI.prototype.format_search_status = function (status, args) { 291 | throw new Error('Must override this method.'); 292 | }; 293 | 294 | /** 295 | * 296 | { city_code: '1', 297 | count: 0, 298 | country_code: '1', 299 | emotiontype: 0, 300 | emotionurl: '', 301 | from: '微博开放平台', 302 | fromurl: 'http://wiki.open.t.qq.com/index.php/%E4%BA%A7%E5%93%81%E7%B1%BBFAQ#.E6.8F.90.E4.BA.A4.E5.BA.94.E7.94.A8.E6.9D.A5.E6.BA.90.E5.AD.97.E6.AE.B5.E5.AE.A1.E6.A0.B8.E8.83.BD.E5.BE.97.E5.88.B0.E4.BB.80.E4.B9.88.E5.A5.BD.E5.A4.84.EF.BC.9F\n', 303 | geo: '广东省中山市康乐路10号', 304 | head: 'http://app.qlogo.cn/mbloghead/cb1c4eb21aa2b52a233a', 305 | id: '102460077174373', 306 | image: null, 307 | isrealname: 2, 308 | isvip: 0, 309 | jing: '113.421234', 310 | latitude: '22.354231', 311 | location: '中国 浙江 杭州', 312 | longitude: '113.421234', 313 | mcount: 0, 314 | music: null, 315 | name: 'node-weibo', 316 | nick: 'node-weibo', 317 | openid: 'EA68676D5E9DA465822CD0CEB2DC6EF5', 318 | origtext: '这是update(user, status, callback) 的单元测试,当前时间 Thu Sep 27 2012 17:04:25 GMT+0800 (CST)', 319 | province_code: '33', 320 | self: 1, 321 | source: null, 322 | status: 0, 323 | text: '这是update(user, status, callback) 的单元测试,当前时间 Thu Sep 27 2012 17:04:25 GMT+0800 (CST)', 324 | timestamp: 1348736665, 325 | type: 1, 326 | user: { 'node-weibo': 'node-weibo' }, 327 | video: null, 328 | wei: '22.354231' } 329 | 330 | */ 331 | TQQAPI.prototype.format_status = function (data, args) { 332 | var status = {}; 333 | status.id = String(data.id); 334 | status.t_url = 'http://t.qq.com/p/t/' + data.id; 335 | if (!data.timestamp) { 336 | return status; 337 | } 338 | status.timestamp = data.timestamp; 339 | status.created_at = new Date(data.timestamp * 1000); 340 | status.text = data.origtext; 341 | data.fromurl = (data.fromurl || 'http://t.qq.com').trim(); 342 | status.source = '' + data.from + ''; 343 | // status.favorited = 344 | if (data.image && data.image[0]) { 345 | var image = data.image[0]; 346 | status.thumbnail_pic = image + '/160'; 347 | status.bmiddle_pic = image + '/460'; 348 | status.original_pic = image + '/2000'; 349 | } 350 | if (data.latitude && String(data.latitude) !== '0') { 351 | status.geo = this.format_geo(data, args); 352 | } 353 | if (data.name) { 354 | status.user = this.format_user(data, args); 355 | } 356 | status.reposts_count = data.count || 0; 357 | status.comments_count = data.mcount || 0; 358 | if (data.source) { 359 | status.retweeted_status = this.format_status(data.source, args); 360 | } 361 | return status; 362 | }; 363 | 364 | /** 365 | * 366 | { birth_day: 1, 367 | birth_month: 1, 368 | birth_year: 2010, 369 | city_code: '1', 370 | comp: null, 371 | country_code: '1', 372 | edu: null, 373 | email: '', 374 | exp: 56, 375 | fansnum: 3, 376 | favnum: 0, 377 | head: 'http://app.qlogo.cn/mbloghead/2045de7c75623f2c2b06', 378 | homecity_code: '', 379 | homecountry_code: '', 380 | homepage: '', 381 | homeprovince_code: '', 382 | hometown_code: '', 383 | idolnum: 46, 384 | industry_code: 0, 385 | introduction: '', 386 | isent: 0, 387 | ismyblack: 0, 388 | ismyfans: 0, 389 | ismyidol: 0, 390 | isrealname: 2, 391 | isvip: 0, 392 | level: 1, 393 | location: '中国 杭州', 394 | mutual_fans_num: 0, 395 | name: 'node-weibo', 396 | nick: 'node-weibo', 397 | openid: 'EA68676D5E9DA465822CD0CEB2DC6EF5', 398 | province_code: '33', 399 | regtime: 1348724066, 400 | send_private_flag: 2, 401 | sex: 1, 402 | tag: null, 403 | tweetinfo: 404 | [ { city_code: '1', 405 | country_code: '1', 406 | emotiontype: 0, 407 | emotionurl: '', 408 | from: '腾讯微博', 409 | fromurl: 'http://t.qq.com\n', 410 | geo: '', 411 | id: '70997003338788', 412 | image: null, 413 | latitude: '0', 414 | location: '中国 杭州', 415 | longitude: '0', 416 | music: null, 417 | origtext: '#新人报到# 伟大的旅程都是从第一条微博开始的!', 418 | province_code: '33', 419 | self: 1, 420 | status: 0, 421 | text: '#新人报到# 伟大的旅程都是从第一条微博开始的!', 422 | timestamp: 1348724111, 423 | type: 1, 424 | video: null } ], 425 | tweetnum: 1, 426 | verifyinfo: '' } 427 | */ 428 | TQQAPI.prototype.format_user = function (data, args) { 429 | var user = {}; 430 | user.id = data.name; 431 | user.t_url = 'http://t.qq.com/' + data.name; 432 | user.screen_name = data.nick; 433 | user.name = data.name; 434 | user.location = data.location || ''; 435 | user.description = data.introduction || ''; 436 | // no url 437 | if (data.head) { 438 | user.profile_image_url = data.head + '/50'; // 竟然直接获取的地址无法拿到头像 439 | user.avatar_large = data.head + '/180'; 440 | } else { 441 | user.profile_image_url = 'http://mat1.gtimg.com/www/mb/images/head_50.jpg'; 442 | user.avatar_large = 'http://mat1.gtimg.com/www/mb/images/head_180.jpg'; 443 | } 444 | user.gender = this.config.gender_map[data.sex||0]; 445 | user.followers_count = data.fansnum || 0; 446 | user.friends_count = data.idolnum || 0; 447 | user.statuses_count = data.tweetnum || 0; 448 | user.favourites_count = data.favnum || 0; 449 | if (data.regtime) { 450 | user.created_at = new Date(data.regtime * 1000); 451 | } 452 | user.following = data.ismyidol || false; 453 | user.follow_me = data.ismyfans || false; 454 | // send_private_flag : 是否允许所有人给当前用户发私信,0-仅有偶像,1-名人+听众,2-所有人, 455 | user.allow_all_act_msg = data.send_private_flag === 2; 456 | // no geo_enabled 457 | user.verified = !!data.isvip; 458 | // no verified_type 459 | user.verified_reason = data.verifyinfo || ''; 460 | // user.remark = 461 | user.allow_all_comment = true; 462 | // user.online_status = true; 463 | user.bi_followers_count = data.mutual_fans_num || 0; 464 | // user.lang 465 | if (data.tweetinfo && data.tweetinfo[0]) { 466 | user.status = this.format_status(data.tweetinfo[0], args); 467 | } 468 | 469 | if (data.tag) { 470 | user.tags = data.tag; 471 | } 472 | return user; 473 | }; 474 | 475 | TQQAPI.prototype.format_count = function (count, args) { 476 | return count; 477 | }; 478 | 479 | TQQAPI.prototype.format_geo = function (data, args) { 480 | var geo = { 481 | longitude: data.longitude, 482 | latitude: data.latitude, 483 | // city_name string City name "广州" 484 | // province_name string Province name "广东" 485 | address: data.geo, 486 | }; 487 | return geo; 488 | }; 489 | 490 | TQQAPI.prototype.format_comment = function (data, args) { 491 | var comment = this.format_status(data, args); 492 | if (comment.retweeted_status) { 493 | comment.status = comment.retweeted_status; 494 | delete comment.retweeted_status; 495 | } 496 | return comment; 497 | }; 498 | 499 | TQQAPI.prototype.format_message = function (message, args) { 500 | var recipient = null; 501 | if (message.toname) { 502 | // tohead: 'http://app.qlogo.cn/mbloghead/03cfac444e03cafd2a3a', 503 | // toisvip: 0, 504 | // toname: 'fengmk2', 505 | // tonick: 'Python发烧友', 506 | recipient = { 507 | name: message.toname, 508 | nick: message.tonick, 509 | isvip: message.toisvip, 510 | head: message.tohead 511 | }; 512 | } 513 | 514 | message = this.format_status(message, args); 515 | if (message.user) { 516 | message.sender = message.user; 517 | delete message.user; 518 | } 519 | if (recipient) { 520 | message.recipient = this.format_user(recipient); 521 | } 522 | return message; 523 | }; 524 | 525 | TQQAPI.prototype.format_emotion = function (emotion, args) { 526 | throw new Error('Must override this method.'); 527 | }; 528 | 529 | TQQAPI.prototype.format_favorite = function (status, args) { 530 | var favorite = { 531 | created_at: new Date(status.storetime * 1000), 532 | status: this.format_status(status) 533 | }; 534 | return favorite; 535 | }; 536 | 537 | /** 538 | * Params converters 539 | */ 540 | 541 | TQQAPI.prototype.convert_friendship = function (data) { 542 | var args = { 543 | name: data.uid 544 | }; 545 | return args; 546 | }; 547 | 548 | TQQAPI.prototype.convert_comment = function (comment) { 549 | // http://wiki.open.t.qq.com/index.php/%E5%BE%AE%E5%8D%9A%E7%9B%B8%E5%85%B3/%E7%82%B9%E8%AF%84%E4%B8%80%E6%9D%A1%E5%BE%AE%E5%8D%9A 550 | var data = { 551 | content: comment.comment, 552 | reid: comment.id 553 | }; 554 | return data; 555 | }; 556 | 557 | TQQAPI.prototype.convert_message = function (message) { 558 | // http://wiki.open.t.qq.com/index.php/API%E6%96%87%E6%A1%A3/%E7%A7%81%E4%BF%A1%E7%9B%B8%E5%85%B3/%E5%8F%91%E7%A7%81%E4%BF%A1 559 | var data = { 560 | content: message.text, 561 | contentflag: 1, 562 | name: message.uid, 563 | }; 564 | if (message.openid) { 565 | data.fopenid = message.openid; 566 | } 567 | // TODO: support pic 568 | return data; 569 | }; 570 | 571 | TQQAPI.prototype.convert_status = function (status) { 572 | // syncflag 微博同步到空间分享标记(可选,0-同步,1-不同步,默认为0),目前仅支持oauth1.0鉴权方式 573 | var data = { 574 | content: status.status 575 | }; 576 | if (status.long) { 577 | data.longitude = status.long; 578 | data.latitude = status.lat; 579 | } 580 | if (status.id) { 581 | data.reid = status.id; 582 | } 583 | return data; 584 | }; 585 | 586 | TQQAPI.prototype.convert_user = function (user) { 587 | var data = { 588 | name: user.uid || user.screen_name 589 | }; 590 | return data; 591 | }; 592 | 593 | TQQAPI.prototype.convert_ids = function (ids) { 594 | return { 595 | ids: ids, 596 | flag: '2' 597 | }; 598 | }; 599 | 600 | TQQAPI.prototype.convert_user_search_cursor = function (cursor) { 601 | var data = { 602 | keyword: cursor.q, 603 | pagesize: cursor.count, 604 | page: cursor.page || 1 605 | }; 606 | return data; 607 | }; 608 | 609 | /** 610 | * pageflag 611 | 分页标识(0:第一页,1:向下翻页,2:向上翻页) 612 | pagetime 613 | 本页起始时间(第一页:填0,向上翻页:填上一次请求返回的第一条记录时间,向下翻页:填上一次请求返回的最后一条记录时间) 614 | reqnum 615 | 每次请求记录的条数(1-70条) 616 | type 617 | 拉取类型(需填写十进制数字) 618 | 0x1 原创发表 0x2 转载 如需拉取多个类型请使用|,如(0x1|0x2)得到3,则type=3即可,填零表示拉取所有类型 619 | contenttype 620 | 内容过滤。0-表示所有类型,1-带文本,2-带链接,4-带图片,8-带视频,0x10-带音频 621 | 建议不使用contenttype为1的类型,如果要拉取只有文本的微博,建议使用0x80 622 | * 623 | */ 624 | TQQAPI.prototype.convert_cursor = function (cursor) { 625 | var data = {}; 626 | // type: 拉取类型, 0x1 原创发表 0x2 转载 0x8 回复 0x10 空回 0x20 提及 0x40 点评 627 | data.type = String(0x1 | 0x2 | 0x8 | 0x10 | 0x20); 628 | data.contenttype = '0'; 629 | data.reqnum = cursor.count; 630 | if (cursor.max_id) { 631 | // get older statuses 632 | data.pageflag = '1'; 633 | data.pagetime = cursor.max_time; 634 | data.lastid = cursor.max_id; 635 | } else if (cursor.since_id) { 636 | // get newer statuses 637 | // 0:第一页,1:向下翻页,2:向上翻页 638 | data.pageflag = '2'; 639 | data.pagetime = cursor.since_time; 640 | data.lastid = cursor.sina_id; 641 | } else { 642 | // top page 643 | data.pageflag = '0'; 644 | data.pagetime = '0'; 645 | data.lastid = '0'; 646 | } 647 | if (typeof cursor.callback === 'function') { 648 | data = cursor.callback(data); 649 | } 650 | if (cursor.page) { 651 | data.page = cursor.page; 652 | } 653 | return data; 654 | }; 655 | 656 | /** 657 | * Status 658 | */ 659 | 660 | TQQAPI.prototype.repost_timeline = function (user, cursor, callback) { 661 | cursor.callback = function (data) { 662 | data.rootid = cursor.id; 663 | data.flag = '0'; 664 | // twitterid 微博id,与pageflag、pagetime共同使用,实现翻页功能(第1页填0,继续向下翻页,填上一次请求返回的最后一条记录id) 665 | if (data.lastid) { 666 | data.twitterid = data.lastid; 667 | delete data.lastid; 668 | } 669 | return data; 670 | }; 671 | return TQQAPI.super_.prototype.repost_timeline.call(this, user, cursor, callback); 672 | }; 673 | 674 | TQQAPI.prototype.user_timeline = function (user, cursor, callback) { 675 | cursor.callback = function (data) { 676 | if (cursor.uid || cursor.screen_name) { 677 | data.name = cursor.uid || cursor.screen_name; 678 | } 679 | return data; 680 | }; 681 | return TQQAPI.super_.prototype.user_timeline.call(this, user, cursor, callback); 682 | }; 683 | 684 | /** 685 | * Comment 686 | */ 687 | 688 | TQQAPI.prototype.comments_timeline = function (user, cursor, callback) { 689 | cursor.callback = function (data) { 690 | data.type = String(0x40); 691 | return data; 692 | }; 693 | return TQQAPI.super_.prototype.comments_timeline.call(this, user, cursor, callback); 694 | }; 695 | 696 | TQQAPI.prototype.comments = function (user, cursor, callback) { 697 | cursor.callback = function (data) { 698 | data.rootid = cursor.id; 699 | data.flag = '1'; 700 | if (data.lastid) { 701 | data.twitterid = data.lastid; 702 | delete data.lastid; 703 | } 704 | return data; 705 | }; 706 | return TQQAPI.super_.prototype.comments.call(this, user, cursor, callback); 707 | }; 708 | 709 | TQQAPI.prototype.comment_destroy = function (user, cid, callback) { 710 | callback(new TypeError('comment_destroy not support.')); 711 | }; 712 | 713 | /** 714 | * Favorite 715 | */ 716 | 717 | TQQAPI.prototype.favorite_show = function (user, id, callback) { 718 | var self = this; 719 | self.show(user, id, function (err, status) { 720 | if (err) { 721 | err.name = self.errorname('favorite_show'); 722 | return callback(err); 723 | } 724 | var favorite = { 725 | status: status, 726 | created_at: status.created_at 727 | }; 728 | callback(null, favorite); 729 | }); 730 | }; 731 | 732 | /** 733 | * FriendShip 734 | */ 735 | 736 | TQQAPI.prototype.friendship_show = function (user, data, callback) { 737 | var args = { 738 | names: data.target_id, 739 | flag: 2 740 | }; 741 | TQQAPI.super_.prototype.friendship_show.call(this, user, args, function (err, relations) { 742 | if (err) { 743 | return callback(err); 744 | } 745 | var relation = relations[data.target_id] || {}; 746 | // name1:{isidol:true,isfans,false}, 747 | // isfans: following source 748 | // isidol: followed_by source 749 | var ship = { 750 | target: { 751 | id: data.target_id, 752 | following: relation.isfans, 753 | followed_by: relation.isidol, 754 | }, 755 | source: { 756 | id: data.source_id 757 | } 758 | }; 759 | callback(null, ship); 760 | }); 761 | }; 762 | 763 | 764 | /** 765 | * Search 766 | */ 767 | 768 | TQQAPI.prototype.search = function (user, query, cursor, callback) { 769 | cursor = cursor || {}; 770 | var q = { 771 | keyword: query.q 772 | }; 773 | if (query.long && query.lat && query.radius) { 774 | q.longitude = query.long; 775 | q.latitude = query.lat; 776 | q.radius = query.radius; 777 | } 778 | cursor.callback = function (data) { 779 | data.pagesize = data.reqnum || 20; 780 | return data; 781 | }; 782 | return TQQAPI.super_.prototype.search.call(this, user, q, cursor, callback); 783 | }; 784 | 785 | TQQAPI.prototype.search_suggestions_at_users = function (user, cursor, callback) { 786 | return this.user_search(user, cursor.q, cursor, function (err, result) { 787 | if (err) { 788 | return callback(err); 789 | } 790 | var users = []; 791 | var items = result.items || []; 792 | for (var i = 0; i < items.length; i++) { 793 | var item = items[i]; 794 | users.push({ 795 | id: item.id, 796 | screen_name: item.screen_name, 797 | remark: item.name, 798 | }); 799 | } 800 | result.items = users; 801 | callback(null, result); 802 | }); 803 | }; 804 | 805 | TQQAPI.prototype.comments_to_me = TQQAPI.prototype.comments_timeline; 806 | 807 | -------------------------------------------------------------------------------- /lib/tsohu.js: -------------------------------------------------------------------------------- 1 | (function(exports){ 2 | 3 | var TSinaAPI = null , 4 | querystring = require('querystring'); 5 | 6 | if(typeof require !== 'undefined') { 7 | TSinaAPI = require('./tsina').TSinaAPI; 8 | } else { 9 | TSinaAPI = weibo.tsina.TSinaAPI; 10 | } 11 | 12 | var TSOHUAPI = exports.TSOHUAPI = Object.inherits({}, TSinaAPI, { 13 | config: Object.extend({}, TSinaAPI.config, { 14 | host: 'http://api.t.sohu.com', 15 | user_home_url: 'http://t.sohu.com/', 16 | search_url: 'http://t.sohu.com/k/', 17 | result_format: '.json', 18 | source: '', 19 | oauth_key: '', 20 | oauth_secret: '', 21 | oauth_host: 'http://api.t.sohu.com', 22 | oauth_authorize: '/oauth/authorize', 23 | oauth_request_token: '/oauth/request_token', 24 | oauth_access_token: '/oauth/access_token', 25 | // 竟然是通过get传递 26 | oauth_params_by_get: true, 27 | support_comment: false, // 不支持comment_timeline 28 | support_do_comment: true, 29 | support_repost_timeline: true, // 支持查看转发列表 30 | support_favorites_max_id: true, 31 | reply_dont_need_at_screen_name: true, // @回复某条微博 无需填充@screen_name 32 | rt_at_name: true, // RT的@name而不是@screen_name 33 | repost_delimiter: ' || ', //转发时的分隔符 34 | support_counts: false, // 只有rt_count这个,不过貌似有问题,总是404。暂时隐藏 35 | 36 | latitude_field: 'wei', // 纬度参数名 37 | longitude_field: 'jing', // 经度参数名 38 | friends_timeline: '/statuses/home_timeline', 39 | repost_timeline: '/t/re_list_repost', 40 | 41 | mentions: '/statuses/mentions_timeline', 42 | followers: '/friends/user_fanslist', 43 | friends: '/friends/user_idollist', 44 | favorites: '/fav/list_t', 45 | favorites_create: '/fav/addt', 46 | favorites_destroy: '/fav/delt', 47 | counts: '/t/re_count', //仅仅是转播数 48 | status_show: '/t/show', 49 | update: '/statuses/update', 50 | upload: '/statuses/upload', 51 | repost: '/t/re_add', 52 | comment: '/t/comment', 53 | comments: '/t/re_list', 54 | destroy: '/t/del', 55 | destroy_msg: '/private/del', 56 | direct_messages: '/private/recv', 57 | sent_direct_messages: '/private/send', 58 | new_message: '/private/add', 59 | rate_limit_status: '/account/rate_limit_status', 60 | friendships_create: '/friends/add', 61 | friendships_destroy: '/friends/del', 62 | friendships_show: '/friends/check', 63 | reset_count: '/statuses/reset_count', 64 | user_show: '/user/other_info', 65 | 66 | // 用户标签 67 | tags: '/tags', 68 | create_tag: '/tags/create', 69 | destroy_tag: '/tags/destroy', 70 | tags_suggestions: '/tags/suggestions', 71 | 72 | // 搜索 73 | search: '/search/t', 74 | user_search: '/search/user', 75 | verify_credentials: '/account/verify_credentials', 76 | 77 | gender_map: {0:'n', 1:'m', 2:'f'}, 78 | 79 | ErrorCodes: { 80 | 1: '参数错误', 81 | 2: '频率受限', 82 | 3: '鉴权失败', 83 | 4: '服务器内部错误' 84 | } 85 | }), 86 | 87 | rate_limit_status: function(data, callback){ 88 | callback({error: _u.i18n("comm_no_api")}); 89 | }, 90 | reset_count: function(data, callback) { 91 | callback(); 92 | }, 93 | 94 | format_upload_params: function(user, data, pic , boundary) { 95 | 96 | }, 97 | 98 | url_encode: function(text) { 99 | return this.super_.url_encode(text); 100 | }, 101 | 102 | upload : function(data, pic, callback, context){ 103 | this.super_.upload.call(this, data, pic, callback, context); 104 | }, 105 | 106 | // 同时获取用户信息 user_show 107 | user_timeline: function(data, callback, context) { 108 | var both = this.combo(function(user_info_args, timeline_args) { 109 | var user_info = user_info_args[1] 110 | , timeline = timeline_args[1]; 111 | if(user_info && timeline) { 112 | timeline.user = user_info; 113 | } 114 | callback.apply(context, timeline_args); 115 | }); 116 | var user = data.user; 117 | var params = {name: data.id || data.screen_name, user: user}; 118 | this.user_show(params, both.add()); 119 | this.super_.user_timeline.call(this, data, both.add()); 120 | }, 121 | before_send_request: function(args, user) { 122 | if(args.play_load == 'string') { 123 | // oauth 124 | return; 125 | } 126 | args.data.format = 'json'; 127 | args.content_type = 'text/json'; 128 | }, 129 | 130 | format_result: function(data, play_load, args) { 131 | if(data.error){ 132 | return data; 133 | } 134 | var items = data.results || data.users || data; 135 | if(items instanceof Array) { 136 | for(var i in items) { 137 | items[i] = this.format_result_item(items[i], play_load, args); 138 | } 139 | } else { 140 | data = this.format_result_item(data, play_load, args); 141 | } 142 | if(args.url == this.config.search && data.next_page) { 143 | // "next_page":"?page=2&max_id=1291867917&q=fawave", 提取max_id 144 | var p = urlutil.parse(data.next_page, true).query; 145 | data.max_id = p.max_id; 146 | } 147 | return data; 148 | }, 149 | 150 | format_result_item: function(data, play_load, args, users) { 151 | if(play_load == 'user' && data && data.id) { 152 | data.t_url = 'http://t.sohu.com/people?uid=' + (data.domain || data.id); 153 | data.name = data.name ? data.name : data.screen_name; 154 | } else if(play_load == 'status') { 155 | if(!data.user) { // search data 156 | data.user = { 157 | name : data.from_user, 158 | screen_name: data.from_user, 159 | profile_image_url: data.profile_image_url, 160 | id: data.from_user_id 161 | }; 162 | delete data.profile_image_url; 163 | delete data.from_user; 164 | delete data.from_user_id; 165 | } 166 | this.format_result_item(data.user, 'user', args); 167 | 168 | if(data.retweeted_status) { 169 | data.retweeted_status = this.format_result_item(data.retweeted_status, 'status', args); 170 | } 171 | // 设置status的t_url 172 | // var tpl = this.config.host + '/{{user.id}}/statuses/{{id}}'; 173 | data.t_url = 'http://t.sohu.com/m/'+data.id; 174 | } else if(play_load == 'message') { 175 | this.format_result_item(data.sender, 'user', args); 176 | this.format_result_item(data.recipient, 'user', args); 177 | } else if(play_load == 'comment') { 178 | this.format_result_item(data.user, 'user', args); 179 | this.format_result_item(data.status, 'status', args); 180 | } 181 | return data; 182 | } 183 | }); 184 | 185 | })( (function(){ 186 | if(typeof exports === 'undefined') { 187 | window.weibo.tsohu = {}; 188 | return window.weibo.tsohu; 189 | } else { 190 | return exports; 191 | } 192 | })() ); -------------------------------------------------------------------------------- /lib/twitter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | (function(exports){ 6 | 7 | var TSinaAPI; 8 | if(typeof require !== 'undefined') { 9 | TSinaAPI = require('./tsina').TSinaAPI; 10 | } else { 11 | TSinaAPI = weibo.tsina.TSinaAPI; 12 | } 13 | 14 | /** 15 | * Twitter API 16 | */ 17 | var TwitterAPI = exports.TwitterAPI = Object.inherits({}, TSinaAPI, { 18 | config: Object.extend({}, TSinaAPI.config, { 19 | host: 'https://api.twitter.com', 20 | user_home_url: 'https://twitter.com/', 21 | search_url: 'https://twitter.com/search?q=', 22 | source: '', 23 | oauth_key: '', 24 | oauth_secret: '', 25 | repost_pre: 'RT', 26 | support_comment: false, 27 | support_do_comment: false, 28 | support_repost: false, 29 | support_upload: false, 30 | need_source: false, 31 | oauth_callback: 'oob', 32 | search: '/search_statuses', 33 | repost: '/statuses/update', 34 | retweet: '/statuses/retweet/{{id}}', 35 | favorites_create: '/favorites/create/{{id}}', 36 | friends_timeline: '/statuses/home_timeline', 37 | search: '/search' 38 | }), 39 | 40 | before_send_request: function(args) { 41 | if(args.url == this.config.repost) { 42 | if(args.data.id) { 43 | args.data.in_reply_to_status_id = args.data.id; 44 | delete args.data.id; 45 | } 46 | } else if(args.url == this.config.new_message) { 47 | // id => user 48 | args.data.user = args.data.id; 49 | delete args.data.id; 50 | } else if(args.url == this.config.search) { 51 | args.data.rpp = args.data.count; 52 | args.data.show_user = 'true'; 53 | delete args.data.count; 54 | delete args.data.source; 55 | } 56 | }, 57 | 58 | /** 59 | * Format result data 60 | * 61 | * @return {Object} depend on play_load 62 | * @api public 63 | */ 64 | format_result_item: function(data, play_load, args) { 65 | if(play_load == 'status' && data.id) { 66 | data.id = data.id_str || data.id; 67 | data.in_reply_to_status_id = data.in_reply_to_status_id_str || data.in_reply_to_status_id; 68 | var tpl = this.config.user_home_url + '{{user.screen_name}}/status/{{id}}'; 69 | this.format_result_item(data.user, 'user', args); 70 | if(data.retweeted_status) { 71 | data.retweeted_status.id = data.retweeted_status.id_str || data.retweeted_status.id; 72 | data.retweeted_status.t_url = tpl.format(data.retweeted_status); 73 | this.format_result_item(data.retweeted_status.user, 'user', args); 74 | } 75 | // search 76 | if(data.from_user && !data.user) { 77 | data.user = { 78 | id: data.from_user_id_str || data.from_user_id, 79 | profile_image_url: data.profile_image_url, 80 | screen_name: data.from_user 81 | }; 82 | delete data.from_user_id_str; 83 | delete data.profile_image_url; 84 | delete data.from_user; 85 | //data.source = htmldecode(data.source); 86 | } 87 | data.t_url = tpl.format(data); 88 | } else if(play_load == 'user' && data && data.id) { 89 | data.t_url = this.config.user_home_url + (data.screen_name || data.id); 90 | } 91 | return data; 92 | }, 93 | 94 | // 无需urlencode 95 | url_encode: function(text) { 96 | return text; 97 | } 98 | }); 99 | 100 | 101 | })( (function(){ 102 | if(typeof exports === 'undefined') { 103 | window.weibo.twitter = {}; 104 | return window.weibo.twitter; 105 | } else { 106 | return exports; 107 | } 108 | })() ); -------------------------------------------------------------------------------- /lib/urllib.js: -------------------------------------------------------------------------------- 1 | 2 | var Base64 = require('./base64'); 3 | var utils = require('./utils'); 4 | 5 | /** 6 | * Fixed JSON bad word, more detail see [JSON parse在各浏览器的兼容性列表](http://www.cnblogs.com/rubylouvre/archive/2011/02/12/1951760.html) 7 | * @type {String} 8 | * @const 9 | */ 10 | exports.RE_JSON_BAD_WORD = /[\u000B\u000C]/ig; 11 | 12 | /** 13 | * The default request timeout(in milliseconds) 14 | * @type {Object.} 15 | * @const 16 | */ 17 | exports.TIMEOUT = 60000; 18 | 19 | function format_args(args) { 20 | if (!args) { 21 | args = {}; 22 | } 23 | if (!args.timeout) { 24 | args.timeout = exports.TIMEOUT; 25 | } 26 | args.type = (args.type || 'GET').toUpperCase(); 27 | return args; 28 | } 29 | 30 | function format_result(args, data, response, callback, context) { 31 | var error = null; 32 | var status_code = 0; 33 | if (response) { 34 | status_code = response.status || response.statusCode; 35 | } 36 | if (status_code === 200 || status_code === 201) { 37 | if (args.dataType === 'json' && typeof data === 'string') { 38 | try { 39 | data = JSON.parse(data); 40 | } catch (e) { 41 | error = new Error('JSON format error'); 42 | error.name = 'JSONParseError'; 43 | error.data = data; 44 | error.status_code = error.statusCode = status_code; 45 | data = null; 46 | } 47 | } 48 | } else { 49 | error = data; 50 | if (typeof error === 'string') { 51 | try { 52 | error = JSON.parse(data); 53 | var err = new Error(); 54 | err.name = 'HTTPResponseError'; 55 | for (var k in error) { 56 | err[k] = error[k]; 57 | } 58 | if (!err.message) { 59 | err.message = error.error || data; 60 | } 61 | error = err; 62 | } catch (e) { 63 | error = new Error(data || 'status ' + status_code); 64 | error.name = 'JSONParseError'; 65 | } 66 | error.status_code = error.statusCode = status_code; 67 | } 68 | if (error) { 69 | error.status_code = error.statusCode = status_code; 70 | } 71 | data = null; 72 | } 73 | if (callback) { 74 | callback.call(context, error, data, response); 75 | } 76 | } 77 | 78 | var request; 79 | if (typeof require !== 'undefined') { 80 | request = require('urllib').request; 81 | } else { 82 | /** 83 | * 封装所有http请求,自动区分处理http和https 84 | * 85 | * @require jQuery 86 | * @param {String} url 87 | * @param {Object} args 88 | * - data: request data 89 | * - content: optional, if set content, `data` will ignore 90 | * - type: optional, could be GET | POST | DELETE | PUT, default is GET 91 | * - dataType: `text` or `json`, default is text 92 | * - processData: process data or not 93 | * - headers: http request headers 94 | * - timeout: request timeout, default is urllib.TIMEOUT(60 seconds) 95 | * @param {Function} callback 96 | * @param {Object} optional context of callback, callback.call(context, data, error, res) 97 | * @api public 98 | */ 99 | request = function (url, args, callback) { 100 | args = format_args(args); 101 | var processData = args.process_data || args.processData || true; 102 | if (args.content) { 103 | processData = false; 104 | args.data = args.content; 105 | } 106 | var dataType = args.dataType || 'text'; 107 | $.ajax({ 108 | url: url, 109 | type: args.type, 110 | headers: args.headers || {}, 111 | data: args.data, 112 | processData: processData, 113 | timeout: args.timeout, 114 | dataType: dataType, 115 | success: function (data, text_status, xhr) { 116 | callback(null, data, xhr); 117 | }, 118 | error: function (xhr, text_status, err) { 119 | if (!err) { 120 | err = new Error(text_status); 121 | err.name = 'AjaxRequestError'; 122 | } 123 | callback(err, null, xhr); 124 | } 125 | }); 126 | }; 127 | } 128 | 129 | exports.request = function (url, args, callback, context) { 130 | args = format_args(args); 131 | if (args.user && args.user.proxy) { 132 | if (args.type === 'GET' && args.data) { 133 | url = utils.urljoin(url, args.data); 134 | delete args.data; 135 | } 136 | url = args.user.proxy + '?url=' + encodeURIComponent(url); 137 | } 138 | request(url, args, function (err, data, res) { 139 | if (err) { 140 | return format_result(args, err, res, callback, context); 141 | } 142 | if (typeof Buffer !== 'undefined' && Buffer.isBuffer(data)) { 143 | data = data.toString(); 144 | } 145 | format_result(args, data, res, callback, context); 146 | }); 147 | }; 148 | 149 | /** 150 | * 生成HTTP Basic Authentication的字符串:"Base base64String" 151 | * 152 | * @param {String} user 153 | * @param {String} password 154 | * @return {String} 'Basic xxxxxxxxxxxxxxxx' 155 | * @api public 156 | */ 157 | exports.make_base_auth_header = function (user, password) { 158 | var token = user + ':' + password; 159 | var hash = Base64.encode(token); 160 | return "Basic " + hash; 161 | }; 162 | 163 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * node-weibo - lib/utils.js 3 | * Copyright(c) 2012 fengmk2 4 | * MIT Licensed 5 | */ 6 | 7 | "use strict"; 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var STRING_FORMAT_REGEX = exports.STRING_FORMAT_REGEX = /\{\{([\w\s\.\'\"\(\),-\[\]]+)?\}\}/g; 14 | 15 | /** 16 | * 格式化字符串 17 | * eg: 18 | * '{0}天有{1}个小时'.format([1, 24]) 19 | * or 20 | * '{{day}}天有{{hour}}个小时'.format({day:1, hour:24}}) 21 | * @param {Object} values 22 | */ 23 | 24 | /** 25 | * Simple string template helper. 26 | * 27 | * @param {String} s, template string 28 | * @param {Object|Function} values, template values or match callback. 29 | * @return {String} 30 | */ 31 | exports.format = function (s, values) { 32 | var cb; 33 | if (typeof values === 'function') { 34 | cb = values; 35 | } else { 36 | cb = function (match, key) { 37 | try { 38 | return values[key] || eval('(values.' + key + ')'); 39 | } catch (e) { 40 | console.log(JSON.stringify([s, values, key])); 41 | // return s; 42 | throw e; 43 | } 44 | }; 45 | } 46 | return s.replace(STRING_FORMAT_REGEX, cb); 47 | }; 48 | 49 | var b64_hmac_sha1 = require('./sha1').b64_hmac_sha1; 50 | var crypto = require('crypto'); 51 | 52 | var querystring = { 53 | parse: function (s) { 54 | var qs = {}; 55 | if (typeof s !== 'string') { 56 | return qs; 57 | } 58 | var pairs = s.split('&'); 59 | for (var i = 0, len = pairs.length; i < len; i++) { 60 | var pair = pairs[i].split('=', 2); 61 | if (pair.length !== 2) { 62 | continue; 63 | } 64 | var key = pair[0].trim(); 65 | if (!key) { 66 | continue; 67 | } 68 | qs[decodeURIComponent(key)] = decodeURIComponent(pair[1]); 69 | } 70 | return qs; 71 | }, 72 | stringify: function (data) { 73 | var pairs = []; 74 | data = data || {}; 75 | for (var k in data) { 76 | pairs.push(encodeURIComponent(k) + '=' + encodeURIComponent('' + data[k])); 77 | } 78 | return pairs.join('&'); 79 | } 80 | }; 81 | 82 | function urljoin(url, params) { 83 | if (typeof params === 'object') { 84 | params = querystring.stringify(params); 85 | } 86 | if (!params) { 87 | return url; 88 | } 89 | if (url.indexOf('?') < 0) { 90 | url += '?'; 91 | } else { 92 | url += '&'; 93 | } 94 | return url + params; 95 | } 96 | 97 | function base64HmacSha1(baseString, key) { 98 | if (b64_hmac_sha1) { 99 | return b64_hmac_sha1(key, baseString); 100 | } 101 | return new crypto.Hmac().init("sha1", key).update(baseString).digest("base64"); 102 | } 103 | 104 | // HTML 编码 105 | // test: hard code testing 。。。 '"!@#$%^&*()-=+ |][ {} ~` &&&&& < & C++ c++c + +c & 106 | function htmlencode(str) { 107 | if (!str) { return ''; } 108 | return str.replace(/&/g, '&').replace(//g, '>'); 109 | } 110 | 111 | exports.extend = function (destination) { 112 | for (var i = 1, len = arguments.length; i < len; i++) { 113 | var source = arguments[i]; 114 | if (!source) { 115 | continue; 116 | } 117 | for (var property in source) { 118 | destination[property] = source[property]; 119 | } 120 | } 121 | return destination; 122 | }; 123 | 124 | exports.querystring = querystring; 125 | exports.base64HmacSha1 = base64HmacSha1; 126 | exports.urljoin = urljoin; 127 | exports.htmlencode = htmlencode; 128 | 129 | var MIME_TYPES = { 130 | 'jpg': 'image/jpeg', 131 | 'jpeg': 'image/jpeg', 132 | 'gif': 'image/gif', 133 | 'png': 'image/png', 134 | 'bmp': 'image/bmp', 135 | }; 136 | 137 | var BIN_TYPE = 'application/octet-stream'; 138 | 139 | exports.mimeLookup = function (name, fallback) { 140 | var ext = name.replace(/.*[\.\/]/, '').toLowerCase(); 141 | return MIME_TYPES[ext] || fallback || BIN_TYPE; 142 | }; 143 | 144 | /** 145 | * Escape the given string of `html`. 146 | * 147 | * @param {String} html 148 | * @return {String} 149 | */ 150 | exports.escape = function (html) { 151 | return String(html) 152 | .replace(/&(?!\w+;)/g, '&') 153 | .replace(//g, '>') 155 | .replace(/"/g, '"'); 156 | }; 157 | 158 | /** 159 | * Remove all html tags. 160 | * 161 | * @param {String} s 162 | * @return {String} 163 | */ 164 | exports.removeHTML = function (s) { 165 | return s.replace(/(<.*?>)/ig, ''); 166 | }; 167 | 168 | function getChromeVersion() { 169 | var m = /Chrome\/(\d+)/i.exec(window.navigator.userAgent); 170 | if (m) { 171 | return m[1]; 172 | } 173 | return; 174 | } 175 | exports.getChromeVersion = getChromeVersion; 176 | 177 | function buildBlob(parts) { 178 | var blob = null; 179 | var version = parseInt(getChromeVersion() || 0, 10); 180 | // https://developer.mozilla.org/en/DOM/Blob 181 | // Chrome 20+ support Blob() constructor 182 | if (version >= 20) { 183 | blob = new window.Blob(parts); 184 | } else { 185 | if (typeof window.BlobBuilder === 'undefined') { 186 | window.BlobBuilder = window.WebKitBlobBuilder; 187 | } 188 | var bb = new window.BlobBuilder(); 189 | for (var i = 0; i < parts.length; i++) { 190 | bb.append(parts[i]); 191 | } 192 | blob = bb.getBlob(); 193 | } 194 | return blob; 195 | } 196 | exports.buildBlob = buildBlob; 197 | 198 | exports.xhrProvider = function (onprogress) { 199 | return function () { 200 | var xhr = jQuery.ajaxSettings.xhr(); 201 | if (onprogress && xhr.upload) { 202 | xhr.upload.addEventListener('progress', onprogress, false); 203 | } 204 | return xhr; 205 | }; 206 | }; 207 | 208 | exports.build_upload_params = function (data, pic) { 209 | pic.keyname = pic.keyname || 'file'; 210 | var boundary = '----multipartformboundary' + Date.now(); 211 | var dashdash = '--'; 212 | var crlf = '\r\n'; 213 | 214 | /* Build RFC2388 string. */ 215 | var builder = ''; 216 | 217 | builder += dashdash; 218 | builder += boundary; 219 | builder += crlf; 220 | 221 | for (var key in data) { 222 | var value = encodeURIComponent(data[key]); 223 | /* Generate headers. key */ 224 | builder += 'Content-Disposition: form-data; name="' + key + '"'; 225 | builder += crlf; 226 | builder += crlf; 227 | /* Append form data. */ 228 | builder += value; 229 | builder += crlf; 230 | 231 | /* Write boundary. */ 232 | builder += dashdash; 233 | builder += boundary; 234 | builder += crlf; 235 | } 236 | /* Generate headers. [PIC] */ 237 | builder += 'Content-Disposition: form-data; name="' + pic.keyname + '"'; 238 | var filename = pic.fileName || pic.name; 239 | if (filename) { 240 | builder += '; filename="' + filename + '"'; 241 | } 242 | builder += crlf; 243 | var contentType = pic.fileType || pic.type; 244 | builder += 'Content-Type: '+ contentType; 245 | builder += crlf; 246 | builder += crlf; 247 | var parts = []; 248 | parts.push(builder); 249 | parts.push(pic); 250 | 251 | builder = crlf; 252 | /* Mark end of the request.*/ 253 | builder += dashdash; 254 | builder += boundary; 255 | builder += dashdash; 256 | builder += crlf; 257 | parts.push(builder); 258 | 259 | var blob = buildBlob(parts); 260 | blob.contentType = 'multipart/form-data; boundary=' + boundary; 261 | return blob; 262 | }; 263 | -------------------------------------------------------------------------------- /lib/weibo.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * node-weibo - lib/weibo.js 3 | * Copyright(c) 2012 fengmk2 4 | * MIT Licensed 5 | */ 6 | 7 | "use strict"; 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var TBaseOauthV2 = require('./tbase_oauth_v2'); 14 | var inherits = require('util').inherits; 15 | var utils = require('./utils'); 16 | var weiboutil = require('./weibo_util'); 17 | 18 | 19 | function WeiboAPI(options) { 20 | WeiboAPI.super_.call(this); 21 | 22 | this.blogtype = 'weibo'; 23 | 24 | var config = utils.extend({}, options, { 25 | host: 'https://api.weibo.com/2', 26 | user_home_url: 'http://weibo.com/n/', 27 | search_url: 'http://s.weibo.com/weibo/', 28 | 29 | oauth_host: 'https://api.weibo.com/oauth2', 30 | oauth_authorize: '/authorize', 31 | oauth_access_token: '/access_token', 32 | verify_credentials: '/users/show', 33 | 34 | comments: '/comments/show', 35 | comment_create: '/comments/create', 36 | comment_reply: '/comments/reply', 37 | comment_destroy: '/comments/destroy', 38 | 39 | support_search: false, 40 | support_user_search: false, 41 | support_direct_messages_both: false, 42 | support_direct_messages: false, 43 | support_direct_messages_sent: false, 44 | support_direct_message_create: false, 45 | support_direct_message_destroy: false, 46 | 47 | }); 48 | 49 | this.init(config); 50 | } 51 | 52 | inherits(WeiboAPI, TBaseOauthV2); 53 | module.exports = WeiboAPI; 54 | 55 | /** 56 | * Result getters 57 | */ 58 | 59 | WeiboAPI.prototype.get_result_items = function (data, playload, args) { 60 | return data.statuses || data.comments || data.reposts || 61 | data.messages || data.favorites || data; 62 | }; 63 | 64 | /** 65 | * Result formatters 66 | */ 67 | 68 | WeiboAPI.prototype.format_search_status = function (status, args) { 69 | return status; 70 | }; 71 | 72 | WeiboAPI.prototype.format_status = function (status, args) { 73 | status.id = status.idstr; 74 | status.created_at = new Date(status.created_at); 75 | if (status.user) { 76 | status.user = this.format_user(status.user, args); 77 | status.t_url = 'http://weibo.com/' + status.user.id + '/' + weiboutil.mid2url(status.mid); 78 | } 79 | 80 | // geo: { type: 'Point', coordinates: [ 22.354231, 113.421234 ] } latitude, longitude 81 | if (status.geo && status.geo.type === 'Point' && status.geo.coordinates) { 82 | var geo = { 83 | latitude: String(status.geo.coordinates[0]), 84 | longitude: String(status.geo.coordinates[1]), 85 | }; 86 | status.geo = geo; 87 | } 88 | 89 | if (status.retweeted_status) { 90 | status.retweeted_status = this.format_status(status.retweeted_status, args); 91 | if (!status.retweeted_status.t_url) { 92 | status.retweeted_status.t_url = 93 | 'http://weibo.com/' + status.user.id + '/' + weiboutil.mid2url(status.retweeted_status.mid); 94 | } 95 | } 96 | return status; 97 | }; 98 | 99 | WeiboAPI.prototype.format_user = function (user, args) { 100 | user.id = user.idstr; 101 | user.created_at = new Date(user.created_at); 102 | user.t_url = 'http://weibo.com/' + (user.domain || user.id); 103 | if (user.status) { 104 | user.status = this.format_status(user.status, args); 105 | if (!user.status.t_url) { 106 | user.status.t_url = user.t_url + '/' + weiboutil.mid2url(user.status.mid || user.status.id); 107 | } 108 | } 109 | return user; 110 | }; 111 | 112 | WeiboAPI.prototype.format_comment = function (comment, args) { 113 | comment.id = comment.idstr; 114 | comment.created_at = new Date(comment.created_at); 115 | if (comment.user) { 116 | comment.user = this.format_user(comment.user, args); 117 | } 118 | if (comment.status) { 119 | comment.status = this.format_status(comment.status, args); 120 | } 121 | if (comment.reply_comment) { 122 | comment.reply_comment = this.format_comment(comment.reply_comment, args); 123 | } 124 | return comment; 125 | }; 126 | 127 | WeiboAPI.prototype.format_message = function (message, args) { 128 | return message; 129 | }; 130 | 131 | WeiboAPI.prototype.format_emotion = function (emotion, args) { 132 | return emotion; 133 | }; 134 | 135 | WeiboAPI.prototype.format_count = function (count, args) { 136 | count.id = String(count.id); 137 | return count; 138 | }; 139 | 140 | WeiboAPI.prototype.format_favorite = function (favorite, args) { 141 | favorite.status = this.format_status(favorite.status); 142 | favorite.created_at = new Date(favorite.favorited_time); 143 | delete favorite.favorited_time; 144 | return favorite; 145 | }; 146 | 147 | /** 148 | * User 149 | */ 150 | 151 | WeiboAPI.prototype.verify_credentials = function (user, callback) { 152 | var uid = user.uid || user.id; 153 | return this.user_show(user, uid, null, callback); 154 | }; 155 | 156 | /** 157 | * Comment 158 | */ 159 | 160 | WeiboAPI.prototype.comment_reply = function (user, cid, id, comment, callback) { 161 | if (comment.without_mention === undefined || comment.without_mention === null) { 162 | // dont auto add reply@user text to reply comment. 163 | comment.without_mention = '1'; 164 | } 165 | return WeiboAPI.super_.prototype.comment_reply.call(this, user, cid, id, comment, callback); 166 | }; 167 | -------------------------------------------------------------------------------- /lib/weibo_util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 新浪微博mid与url互转实用工具 3 | * 作者: XiNGRZ (http://weibo.com/xingrz) 4 | */ 5 | 6 | var WeiboUtil = module.exports = { 7 | // 62进制字典 8 | str62keys: [ 9 | "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", 10 | "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", 11 | "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" 12 | ], 13 | }; 14 | 15 | /** 16 | * 62进制值转换为10进制 17 | * @param {String} str62 62进制值 18 | * @return {String} 10进制值 19 | */ 20 | WeiboUtil.str62to10 = function (str62) { 21 | var i10 = 0; 22 | for (var i = 0; i < str62.length; i++) { 23 | var n = str62.length - i - 1; 24 | var s = str62[i]; 25 | i10 += this.str62keys.indexOf(s) * Math.pow(62, n); 26 | } 27 | return i10; 28 | }; 29 | 30 | /** 31 | * 10进制值转换为62进制 32 | * @param {String} int10 10进制值 33 | * @return {String} 62进制值 34 | */ 35 | WeiboUtil.int10to62 = function (int10) { 36 | var s62 = ''; 37 | var r = 0; 38 | while (int10 !== 0 && s62.length < 100) { 39 | r = int10 % 62; 40 | s62 = this.str62keys[r] + s62; 41 | int10 = Math.floor(int10 / 62); 42 | } 43 | return s62; 44 | }; 45 | 46 | /** 47 | * URL字符转换为mid 48 | * @param {String} url 微博URL字符,如 "wr4mOFqpbO" 49 | * @return {String} 微博mid,如 "201110410216293360" 50 | */ 51 | WeiboUtil.url2mid = function (url) { 52 | var mid = ''; 53 | //从最后往前以4字节为一组读取URL字符 54 | for (var i = url.length - 4; i > -4; i = i - 4) { 55 | var offset1 = i < 0 ? 0 : i; 56 | var offset2 = i + 4; 57 | var str = url.substring(offset1, offset2); 58 | 59 | str = this.str62to10(str); 60 | if (offset1 > 0) { 61 | //若不是第一组,则不足7位补0 62 | while (str.length < 7) { 63 | str = '0' + str; 64 | } 65 | } 66 | 67 | mid = str + mid; 68 | } 69 | 70 | return mid; 71 | }; 72 | 73 | /** 74 | * mid转换为URL字符 75 | * @param {String} mid 微博mid,如 "201110410216293360" 76 | * @return {String} 微博URL字符,如 "wr4mOFqpbO" 77 | */ 78 | WeiboUtil.mid2url = function (mid) { 79 | if (!mid) { 80 | return mid; 81 | } 82 | mid = String(mid); //mid数值较大,必须为字符串! 83 | if (!/^\d+$/.test(mid)) { return mid; } 84 | var url = ''; 85 | // 从最后往前以7字节为一组读取mid 86 | for (var i = mid.length - 7; i > -7; i = i - 7) { 87 | var offset1 = i < 0 ? 0 : i; 88 | var offset2 = i + 7; 89 | var num = mid.substring(offset1, offset2); 90 | 91 | num = this.int10to62(num); 92 | url = num + url; 93 | } 94 | return url; 95 | }; -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-modules/weibo/6c5c713f4d56107ed6b1a5c3d1d8ec7b2c21a00b/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "weibo", 3 | "version": "0.6.11", 4 | "description": "Weibo SDK, base on node. Now support weibo, tqq, tsohu, twitter and github.", 5 | "repository": "git://github.com/fengmk2/node-weibo.git", 6 | "homepage": "http://github.com/fengmk2/node-weibo", 7 | "author": "fengmk2 (http://fengmk2.github.com)", 8 | "main": "index", 9 | "scripts": { 10 | "test": "make test-g G='tqq API'" 11 | }, 12 | "devDependencies": { 13 | "browserify": ">=1.16.1", 14 | "domready": ">=0.2.11", 15 | "connect": ">=1.8.0", 16 | "jscover": "*", 17 | "should": "*", 18 | "mocha": "*", 19 | "contributors": "*" 20 | }, 21 | "dependencies": { 22 | "eventproxy": "0.2.6", 23 | "emoji": ">=0.2.1", 24 | "urllib": ">=0.5.5" 25 | }, 26 | "keywords": [ 27 | "framework", "web", "rest", "restful", 28 | "weibo", "qq", "open", "github", "twitter", "facebook", 29 | "sohu", "163", "sina", "oauth", 30 | "sdk" 31 | ], 32 | "engines": { "node": ">= 0.8.0" } 33 | } 34 | -------------------------------------------------------------------------------- /test/base64.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var libpath = process.env.WEIBO_COV ? '../lib-cov' : '../lib'; 6 | var base64 = require(libpath + '/base64'); 7 | 8 | describe('base64.js', function () { 9 | 10 | var cases = [ 11 | 'foo', 12 | '哈哈1239!@#!@¥!¥!@¥!@#!¥ 中文字幕', 13 | '哦w额u热wrjlw而wljr哦wj萨fhlsfjs我是的哦啊呸留学生;蓄势待发哦w二坡第五大文排版', 14 | 'XX你好啊!dawa\';:\"/?.>?》!@#!¥%⋯⋯—*(}{"' 15 | ]; 16 | 17 | it('should encode right', function () { 18 | cases.forEach(function (word) { 19 | base64.encode(word).should.equal(new Buffer(word).toString('base64')); 20 | }); 21 | }); 22 | 23 | it('should decode right', function () { 24 | cases.forEach(function (word) { 25 | new Buffer(base64.encode(word), 'base64').toString().should.equal(word); 26 | }); 27 | }); 28 | 29 | }); 30 | 31 | -------------------------------------------------------------------------------- /test/browser/entry.js: -------------------------------------------------------------------------------- 1 | var weibo = require('../../'); 2 | var domready = require('domready'); 3 | 4 | domready(function () { 5 | var blogtypes = [ 6 | 'weibo', 'tqq', 'github', 'twitter' 7 | ]; 8 | var $console = $('#console'); 9 | var html = $console.html(); 10 | for (var i = 0; i < blogtypes.length; i++) { 11 | var blogtype = blogtypes[i]; 12 | var api = weibo.TYPES[blogtype]; 13 | if (!api) { 14 | html += '
  • ' + blogtype + ' error.
  • '; 15 | } else { 16 | html += '
  • ' + blogtype + ' success.
  • '; 17 | } 18 | } 19 | $console.html(html); 20 | }); -------------------------------------------------------------------------------- /test/browser/weibo_browser_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Weibo SDK test 4 | 5 | 6 | 7 | 8 | 9 |
    10 |
    Weibo SDK test
    11 |
      12 |
      13 | 14 | -------------------------------------------------------------------------------- /test/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var libpath = process.env.WEIBO_COV ? '../lib-cov' : '../lib'; 6 | var weibo = require(libpath + '/tapi'); 7 | 8 | var oauth_callback = 'http://nodeweibo.org/oauth/callback'; 9 | 10 | // init appkey 11 | weibo.init('tqq', '801196838', '9f1a88caa8709de7dccbe3cae4bdc962', 'oob'); 12 | weibo.init('weibo', '1122960051', 'e678e06f627ffe0e60e2ba48abe3a1e3', oauth_callback); 13 | weibo.init('github', '8e14edfda73a71f1f226', 'e678e06f627ffe0e60e2ba48abe3a1e3', oauth_callback); 14 | 15 | // weibo.init('twitter', 'i1aAkHo2GkZRWbUOQe8zA', 'MCskw4dW5dhWAYKGl3laRVTLzT8jTonOIOpmzEY', 'oob'); 16 | 17 | var users = exports.users = { 18 | tqq: { 19 | blogtype: 'tqq', 20 | oauth_token: '2d746f8c91ae4baea7243a6867cf309f', 21 | oauth_token_secret: '2bec75e9ddad6b27067e384a84550e38', 22 | name: 'node-weibo' 23 | }, 24 | // weibo: { 25 | // blogtype: 'weibo', 26 | // access_token: '2.00EkofzBtMpzNBb9bc3108d8MwDTTE', 27 | // uid: 1827455832, 28 | // }, 29 | // github: { 30 | // blogtype: 'github', 31 | // access_token: '23c53f2f70977a0a2c15c77c840c2a65247f9299', 32 | // } 33 | }; 34 | 35 | // var user = users.tqq; 36 | var user = users.weibo; 37 | // var user = users.github; 38 | // weibo.get_authorization_url(user, function (err, auth_info) { 39 | // console.log(err, auth_info); 40 | // }); 41 | 42 | // {"access_token":"2.00EkofzBtMpzNBb9bc3108d8MwDTTE","remind_in":"633971","expires_in":633971,"uid":"1827455832"}' 43 | // 44 | // http://localhost.nodeweibo.com/oauth/callback?code=8c3ef76abed0eeb0c789f5fc56b568f8 45 | // http://open.t.qq.com/cgi-bin/oob?oauth_token=86e8a48c83904d918cad0d513d4bf99d&oauth_verifier=796840&openid=EA68676D5E9DA465822CD0CEB2DC6EF5&openkey=1E7DE375708D08ECCB665ACB0362BD05 46 | // http://localhost.nodeweibo.com:8088/oauth/callback?code=70c616ef260be1304e12&state=1348901925953 47 | // weibo.get_access_token({ 48 | // blogtype: user.blogtype, 49 | // oauth_verifier: '168c90094e41d2ec882504a45ba0caeb', 50 | // // state: '1348901925953', 51 | // }, function (err, auth_user) { 52 | // console.log(err, auth_user); 53 | // }); 54 | 55 | // weibo.user_timeline(user, function (err, data) { 56 | // console.log(data.items[0]); 57 | // }); 58 | 59 | -------------------------------------------------------------------------------- /test/mk2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-modules/weibo/6c5c713f4d56107ed6b1a5c3d1d8ec7b2c21a00b/test/mk2.jpg -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require should 2 | --require test/config.js -------------------------------------------------------------------------------- /test/oauth.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * node-weibo - oauth test 3 | * Copyright(c) 2012 fengmk2 4 | * MIT Licensed 5 | */ 6 | 7 | /** 8 | * Module dependencies. 9 | */ 10 | 11 | var libpath = process.env.WEIBO_COV ? '../lib-cov' : '../lib'; 12 | var oauth = require(libpath + '/oauth'); 13 | 14 | describe('oauth test', function () { 15 | 16 | it('should return second timestamp', function () { 17 | oauth.timestamp().toString().should.length((new Date().getTime() / 1000).toFixed(0).length); 18 | }); 19 | 20 | it('should return nonce', function () { 21 | for (var i = 0; i < 100; i++) { 22 | oauth.nonce(i).should.length(i); 23 | } 24 | oauth.nonce().should.length(0); 25 | }); 26 | 27 | }); -------------------------------------------------------------------------------- /test/proxy.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var urlparse = require('url').parse; 3 | 4 | http.createServer(function (req, res) { 5 | var url = urlparse(req.url, true).query.url; 6 | if (!url) { 7 | return res.end(req.method + ': ' + req.url); 8 | } 9 | var target = urlparse(url); 10 | var headers = {}; 11 | for (var k in req.headers) { 12 | if (k === 'host' || k === 'connection') { 13 | continue; 14 | } 15 | headers[k] = req.headers[k]; 16 | } 17 | var options = { 18 | host: target.hostname, 19 | port: target.port || 80, 20 | path: target.path, 21 | method: req.method, 22 | headers: headers 23 | }; 24 | 25 | var proxyReq = http.request(options, function (response) { 26 | res.writeHead(response.statusCode, response.headers); 27 | response.on('data', function (chunk) { 28 | res.write(chunk); 29 | }); 30 | response.on('end', function () { 31 | res.end(); 32 | }); 33 | }); 34 | 35 | proxyReq.on('error', function (err) { 36 | proxyReq.abort(); 37 | res.writeHead(500); 38 | res.end(url + ' error: ' + err.message); 39 | }); 40 | 41 | req.on('data', function (chunk) { 42 | // console.log('data', chunk.toString()); 43 | proxyReq.write(chunk); 44 | }); 45 | req.on('end', function () { 46 | proxyReq.end(); 47 | }); 48 | }).listen(37456); -------------------------------------------------------------------------------- /test/snake.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-modules/weibo/6c5c713f4d56107ed6b1a5c3d1d8ec7b2c21a00b/test/snake.jpg -------------------------------------------------------------------------------- /test/tqq_text_process.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * node-weibo - test/weibo_text_process.js 3 | * Copyright(c) 2012 fengmk2 4 | * MIT Licensed 5 | */ 6 | 7 | "use strict"; 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var libpath = process.env.WEIBO_COV ? '../lib-cov' : '../lib'; 14 | var TQQAPI = require(libpath + '/tqq'); 15 | 16 | describe('tqq_text_process.js', function () { 17 | 18 | var tqq = new TQQAPI(); 19 | 20 | describe('process_at()', function () { 21 | 22 | it('should handle process tqq at user', function () { 23 | 24 | var users = { 25 | 'Foreverdream-sky': '唯美-English', 26 | 'I-Capricorn': '摩羯-座丶心理', 27 | QQphoto: '腾讯图片', 28 | 'i-loveangel': '天使爱丶美丽', 29 | i1ii18: '海绵宝宝精选', 30 | iamtiejianxia: '铁肩侠', 31 | kds: '', 32 | lanjingr: '爱美女潮这看', 33 | linyiwangren: '临沂网', 34 | meilishuo: '美丽说', 35 | mijiuClub: '米九', 36 | qlwbyw: '齐鲁晚报', 37 | sdTeChan: '爆笑女神', 38 | sdnews: '鲁网', 39 | tjTeChan: '弗洛伊德行为心理学', 40 | v5boos: '幸福心理学', 41 | vip445: '热门搞笑排行榜', 42 | vip489: '搞笑排行榜', 43 | wesc: '四川微新闻', 44 | yumcea: '狮子座专属' 45 | }; 46 | var cases = [ 47 | ['@Foreverdream-sky', '@唯美-English(@Foreverdream-sky)'], 48 | ['@mk2', '@mk2'], 49 | ['你好@Foreverdream-sky 我是@vip489', 50 | '你好@唯美-English(@Foreverdream-sky) 我是@搞笑排行榜(@vip489)'], 51 | ['#@Foreverdream-sky', '#@唯美-English(@Foreverdream-sky)'], 52 | ]; 53 | cases.forEach(function (item) { 54 | tqq.process_at(item[0], users).should.equal(item[1]); 55 | }); 56 | }); 57 | 58 | }); 59 | 60 | describe('process_search()', function () { 61 | 62 | xit('should convert #hash tag# to search url', function () { 63 | var cases = [ 64 | ['#沪js#', '#沪js#'], 65 | ['#沪js##123#', '#沪js##123#'], 66 | ['#foo bar 123#123123', '#foo bar 123#123123'], 67 | ]; 68 | cases.forEach(function (item) { 69 | weiboapi.process_search(item[0]).should.equal(item[1]); 70 | }); 71 | }); 72 | 73 | }); 74 | 75 | describe('process_text()', function () { 76 | 77 | xit('should process url, @user, #hash#', function () { 78 | var cases = [ 79 | ['http://t.cn/zlcThPG @user#沪js#', 'http://t.cn/zlcThPG @user#沪js#'], 80 | ['#沪js##123#www.baidu.com', '#沪js##123#www.baidu.com'], 81 | ['#foo bar 123#123123', '#foo bar 123#123123'], 82 | ['', ' '], 83 | [null, ' '], 84 | [undefined, ' '], 85 | ]; 86 | cases.forEach(function (item) { 87 | weibo.process_text({blogtype: 'weibo'}, {text: item[0]}).should.equal(item[1]); 88 | }); 89 | }); 90 | 91 | }); 92 | 93 | }); 94 | -------------------------------------------------------------------------------- /test/tsina_public_timeline.json: -------------------------------------------------------------------------------- 1 | [{"created_at":"Sat May 26 18:11:15 +0800 2012","id":3449997960257406,"text":"我获得微身份勋章啦!我是个有身份的人了,你也快点来领取吧。咱们都是靠谱的人!http://weibo.com/z/vshenfen/","source":"专题","favorited":false,"truncated":false,"in_reply_to_status_id":"","in_reply_to_user_id":"","in_reply_to_screen_name":"","thumbnail_pic":"http://ww1.sinaimg.cn/thumbnail/6769a1d2jw1dpykku6qdhj.jpg","bmiddle_pic":"http://ww1.sinaimg.cn/bmiddle/6769a1d2jw1dpykku6qdhj.jpg","original_pic":"http://ww1.sinaimg.cn/large/6769a1d2jw1dpykku6qdhj.jpg","geo":null,"mid":"3449997960257406","user":{"id":1870721923,"screen_name":"邬程程_Cc","name":"邬程程_Cc","province":"44","city":"20","location":"广东 中山","description":"为什么不去试试,一切皆有可能!","url":"http://123.lov2cheng.blog.163.com","profile_image_url":"http://tp4.sinaimg.cn/1870721923/50/5625484329/0","domain":"","gender":"f","followers_count":296,"friends_count":152,"statuses_count":560,"favourites_count":0,"created_at":"Mon Nov 22 00:00:00 +0800 2010","following":false,"allow_all_act_msg":false,"geo_enabled":true,"verified":false},"annotations":[{"id":1034,"appid":109,"name":"微身份","title":"微身份","url":"http://weibo.com/z/vshenfen/index.html","skey":"","detailid":"vshenfen/index.html"}]},{"created_at":"Sat May 26 18:11:14 +0800 2012","id":3449997960257458,"text":"少林武术学校很多,这是其中一个,有点军训的味道.","source":"小米手机","favorited":false,"truncated":false,"in_reply_to_status_id":"","in_reply_to_user_id":"","in_reply_to_screen_name":"","thumbnail_pic":"http://ww4.sinaimg.cn/thumbnail/489a4d35jw1dtbvwjjy58j.jpg","bmiddle_pic":"http://ww4.sinaimg.cn/bmiddle/489a4d35jw1dtbvwjjy58j.jpg","original_pic":"http://ww4.sinaimg.cn/large/489a4d35jw1dtbvwjjy58j.jpg","geo":null,"mid":"3449997960257458","user":{"id":1218071861,"screen_name":"听海逐浪","name":"听海逐浪","province":"44","city":"1","location":"广东 广州","description":"有朋自远方来,不亦乐乎。","url":"","profile_image_url":"http://tp2.sinaimg.cn/1218071861/50/1283563774/1","domain":"cnnoon","gender":"m","followers_count":171,"friends_count":127,"statuses_count":390,"favourites_count":13,"created_at":"Sat Aug 28 09:37:16 +0800 2010","following":false,"allow_all_act_msg":false,"geo_enabled":true,"verified":false}},{"created_at":"Sat May 26 18:11:14 +0800 2012","id":3449997960257479,"text":"[泪][泪][泪][泪][泪][泪][泪][泪][泪][泪][泪][泪][泪][泪][泪][泪][泪][泪][泪][泪]","source":"HTC微客","favorited":false,"truncated":false,"in_reply_to_status_id":"","in_reply_to_user_id":"","in_reply_to_screen_name":"","geo":null,"mid":"3449997960257479","user":{"id":2509987232,"screen_name":"rainbow微笑2","name":"rainbow微笑2","province":"44","city":"1","location":"广东 广州","description":"","url":"","profile_image_url":"http://tp1.sinaimg.cn/2509987232/50/5615649991/0","domain":"","gender":"f","followers_count":36,"friends_count":86,"statuses_count":269,"favourites_count":12,"created_at":"Tue Nov 08 00:00:00 +0800 2011","following":false,"allow_all_act_msg":false,"geo_enabled":true,"verified":false}},{"created_at":"Sat May 26 18:11:14 +0800 2012","id":3449997963574558,"text":"次肉次肉 #WeicoPintu#","source":"Weico.iPhone版","favorited":false,"truncated":false,"in_reply_to_status_id":"","in_reply_to_user_id":"","in_reply_to_screen_name":"","thumbnail_pic":"http://ww3.sinaimg.cn/thumbnail/7763639bjw1dtbvwjuvj4j.jpg","bmiddle_pic":"http://ww3.sinaimg.cn/bmiddle/7763639bjw1dtbvwjuvj4j.jpg","original_pic":"http://ww3.sinaimg.cn/large/7763639bjw1dtbvwjuvj4j.jpg","geo":null,"mid":"3449997963574558","user":{"id":2003002267,"screen_name":"jUnEJuNe615","name":"jUnEJuNe615","province":"400","city":"5","location":"海外 加拿大","description":"小妇人","url":"","profile_image_url":"http://tp4.sinaimg.cn/2003002267/50/5604276364/0","domain":"nuinuiyan","gender":"f","followers_count":198,"friends_count":197,"statuses_count":693,"favourites_count":236,"created_at":"Thu Mar 03 00:00:00 +0800 2011","following":false,"allow_all_act_msg":false,"geo_enabled":true,"verified":false},"annotations":[{"device":"WeicoLomo|Original"}]},{"created_at":"Sat May 26 18:11:15 +0800 2012","id":3449997964247246,"text":"积极思考造成积极人生,消极思考造成消极人生。","source":"iPhone客户端","favorited":false,"truncated":false,"in_reply_to_status_id":"","in_reply_to_user_id":"","in_reply_to_screen_name":"","geo":null,"mid":"3449997964247246","user":{"id":2618597392,"screen_name":"xa5p","name":"xa5p","province":"32","city":"5","location":"江苏 苏州","description":"","url":"","profile_image_url":"http://tp1.sinaimg.cn/2618597392/50/5619256555/1","domain":"","gender":"m","followers_count":1385,"friends_count":81,"statuses_count":1074,"favourites_count":0,"created_at":"Mon Dec 19 00:00:00 +0800 2011","following":false,"allow_all_act_msg":false,"geo_enabled":true,"verified":false}},{"created_at":"Sat May 26 18:11:13 +0800 2012","id":3449997964247253,"text":"窑洞避暑有木有!@Gentleman_EMON先森 @饭饭饭饭仔XXX @Angel新兰_ @新闻夜总汇文静","source":"UC浏览器","favorited":false,"truncated":false,"in_reply_to_status_id":"","in_reply_to_user_id":"","in_reply_to_screen_name":"","thumbnail_pic":"http://ww1.sinaimg.cn/thumbnail/73340175jw1dtbvwjpsr6j.jpg","bmiddle_pic":"http://ww1.sinaimg.cn/bmiddle/73340175jw1dtbvwjpsr6j.jpg","original_pic":"http://ww1.sinaimg.cn/large/73340175jw1dtbvwjpsr6j.jpg","geo":null,"mid":"3449997964247253","user":{"id":1932788085,"screen_name":"名侦探柯南狂","name":"名侦探柯南狂","province":"400","city":"15","location":"海外 日本","description":"柯南狂官方微博欢迎柯迷们私信投稿和合作!喜欢柯南的你 never go alone~","url":"","profile_image_url":"http://tp2.sinaimg.cn/1932788085/50/5602598841/1","domain":"","gender":"m","followers_count":14261,"friends_count":325,"statuses_count":3474,"favourites_count":73,"created_at":"Mon Jan 24 00:00:00 +0800 2011","following":false,"allow_all_act_msg":true,"geo_enabled":true,"verified":false}},{"created_at":"Sat May 26 18:11:15 +0800 2012","id":3449997964247314,"text":"你又愿意让我一个人,一个人在人海浮沉,承受这世界的残忍。","source":"Android客户端","favorited":false,"truncated":false,"in_reply_to_status_id":"","in_reply_to_user_id":"","in_reply_to_screen_name":"","thumbnail_pic":"http://ww1.sinaimg.cn/thumbnail/630d9e7djw1dtbvwk0wu1j.jpg","bmiddle_pic":"http://ww1.sinaimg.cn/bmiddle/630d9e7djw1dtbvwk0wu1j.jpg","original_pic":"http://ww1.sinaimg.cn/large/630d9e7djw1dtbvwk0wu1j.jpg","geo":null,"mid":"3449997964247314","user":{"id":1661836925,"screen_name":"Miss-Lee李小姐","name":"Miss-Lee李小姐","province":"44","city":"13","location":"广东 惠州","description":"爱你 痛彻我心扉。","url":"http://happyending.yan.blog.163.com/","profile_image_url":"http://tp2.sinaimg.cn/1661836925/50/5618991300/0","domain":"prayforuyan","gender":"f","followers_count":243,"friends_count":151,"statuses_count":1323,"favourites_count":1049,"created_at":"Wed Nov 18 16:23:14 +0800 2009","following":false,"allow_all_act_msg":false,"geo_enabled":true,"verified":false}},{"created_at":"Sat May 26 18:11:14 +0800 2012","id":3449997964247321,"text":"曾经,以为爱一个人就是一生一世,牵他的的手,一起老去。后来,终于在眼泪中懂得,爱若烟花,刹那芳菲,太真太美又太短暂,来不及眨眼,泪就落下了。这一生,两情相悦的人总是太少。这些年来,不敢让自己去想太多,沾染太多,只是让自己沉浸在忧伤的国度中,自己舔舐伤口,慰藉疗伤。","source":"Android客户端","favorited":false,"truncated":false,"in_reply_to_status_id":"","in_reply_to_user_id":"","in_reply_to_screen_name":"","geo":null,"mid":"3449997964247321","user":{"id":2525288512,"screen_name":"何必如此任性--家琪","name":"何必如此任性--家琪","province":"44","city":"1","location":"广东 广州","description":"榄核②中。。Ⅲ②班噶小孩童。。现读農校,①①届,会计①班噶小孩,不一定互粉,只关注有兴趣的。。","url":"","profile_image_url":"http://tp1.sinaimg.cn/2525288512/50/5632922583/0","domain":"","gender":"f","followers_count":132,"friends_count":100,"statuses_count":169,"favourites_count":2,"created_at":"Thu Nov 10 11:04:37 +0800 2011","following":false,"allow_all_act_msg":false,"geo_enabled":true,"verified":false}},{"created_at":"Sat May 26 18:11:15 +0800 2012","id":3449997964247324,"text":"3449997964451959","source":"微游戏","favorited":false,"truncated":false,"in_reply_to_status_id":"","in_reply_to_user_id":"","in_reply_to_screen_name":"","geo":null,"mid":"3449997964247324","user":{"id":1805306667,"screen_name":"懶小毛","name":"懶小毛","province":"44","city":"1","location":"广东 广州","description":"我就是我,我還是我.。 人生那麼長,我選擇開心的過~","url":"","profile_image_url":"http://tp4.sinaimg.cn/1805306667/50/5628063635/0","domain":"momo7o","gender":"f","followers_count":200,"friends_count":195,"statuses_count":799,"favourites_count":82,"created_at":"Tue Aug 31 00:00:00 +0800 2010","following":false,"allow_all_act_msg":false,"geo_enabled":true,"verified":false}},{"created_at":"Sat May 26 18:11:15 +0800 2012","id":3449997964247334,"text":"【人性中最美好的品德是纯善】人生最悲哀的感受莫过于\"人有眷属,唯我独无\"。因此,菩萨道行者说:\"看待世间一切众生,应该把年老者当作自己的父母去孝敬他;年龄与自己相近者,就当作兄弟姐妹去敬爱他;年龄比较幼小的,则当作自己的子女一般去爱护他……\"这是人性中最高洁、最真、最善、最美的爱。","source":"中兴手机","favorited":false,"truncated":false,"in_reply_to_status_id":"","in_reply_to_user_id":"","in_reply_to_screen_name":"","geo":null,"mid":"3449997964247334","user":{"id":1900675751,"screen_name":"鑫来隐形纱窗专卖店","name":"鑫来隐形纱窗专卖店","province":"23","city":"4","location":"黑龙江 鹤岗","description":"人品高于一切","url":"","profile_image_url":"http://tp4.sinaimg.cn/1900675751/50/5619629557/1","domain":"","gender":"m","followers_count":38,"friends_count":2000,"statuses_count":22,"favourites_count":1,"created_at":"Thu Dec 23 00:00:00 +0800 2010","following":false,"allow_all_act_msg":false,"geo_enabled":true,"verified":false}},{"created_at":"Sat May 26 18:11:15 +0800 2012","id":3449997964247405,"text":"【反叛的鲁鲁修】多少妹子羡慕娜娜莉有一个那样的哥哥,多少屌丝羡慕鲁鲁修有一个这样的妹妹!_~","source":"青团团购","favorited":false,"truncated":false,"in_reply_to_status_id":"","in_reply_to_user_id":"","in_reply_to_screen_name":"","thumbnail_pic":"http://ww1.sinaimg.cn/thumbnail/69579846jw1dtbvwjpbh2j.jpg","bmiddle_pic":"http://ww1.sinaimg.cn/bmiddle/69579846jw1dtbvwjpbh2j.jpg","original_pic":"http://ww1.sinaimg.cn/large/69579846jw1dtbvwjpbh2j.jpg","geo":null,"mid":"3449997964247405","user":{"id":1767348294,"screen_name":"yapals","name":"yapals","province":"33","city":"1","location":"浙江 杭州","description":"http://youthmay.taobao.com","url":"http://youthmay.taobao.com","profile_image_url":"http://tp3.sinaimg.cn/1767348294/50/5597457577/1","domain":"derek8","gender":"m","followers_count":1108,"friends_count":295,"statuses_count":298,"favourites_count":0,"created_at":"Sat Mar 05 00:00:00 +0800 2011","following":false,"allow_all_act_msg":false,"geo_enabled":true,"verified":false}},{"created_at":"Sat May 26 18:11:15 +0800 2012","id":3449997964247410,"text":"我真穷。","source":"Android客户端","favorited":false,"truncated":false,"in_reply_to_status_id":"","in_reply_to_user_id":"","in_reply_to_screen_name":"","geo":null,"mid":"3449997964247410","user":{"id":2557827674,"screen_name":"执字粒","name":"执字粒","province":"11","city":"5","location":"北京 朝阳区","description":"诚实的孤独","url":"","profile_image_url":"http://tp3.sinaimg.cn/2557827674/50/5631794631/0","domain":"","gender":"f","followers_count":14,"friends_count":5,"statuses_count":168,"favourites_count":0,"created_at":"Thu Nov 24 17:59:48 +0800 2011","following":false,"allow_all_act_msg":false,"geo_enabled":true,"verified":false}},{"created_at":"Sat May 26 18:11:15 +0800 2012","id":3449997964451939,"text":"music切克闹 嘿~","source":"iPhone客户端","favorited":false,"truncated":false,"in_reply_to_status_id":"","in_reply_to_user_id":"","in_reply_to_screen_name":"","thumbnail_pic":"http://ww3.sinaimg.cn/thumbnail/827a4268jw1dtbvwk1e3yj.jpg","bmiddle_pic":"http://ww3.sinaimg.cn/bmiddle/827a4268jw1dtbvwk1e3yj.jpg","original_pic":"http://ww3.sinaimg.cn/large/827a4268jw1dtbvwk1e3yj.jpg","geo":null,"mid":"3449997964451939","user":{"id":2189050472,"screen_name":"鲨鱼Aluan","name":"鲨鱼Aluan","province":"12","city":"9","location":"天津 大港区","description":"霸气不羁鲨叔 废话少说 非诚勿扰 家有活宝 请勿打扰✨","url":"","profile_image_url":"http://tp1.sinaimg.cn/2189050472/50/5631801331/1","domain":"jerryshark","gender":"m","followers_count":3747,"friends_count":148,"statuses_count":426,"favourites_count":94,"created_at":"Thu Jun 23 10:43:47 +0800 2011","following":false,"allow_all_act_msg":false,"geo_enabled":true,"verified":false}},{"created_at":"Sat May 26 18:11:14 +0800 2012","id":3449997964451957,"text":"母校裝修沃! 頂起!頂起!","source":"iPhone客户端","favorited":false,"truncated":false,"in_reply_to_status_id":"","in_reply_to_user_id":"","in_reply_to_screen_name":"","thumbnail_pic":"http://ww4.sinaimg.cn/thumbnail/87558ee4jw1dtbvwjq6nij.jpg","bmiddle_pic":"http://ww4.sinaimg.cn/bmiddle/87558ee4jw1dtbvwjq6nij.jpg","original_pic":"http://ww4.sinaimg.cn/large/87558ee4jw1dtbvwjq6nij.jpg","geo":null,"mid":"3449997964451957","user":{"id":2270531300,"screen_name":"邂逅_-黃昏","name":"邂逅_-黃昏","province":"44","city":"20","location":"广东 中山","description":"中山一中\n初二 3 互粉〜〜","url":"","profile_image_url":"http://tp1.sinaimg.cn/2270531300/50/5632838447/1","domain":"","gender":"m","followers_count":205,"friends_count":770,"statuses_count":493,"favourites_count":34,"created_at":"Wed Jul 27 11:05:37 +0800 2011","following":false,"allow_all_act_msg":false,"geo_enabled":true,"verified":false}},{"created_at":"Sat May 26 18:11:14 +0800 2012","id":3449997964451972,"text":"分享了2012夏装新款女装蕾丝裤子女韩版休闲卷边休闲裤女带腰带4245990¥68.00 http://t.cn/zOuZDp8","source":"淘宝网","favorited":false,"truncated":false,"in_reply_to_status_id":"","in_reply_to_user_id":"","in_reply_to_screen_name":"","thumbnail_pic":"http://ww3.sinaimg.cn/thumbnail/900d6ccdjw1dtbvwjo6vmj.jpg","bmiddle_pic":"http://ww3.sinaimg.cn/bmiddle/900d6ccdjw1dtbvwjo6vmj.jpg","original_pic":"http://ww3.sinaimg.cn/large/900d6ccdjw1dtbvwjo6vmj.jpg","geo":null,"mid":"3449997964451972","user":{"id":2416798925,"screen_name":"包包139175","name":"包包139175","province":"14","city":"1","location":"山西 太原","description":"","url":"","profile_image_url":"http://tp2.sinaimg.cn/2416798925/50/5630121838/1","domain":"","gender":"m","followers_count":40,"friends_count":70,"statuses_count":189,"favourites_count":0,"created_at":"Tue Oct 18 00:00:00 +0800 2011","following":false,"allow_all_act_msg":false,"geo_enabled":true,"verified":false}},{"created_at":"Sat May 26 18:11:15 +0800 2012","id":3449997964451973,"text":"分享中少林文曲星的博文:小说连载69 推荐给@头条博客 http://t.cn/zOrAHZk (分享自 @头条博客)","source":"新浪博客","favorited":false,"truncated":false,"in_reply_to_status_id":"","in_reply_to_user_id":"","in_reply_to_screen_name":"","thumbnail_pic":"http://ww3.sinaimg.cn/thumbnail/503e4d51jw1dtbvwjl88kj.jpg","bmiddle_pic":"http://ww3.sinaimg.cn/bmiddle/503e4d51jw1dtbvwjl88kj.jpg","original_pic":"http://ww3.sinaimg.cn/large/503e4d51jw1dtbvwjl88kj.jpg","geo":null,"mid":"3449997964451973","user":{"id":1346260305,"screen_name":"徐培初文曲星","name":"徐培初文曲星","province":"42","city":"5","location":"湖北 宜昌","description":"以诗立博,坚持不辍。净化红尘,PK全国!","url":"http://blog.sina.com.cn/xbc1957124","profile_image_url":"http://tp2.sinaimg.cn/1346260305/50/5597396121/1","domain":"mzhzcm","gender":"m","followers_count":502,"friends_count":905,"statuses_count":1248,"favourites_count":0,"created_at":"Thu Sep 24 00:00:00 +0800 2009","following":false,"allow_all_act_msg":false,"geo_enabled":true,"verified":false}},{"created_at":"Sat May 26 18:11:14 +0800 2012","id":3449997964451989,"text":"这个道理谁都懂开是做到的有几个","source":"MTK客户端","favorited":false,"truncated":false,"in_reply_to_status_id":"","in_reply_to_user_id":"","in_reply_to_screen_name":"","geo":null,"mid":"3449997964451989","user":{"id":2779620745,"screen_name":"手机用户2779620745","name":"手机用户2779620745","province":"53","city":"1000","location":"云南","description":"","url":"","profile_image_url":"http://tp2.sinaimg.cn/2779620745/50/0/1","domain":"","gender":"m","followers_count":1,"friends_count":16,"statuses_count":1,"favourites_count":0,"created_at":"Wed May 23 09:39:53 +0800 2012","following":false,"allow_all_act_msg":false,"geo_enabled":true,"verified":false}},{"created_at":"Sat May 26 18:11:15 +0800 2012","id":3449997964452071,"text":"<匆匆那年>让我重温那年那月,让我看到自己的过往,大家的过往,是一个如此之长的故事,让我看到青春突然白发苍苍。","source":"iPhone客户端","favorited":false,"truncated":false,"in_reply_to_status_id":"","in_reply_to_user_id":"","in_reply_to_screen_name":"","geo":null,"mid":"3449997964452071","user":{"id":1242703230,"screen_name":"兔斯基Viviani","name":"兔斯基Viviani","province":"11","city":"8","location":"北京 海淀区","description":"","url":"http://blog.sina.com.cn/shmilysx","profile_image_url":"http://tp3.sinaimg.cn/1242703230/50/1298361516/0","domain":"","gender":"f","followers_count":69,"friends_count":214,"statuses_count":526,"favourites_count":0,"created_at":"Tue Aug 17 00:00:00 +0800 2010","following":false,"allow_all_act_msg":false,"geo_enabled":true,"verified":false}},{"created_at":"Sat May 26 18:11:15 +0800 2012","id":3449997964452074,"text":"分享图片","source":"新浪微博","favorited":false,"truncated":false,"in_reply_to_status_id":"","in_reply_to_user_id":"","in_reply_to_screen_name":"","thumbnail_pic":"http://ww2.sinaimg.cn/thumbnail/785b33e4gw1dtbvvm2d06j.jpg","bmiddle_pic":"http://ww2.sinaimg.cn/bmiddle/785b33e4gw1dtbvvm2d06j.jpg","original_pic":"http://ww2.sinaimg.cn/large/785b33e4gw1dtbvvm2d06j.jpg","geo":null,"mid":"3449997964452074","user":{"id":2019242980,"screen_name":"la_clope","name":"la_clope","province":"100","city":"1000","location":"其他","description":"","url":"","profile_image_url":"http://tp1.sinaimg.cn/2019242980/50/5619528871/0","domain":"gainbourgienne","gender":"f","followers_count":214,"friends_count":22,"statuses_count":1825,"favourites_count":97,"created_at":"Sat Mar 19 20:53:03 +0800 2011","following":false,"allow_all_act_msg":true,"geo_enabled":false,"verified":false}},{"created_at":"Sat May 26 18:11:12 +0800 2012","id":3449997964452149,"text":"越,明天烧你的遗物给你,你活在这个世上的物证就快要没了!好想好想你!心好痛好痛!","source":"新浪微博手机版","favorited":false,"truncated":false,"in_reply_to_status_id":"","in_reply_to_user_id":"","in_reply_to_screen_name":"","geo":null,"mid":"3449997964452149","user":{"id":1888642887,"screen_name":"迷失了的兔子_聚秀育全","name":"迷失了的兔子_聚秀育全","province":"44","city":"1","location":"广东 广州","description":"想要和陈越一生一世,却被上天折磨,阴阳相隔,没有幸福可言的人!","url":"","profile_image_url":"http://tp4.sinaimg.cn/1888642887/50/5609690473/1","domain":"","gender":"m","followers_count":145,"friends_count":166,"statuses_count":1895,"favourites_count":65,"created_at":"Sun Aug 28 01:12:38 +0800 2011","following":false,"allow_all_act_msg":false,"geo_enabled":true,"verified":false}}] -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * node-weibo - utils test. 3 | * Copyright(c) 2012 fengmk2 4 | * MIT Licensed 5 | */ 6 | 7 | /** 8 | * Module dependencies. 9 | */ 10 | 11 | var libpath = process.env.WEIBO_COV ? '../lib-cov' : '../lib'; 12 | var utils = require(libpath + '/utils'); 13 | var qs = require('querystring'); 14 | var sha1 = require(libpath + '/sha1'); 15 | 16 | 17 | describe('utils.js', function () { 18 | 19 | describe('utils.format(tpl, params)', function () { 20 | it('should format success', function () { 21 | utils.format('{{hello}}, {{foo}}!!!', { 22 | hello: '你好', 23 | foo: 'foolish', 24 | bar: 'bar' 25 | }).should.equal('你好, foolish!!!'); 26 | }); 27 | 28 | it('should format with match callback', function () { 29 | utils.format('{{hello}}, {{foo}}!!!', function (m, k) { 30 | return k + '哈哈'; 31 | }).should.equal('hello哈哈, foo哈哈!!!'); 32 | }); 33 | }); 34 | 35 | describe('utils.querystring.parse()', function () { 36 | 37 | it('should parse success', function () { 38 | var params = { 39 | key: '密码', 40 | name: '名称name==??==', 41 | password: '**!@#!@????123124', 42 | '中文key哈哈': 'Chinese key' 43 | }; 44 | var qstring = qs.stringify(params); 45 | var to = utils.querystring.parse(qstring); 46 | to.should.have.keys(Object.keys(params)); 47 | for (var k in to) { 48 | to[k].should.equal(params[k]); 49 | } 50 | }); 51 | 52 | it('should parse empty', function () { 53 | Object.keys(utils.querystring.parse()).should.length(0); 54 | Object.keys(utils.querystring.parse('')).should.length(0); 55 | Object.keys(utils.querystring.parse(' ')).should.length(0); 56 | Object.keys(utils.querystring.parse(null)).should.length(0); 57 | Object.keys(utils.querystring.parse('abc')).should.length(0); 58 | Object.keys(utils.querystring.parse('=abc')).should.length(0); 59 | }); 60 | 61 | }); 62 | 63 | describe('utils.querystring.stringify()', function () { 64 | 65 | it('should stringify success', function () { 66 | var params = { 67 | key: '密码', 68 | name: '名称name==??==', 69 | password: '**!@#!@????123124', 70 | '中文key哈哈': 'Chinese key' 71 | }; 72 | var decode = utils.querystring.stringify(params); 73 | decode = qs.parse(decode); 74 | decode.should.have.keys(Object.keys(params)); 75 | for (var k in decode) { 76 | decode[k].should.equal(params[k]); 77 | } 78 | }); 79 | }); 80 | 81 | describe('utils.urljoin()', function () { 82 | it('should work', function () { 83 | utils.urljoin('http://foo').should.equal('http://foo'); 84 | utils.urljoin('http://foo', {}).should.equal('http://foo'); 85 | utils.urljoin('http://foo', { bar: 'bar2' }).should.equal('http://foo?bar=bar2'); 86 | utils.urljoin('http://foo?', { bar: 'bar2' }).should.equal('http://foo?&bar=bar2'); 87 | utils.urljoin('http://foo?f1=f2', { bar: 'bar2' }).should.equal('http://foo?f1=f2&bar=bar2'); 88 | }); 89 | }); 90 | 91 | describe('utils.base64HmacSha1()', function () { 92 | it('should create a sha1 hash', function () { 93 | var words = [ 94 | '中文sdfjlsdfjslf', 'foo', '哈红十渡地方级无法技术类j哦w法' 95 | ]; 96 | words.forEach(function (word) { 97 | utils.base64HmacSha1(word, word + 'key').should.equal(sha1.b64_hmac_sha1(word + 'key', word)); 98 | }); 99 | }); 100 | 101 | it('should base64 right', function () { 102 | var bs = 'POST&http%3A%2F%2Fapi.t.sina.com.cn%2Fstatuses%2Frepost.json&id%3D3449709785616243%26oauth_consumer_key%3D4010445928%26oauth_nonce%3D8IuoWgcM2t5QH0xf3DvLlghIgr5pPWnW%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1338066480%26oauth_token%3Dd1ef5fa9aa9fee08fdc6267193a59d6a%26oauth_version%3D1.0%26source%3D4010445928%26status%3D%252525E8%252525BF%25252599%252525E6%25252598%252525AF%252525E6%252525B5%2525258B%252525E8%252525AF%25252595%252525E8%252525BD%252525AC%252525E5%2525258F%25252591%252525E5%252525BE%252525AE%252525E5%2525258D%2525259Arespost%2528%2529%252525EF%252525BC%2525258C%252525E6%2525259D%252525A5%252525E8%25252587%252525AA%252525E5%2525258D%25252595%252525E5%25252585%25252583%252525E6%252525B5%2525258B%252525E8%252525AF%25252595%25252520tapi.test.js%25252520at%25252520Sun%25252520May%2525252027%252525202012%2525252005%2525253A08%2525253A00%25252520GMT%2525252B0800%25252520%2528CST%2529%25252520%2525257C%2525257C%25252520%252525E6%2525258C%25252589%252525E9%25252581%25252593%252525E7%25252590%25252586%252525E6%25252598%252525AF%252525E4%252525B8%2525258D%252525E4%252525BC%2525259A%252525E5%25252587%252525BA%252525E7%2525258E%252525B0%252525E7%2525259A%25252584%252525EF%252525BC%2525258C%252525E5%252525A6%25252582%252525E6%2525259E%2525259C%252525E5%25252587%252525BA%252525E7%2525258E%252525B0%252525E4%252525BA%25252586%252525EF%252525BC%2525258C%252525E5%252525B0%252525B1%252525E6%25252598%252525AF%252525E5%2525258D%25252595%252525E5%25252585%25252583%252525E6%252525B5%2525258B%252525E8%252525AF%25252595%252525E4%252525B8%2525258D%252525E9%25252580%2525259A%252525E8%252525BF%25252587%252525E4%252525BA%25252586%252525E3%25252580%25252582'; 103 | var key = 'd119f62bfb70a4ba8d9b68bf14d6e45a&798722589f339cc4e9e0a66a9b53f693'; 104 | var hash = 'ZM+ttA9KMRSl+XfT9CJrMfnRf14='; 105 | utils.base64HmacSha1(bs, key).should.equal(hash); 106 | require('../lib/sha1').b64_hmac_sha1(key, bs).should.equal(hash); 107 | }); 108 | }); 109 | 110 | describe('mimeLookup()', function () { 111 | var cases = [ 112 | ['', 'application/octet-stream'], 113 | ['image.jpg2', 'application/octet-stream'], 114 | ['image', 'application/octet-stream'], 115 | ['image哈哈', 'application/octet-stream'], 116 | ['jpg', 'image/jpeg'], 117 | ['image.jpg', 'image/jpeg'], 118 | ['中文.jpg', 'image/jpeg'], 119 | ['/a/b/c/d/image.jpg', 'image/jpeg'], 120 | ['../../../../../image.jpg', 'image/jpeg'], 121 | ['jpeg', 'image/jpeg'], 122 | ['gif', 'image/gif'], 123 | ['png', 'image/png'], 124 | ['bmp', 'image/bmp'], 125 | ]; 126 | cases.forEach(function (item) { 127 | utils.mimeLookup(item[0]).should.equal(item[1]); 128 | }); 129 | }); 130 | 131 | }); -------------------------------------------------------------------------------- /test/utils/check.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * node-weibo - test/utils/check.js 3 | * Copyright(c) 2012 fengmk2 4 | * MIT Licensed 5 | */ 6 | 7 | "use strict"; 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var should = require('should'); 14 | 15 | exports.checkUser = checkUser; 16 | exports.checkStatus = checkStatus; 17 | exports.checkComment = checkComment; 18 | exports.checkCount = checkCount; 19 | exports.checkFavorite = checkFavorite; 20 | exports.checkMessage = checkMessage; 21 | 22 | function checkUser(user) { 23 | user.should.have.property('id'); 24 | user.id.should.match(/^[\w\-]+$/).with.be.a('string'); 25 | user.t_url.should.match(/^https?:\/\//); 26 | user.screen_name.should.be.a('string'); 27 | user.should.have.property('name').with.be.a('string'); 28 | user.should.have.property('location').with.be.a('string'); 29 | if (user.description) { 30 | user.description.should.be.a('string'); 31 | } 32 | if (user.url) { 33 | user.url.should.be.a('string'); 34 | } 35 | user.profile_image_url.should.match(/^https?:\/\//); 36 | user.should.have.property('avatar_large').with.match(/^https?:\/\//); 37 | user.should.have.property('gender').with.match(/[mfn]/); 38 | user.should.have.property('followers_count').with.be.a('number'); 39 | user.should.have.property('friends_count').with.be.a('number'); 40 | user.should.have.property('statuses_count').with.be.a('number'); 41 | user.should.have.property('favourites_count').with.be.a('number'); 42 | if ('created_at' in user) { 43 | user.created_at.constructor.should.equal(Date); 44 | } 45 | user.should.have.property('following').with.be.a('boolean'); 46 | if ('allow_all_act_msg' in user) { 47 | user.allow_all_act_msg.should.be.a('boolean'); 48 | } 49 | if ('geo_enabled' in user) { 50 | user.geo_enabled.should.be.a('boolean'); 51 | } 52 | user.should.have.property('verified').with.be.a('boolean'); 53 | if ('verified_type' in user) { 54 | user.verified_type.should.be.a('number'); 55 | } 56 | if ('verified_reason' in user) { 57 | user.verified_reason.should.be.a('string'); 58 | } 59 | if ('remark' in user) { 60 | user.remark.should.be.a('string'); 61 | } 62 | if ('allow_all_comment' in user) { 63 | user.allow_all_comment.should.be.a('boolean'); 64 | } 65 | user.should.have.property('follow_me').with.be.a('boolean'); 66 | if ('online_status' in user) { 67 | user.online_status.should.be.a('number'); 68 | } 69 | if ('bi_followers_count' in user) { 70 | user.bi_followers_count.should.be.a('number'); 71 | } 72 | if ('lang' in user) { 73 | user.lang.should.be.a('string'); 74 | } 75 | if (user.status) { 76 | checkStatus(user.status); 77 | } 78 | } 79 | 80 | function checkStatus(status) { 81 | status.should.have.property('id').with.match(/^\d+$/); 82 | if (status.deleted || !status.created_at) { 83 | return; 84 | } 85 | status.t_url.should.match(/^https?:\/\//); 86 | status.should.have.property('created_at').with.be.an.instanceof(Date); 87 | should.ok(status.created_at); 88 | status.text.should.be.a('string'); 89 | status.should.have.property('source').with.match(/ 4 | * MIT Licensed 5 | */ 6 | 7 | "use strict"; 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var libpath = process.env.WEIBO_COV ? '../lib-cov' : '../lib'; 14 | var WeiboAPI = require(libpath + '/weibo'); 15 | var weibo = require(libpath + '/tapi'); 16 | 17 | describe('weibo_text_process.js', function () { 18 | 19 | var weiboapi = new WeiboAPI(); 20 | 21 | describe('process_at()', function () { 22 | 23 | it('should handle no-acsii correct', function () { 24 | var cases = [ 25 | ['【观点·@任志强】今年提出的1000万套的保障房任务可能根本完不成', 26 | '【观点·@任志强】今年提出的1000万套的保障房任务可能根本完不成'], 27 | ['abc@foo@bar....!@#$', 28 | 'abc@foo@bar....!@#$'], 29 | ]; 30 | cases.forEach(function (item) { 31 | weiboapi.process_at(item[0]).should.equal(item[1]); 32 | }); 33 | }); 34 | 35 | }); 36 | 37 | describe('process_search()', function () { 38 | 39 | it('should convert #hash tag# to search url', function () { 40 | var cases = [ 41 | ['#沪js#', '#沪js#'], 42 | ['#沪js##123#', '#沪js##123#'], 43 | ['#foo bar 123#123123', '#foo bar 123#123123'], 44 | ]; 45 | cases.forEach(function (item) { 46 | weiboapi.process_search(item[0]).should.equal(item[1]); 47 | }); 48 | }); 49 | 50 | }); 51 | 52 | describe('process_text()', function () { 53 | 54 | it('should process url, @user, #hash#', function () { 55 | var cases = [ 56 | ['http://t.cn/zlcThPG @user#沪js#', 'http://t.cn/zlcThPG @user#沪js#'], 57 | ['#沪js##123#www.baidu.com', '#沪js##123#www.baidu.com'], 58 | ['#foo bar 123#123123', '#foo bar 123#123123'], 59 | ['', ' '], 60 | [null, ' '], 61 | [undefined, ' '], 62 | ]; 63 | cases.forEach(function (item) { 64 | weibo.process_text({blogtype: 'weibo'}, {text: item[0]}).should.equal(item[1]); 65 | }); 66 | }); 67 | 68 | }); 69 | 70 | describe('process_emotional()', function () { 71 | it('should show emotionurl', function () { 72 | 73 | var cases = [ 74 | ["[ok]", ''], 75 | ["[呵呵]", ''], 76 | ['[哼]', ''], 77 | ]; 78 | 79 | cases.forEach(function (item) { 80 | weibo.process_text({blogtype: 'weibo'}, {text: item[0]}).should.equal(item[1]); 81 | }); 82 | 83 | }); 84 | }); 85 | 86 | 87 | 88 | }); 89 | --------------------------------------------------------------------------------