├── .versions ├── README.md ├── client └── templates │ └── app.html ├── package.js ├── server └── lib │ └── wechat_js.js ├── shared └── collections │ ├── wechat_js_tickets.js │ └── wechat_tokens.js └── tests └── server ├── collections ├── wechat_js_ticket_spec.js └── wechat_tokens_spec.js └── lib └── wechat_js_spec.js /.versions: -------------------------------------------------------------------------------- 1 | aldeed:collection2@2.3.3 2 | aldeed:simple-schema@1.3.3 3 | amplify@1.0.0 4 | babel-compiler@5.8.24_1 5 | babel-runtime@0.1.4 6 | base64@1.0.4 7 | binary-heap@1.0.4 8 | blaze@2.1.3 9 | blaze-tools@1.0.4 10 | boilerplate-generator@1.0.4 11 | caching-compiler@1.0.0 12 | caching-html-compiler@1.0.2 13 | callback-hook@1.0.4 14 | check@1.1.0 15 | coffeescript@1.0.11 16 | ddp@1.2.2 17 | ddp-client@1.2.1 18 | ddp-common@1.2.2 19 | ddp-server@1.2.2 20 | deps@1.0.9 21 | diff-sequence@1.0.1 22 | ecmascript@0.1.6 23 | ecmascript-runtime@0.2.6 24 | ejson@1.0.7 25 | geojson-utils@1.0.4 26 | html-tools@1.0.5 27 | htmljs@1.0.5 28 | http@1.1.1 29 | huaming:wechat-js@0.0.4 30 | id-map@1.0.4 31 | jquery@1.11.4 32 | less@2.5.1 33 | local-test:huaming:wechat-js@0.0.4 34 | logging@1.0.8 35 | matb33:collection-hooks@0.7.13 36 | meteor@1.1.10 37 | minifiers@1.1.7 38 | minimongo@1.0.10 39 | mongo@1.1.3 40 | mongo-id@1.0.1 41 | npm-mongo@1.4.39_1 42 | observe-sequence@1.0.7 43 | ordered-dict@1.0.4 44 | package-version-parser@3.0.4 45 | practicalmeteor:chai@1.9.2_3 46 | practicalmeteor:loglevel@1.1.0_3 47 | promise@0.5.1 48 | random@1.0.5 49 | reactive-dict@1.1.3 50 | reactive-var@1.0.6 51 | retry@1.0.4 52 | routepolicy@1.0.6 53 | sanjo:jasmine@0.14.0 54 | sanjo:karma@1.5.1 55 | sanjo:long-running-child-process@1.1.3 56 | sanjo:meteor-files-helpers@1.1.0_7 57 | sanjo:meteor-version@1.0.0 58 | session@1.1.1 59 | spacebars@1.0.7 60 | spacebars-compiler@1.0.7 61 | stevezhu:lodash@3.8.0 62 | templating@1.1.5 63 | templating-tools@1.0.0 64 | tracker@1.0.9 65 | ui@1.0.8 66 | underscore@1.0.4 67 | url@1.0.5 68 | velocity:chokidar@1.0.3_1 69 | velocity:core@0.10.0 70 | velocity:html-reporter@0.9.0 71 | velocity:meteor-internals@1.1.0_7 72 | velocity:meteor-stubs@1.1.0 73 | velocity:shim@0.1.0 74 | velocity:source-map-support@0.3.2_1 75 | webapp@1.2.3 76 | webapp-hashing@1.0.5 77 | zimme:collection-behaviours@1.0.4 78 | zimme:collection-timestampable@1.0.6 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a library to use wechat js api, including get accessToken, get jsTicket, using wx library. 2 | 3 | For more details, please refer to [wechat official doc](http://mp.weixin.qq.com/wiki/7/aaa137b55fb2e0456bf8dd9148dd613f.html). 4 | 5 | # Usage 6 | 7 | ``` 8 | meteor add huaming:wechat-js 9 | 10 | ``` 11 | 12 | First configure your WECHAT_APP_ID and WECHAT_APP_SECRET in process.env. 13 | 14 | # WechatJs 15 | 16 | ## WechatJs.refetchAccessToken() 17 | return a new access token from wechat api, ex. { access_token: ‘access_token’, expires_in:7200 } 18 | 19 | ## WechatJs.refetchJsTicket() 20 | return a new jsticket from wechat api, ex. { errorcode: ‘’, errmsg: ‘’, ticket: ‘ticket str’, expires_in: 7200 } 21 | 22 | ## WechatJs.generateTicketSignature( nonsecure, ticket, timestamp, url ) 23 | return a sha1 string by encrypting the splice string of the given parameters, for more details please refer to the official doc about the algorithm for generating signature 24 | 25 | ## WechatJs.generateJsConfig( url ) 26 | return a configure object for given url, ex. { appId: ‘app id’, timestamp: 12345678, nonceStr: ‘abcdef’, signature: ‘signature’, jsApiList: [] } 27 | 28 | # WechatTokens.getCurrentToken() 29 | return a valid token. If the old one has expires, it will request a new one for you. 30 | 31 | # WechatJsTickets.getCurrentTicket() 32 | return a valid js ticket. If the old one has expires, it will request a new one for you. 33 | -------------------------------------------------------------------------------- /client/templates/app.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'huaming:wechat-js', 3 | version: '0.0.4', 4 | // Brief, one-line summary of the package. 5 | summary: 'This is a library to use wechat js api, including get accessToken, get jsTicket, using wx library', 6 | // URL to the Git repository containing the source code for this package. 7 | git: 'https://www.github.com/raohuaming/meteor-wechat-js', 8 | // By default, Meteor will default to using README.md for documentation. 9 | // To avoid submitting documentation, set this field to null. 10 | documentation: 'README.md' 11 | }); 12 | 13 | Package.onUse(function(api) { 14 | api.versionsFrom('1.1.0.2'); 15 | api.use([ 16 | 'http', 17 | 'aldeed:collection2@2.3.3', 18 | 'matb33:collection-hooks@0.7.13', 19 | 'zimme:collection-timestampable@1.0.6', 20 | 'stevezhu:lodash@3.8.0', 21 | ]); 22 | api.use([ 23 | 'templating' 24 | ], 'client'); 25 | api.export('WechatJs', 'server'); 26 | api.export('WechatTokens', 'server'); 27 | api.export('WechatJsTickets', 'server'); 28 | 29 | // Generated with: github.com/philcockfield/meteor-package-paths 30 | api.addFiles('shared/collections/wechat_js_tickets.js', ['client', 'server']); 31 | api.addFiles('shared/collections/wechat_tokens.js', ['client', 'server']); 32 | api.addFiles('server/lib/wechat_js.js', 'server'); 33 | api.addFiles('client/templates/app.html', 'client'); 34 | 35 | }); 36 | 37 | Package.onTest(function(api) { 38 | api.use('sanjo:jasmine@0.14.0'); 39 | api.use('velocity:html-reporter@0.6.2'); 40 | api.use('huaming:wechat-js'); 41 | 42 | // Generated with: github.com/philcockfield/meteor-package-paths 43 | api.addFiles('tests/server/collections/wechat_js_ticket_spec.js', 'server'); 44 | api.addFiles('tests/server/collections/wechat_tokens_spec.js', 'server'); 45 | api.addFiles('tests/server/lib/wechat_js_spec.js', 'server'); 46 | 47 | }); 48 | -------------------------------------------------------------------------------- /server/lib/wechat_js.js: -------------------------------------------------------------------------------- 1 | /* global WechatJs: true, Meteor, lodash, process, WechatTokens, Npm, Mongo, WechatJsTickets */ 2 | WechatJs = (function buildAPI(){ 3 | var APP_ID = process.env.WECHAT_APP_ID; 4 | var APP_SECRET = process.env.WECHAT_APP_SECRET; 5 | 6 | return { 7 | refetchAccessToken: function(){ 8 | var urlTemplate = lodash.template( 9 | 'https://api.weixin.qq.com/cgi-bin/token' + 10 | '?grant_type=client_credential&appid=<%= APP_ID %>&secret=<%= APP_SECRET %>' 11 | ); 12 | var url = urlTemplate({ 13 | APP_ID: APP_ID, 14 | APP_SECRET: APP_SECRET 15 | }); 16 | var response = Meteor.http.get( url ); 17 | if ( response.statusCode === 200 ) { 18 | return response.data; // { access_token, expires_in } 19 | } else { 20 | return undefined; 21 | } 22 | }, 23 | refetchJsTicket: function(){ 24 | var urlTemplate = lodash.template( 25 | 'https://api.weixin.qq.com/cgi-bin/ticket/getticket' + 26 | '?access_token=<%= accessToken %>&type=jsapi' 27 | ); 28 | var url = urlTemplate({ 29 | accessToken: WechatTokens.getCurrentToken() 30 | }); 31 | var response = Meteor.http.get( url ); 32 | if ( response.statusCode === 200 ){ 33 | return response.data; // { errorcode, errmsg, ticket, expires_in } 34 | } else { 35 | return undefined; 36 | } 37 | }, 38 | generateTicketSignature: function( noncestr, ticket, timestamp, url ){ 39 | var crypto = Npm.require('crypto'); 40 | var tmpStrTemplate = lodash.template( 41 | 'jsapi_ticket=<%= ticket %>' + 42 | '&noncestr=<%= noncestr %>' + 43 | '×tamp=<%= timestamp %>' + 44 | '&url=<%= url %>' 45 | ); 46 | var tmpStr = tmpStrTemplate({ 47 | ticket: ticket, 48 | noncestr: noncestr, 49 | timestamp: timestamp, 50 | url: url 51 | }); 52 | var shasum = crypto.createHash('sha1'); 53 | shasum.update(tmpStr); 54 | var shaResult = shasum.digest('hex'); 55 | return shaResult; 56 | }, 57 | generateJsConfig: function( url ){ 58 | var noncestr = ( new Mongo.ObjectID() )._str; 59 | var timestamp = Math.floor(Date.now()/1000); 60 | var ticket = WechatJsTickets.getCurrentTicket(); 61 | var signature = this.generateTicketSignature( noncestr, ticket, timestamp, url ); 62 | return { 63 | appId: APP_ID, 64 | timestamp: timestamp, 65 | nonceStr: noncestr, 66 | signature: signature, 67 | jsApiList: [] 68 | }; 69 | } 70 | }; 71 | })(); 72 | -------------------------------------------------------------------------------- /shared/collections/wechat_js_tickets.js: -------------------------------------------------------------------------------- 1 | /* global WechatJsTickets: true, WechatJs, Meteor, SimpleSchema */ 2 | WechatJsTickets = new Meteor.Collection('wechat_js_tickets'); 3 | 4 | WechatJsTickets.getCurrentTicket = function(){ 5 | var now = new Date(); 6 | 7 | var getTicket = function(){ 8 | return WechatJsTickets.findOne( { expiredDate: { $gt: now } } ); 9 | }; 10 | 11 | var createNewTicket = function(){ 12 | var ticketResult = WechatJs.refetchJsTicket(); 13 | var ticket; 14 | if ( ticketResult ) { 15 | WechatJsTickets.insert( { ticket: ticketResult.ticket, expiredDuration: ticketResult.expires_in } ); 16 | ticket = getTicket(); 17 | return ticket; 18 | } else { 19 | return undefined; 20 | } 21 | }; 22 | 23 | var ticket = getTicket() || createNewTicket() || {}; 24 | return ticket.ticket; 25 | }; 26 | 27 | Meteor.startup(function(){ 28 | var schema = new SimpleSchema({ 29 | ticket: { 30 | type: String 31 | }, 32 | expiredDuration: { 33 | type: Number, 34 | defaultValue: 7200 // in second 35 | }, 36 | expiredDate: { 37 | type: Date, 38 | optional: true, 39 | // default to 10 minuts from now 40 | autoValue: function(){ 41 | if ( this.isInsert ) { 42 | var now = new Date(); 43 | var expiredDuration = this.field('expiredDuration').value - 100; 44 | now.setSeconds( now.getSeconds() + expiredDuration ); 45 | return now; 46 | } else { 47 | this.unset(); 48 | } 49 | } 50 | } 51 | 52 | }); 53 | 54 | WechatJsTickets.attachSchema( schema ); 55 | WechatJsTickets.attachBehaviour('timestampable'); 56 | }); 57 | -------------------------------------------------------------------------------- /shared/collections/wechat_tokens.js: -------------------------------------------------------------------------------- 1 | /* global WechatTokens: true, WechatJs, Meteor, SimpleSchema */ 2 | WechatTokens = new Meteor.Collection('wechat_tokens'); 3 | 4 | WechatTokens.getCurrentToken = function(){ 5 | var now = new Date(); 6 | 7 | var getToken = function(){ 8 | return WechatTokens.findOne( { expiredDate: { $gt: now } } ); 9 | }; 10 | 11 | var createNewToken = function(){ 12 | var tokenResult = WechatJs.refetchAccessToken(); 13 | var token; 14 | if ( tokenResult ) { 15 | WechatTokens.insert( { accessToken: tokenResult.access_token, expiredDuration: tokenResult.expires_in } ); 16 | token = getToken(); 17 | return token; 18 | } else { 19 | return undefined; 20 | } 21 | }; 22 | 23 | var token = getToken() || createNewToken() || {}; 24 | return token.accessToken; 25 | }; 26 | 27 | Meteor.startup(function(){ 28 | var schema = new SimpleSchema({ 29 | accessToken: { 30 | type: String 31 | }, 32 | expiredDuration: { 33 | type: Number, 34 | defaultValue: 7200 // in second 35 | }, 36 | expiredDate: { 37 | type: Date, 38 | optional: true, 39 | // default to 10 minuts from now 40 | autoValue: function(){ 41 | if ( this.isInsert ) { 42 | var now = new Date(); 43 | var expiredDuration = this.field('expiredDuration').value - 100; 44 | now.setSeconds( now.getSeconds() + expiredDuration ); 45 | return now; 46 | } else { 47 | this.unset(); 48 | } 49 | } 50 | } 51 | 52 | }); 53 | 54 | WechatTokens.attachSchema( schema ); 55 | WechatTokens.attachBehaviour('timestampable'); 56 | }); 57 | -------------------------------------------------------------------------------- /tests/server/collections/wechat_js_ticket_spec.js: -------------------------------------------------------------------------------- 1 | /* global WechatJsTickets, expect, spyOn, WechatJs */ 2 | describe('WechatJsTickets', function(){ 3 | var ticketId, ticket; 4 | 5 | beforeEach(function(){ 6 | ticket = { ticket: '123445555', expiredDuration: 500 }; 7 | }); 8 | 9 | afterEach(function(){ 10 | WechatJsTickets.remove( ticketId ); 11 | }); 12 | 13 | describe('-- Schema', function(){ 14 | it('should auto set expiredDate based on expiredDuration', function(){ 15 | ticketId = WechatJsTickets.insert( ticket ); 16 | expect( WechatJsTickets.findOne( ticketId ).expiredDate ).toBeDefined(); 17 | }); 18 | }); 19 | 20 | describe('.getCurrentTicket', function(){ 21 | var ticket, ticketResult; 22 | 23 | it('should return a ticket if there is already one valid ticket in db', function(){ 24 | ticket = { ticket: '12345678' }; 25 | spyOn( WechatJsTickets, 'findOne' ).and.returnValue( ticket ); 26 | expect( WechatJsTickets.getCurrentTicket() ).toEqual( ticket.ticket ); 27 | }); 28 | 29 | it('should call WechatJs.refetchJsTicket if no valid ticket existing', function(){ 30 | ticketResult = { ticket: '12345678', expires_in: 500 }; 31 | spyOn( WechatJsTickets, 'findOne' ).and.returnValue( undefined ); 32 | spyOn( WechatJs, 'refetchJsTicket' ).and.returnValue( ticketResult ); 33 | spyOn( WechatJsTickets, 'insert' ); 34 | WechatJsTickets.getCurrentTicket(); 35 | expect( WechatJs.refetchJsTicket ).toHaveBeenCalled(); 36 | expect( WechatJsTickets.insert ).toHaveBeenCalledWith({ 37 | ticket: ticketResult.ticket, 38 | expiredDuration: ticketResult.expires_in 39 | }); 40 | }); 41 | 42 | it('should return undefined if no valid ticket and faild to fetch new ticket', function(){ 43 | spyOn( WechatJsTickets, 'findOne' ).and.returnValue( undefined ); 44 | spyOn( WechatJs, 'refetchJsTicket' ).and.returnValue( undefined ); 45 | expect( WechatJsTickets.getCurrentTicket() ).toEqual( undefined ); 46 | }); 47 | 48 | }); 49 | }); 50 | 51 | -------------------------------------------------------------------------------- /tests/server/collections/wechat_tokens_spec.js: -------------------------------------------------------------------------------- 1 | /* global WechatTokens, expect, spyOn, WechatJs */ 2 | describe('WechatTokens', function(){ 3 | var tokenId, token; 4 | 5 | beforeEach(function(){ 6 | token = { accessToken: '123445555', expiredDuration: 500 }; 7 | }); 8 | 9 | afterEach(function(){ 10 | WechatTokens.remove( tokenId ); 11 | }); 12 | 13 | describe('-- Schema', function(){ 14 | it('should auto set expiredDate based on expiredDuration', function(){ 15 | tokenId = WechatTokens.insert( token ); 16 | expect( WechatTokens.findOne( tokenId ).expiredDate ).toBeDefined(); 17 | }); 18 | }); 19 | 20 | describe('.getCurrentToken', function(){ 21 | var token, tokenResult; 22 | 23 | it('should return a token if there is already one valid token in db', function(){ 24 | token = { accessToken: '12345678' }; 25 | spyOn( WechatTokens, 'findOne' ).and.returnValue( token ); 26 | expect( WechatTokens.getCurrentToken() ).toEqual( token.accessToken ); 27 | }); 28 | 29 | it('should call WechatJs.refetchAccessToken if no valid token existing', function(){ 30 | tokenResult = { access_token: '12345678', expires_in: 500 }; 31 | spyOn( WechatTokens, 'findOne' ).and.returnValue( undefined ); 32 | spyOn( WechatJs, 'refetchAccessToken' ).and.returnValue( tokenResult ); 33 | spyOn( WechatTokens, 'insert' ); 34 | WechatTokens.getCurrentToken(); 35 | expect( WechatJs.refetchAccessToken ).toHaveBeenCalled(); 36 | expect( WechatTokens.insert ).toHaveBeenCalledWith({ 37 | accessToken: tokenResult.access_token, 38 | expiredDuration: tokenResult.expires_in 39 | }); 40 | }); 41 | 42 | it('should return undefined if no valid token and faild to fetch new token', function(){ 43 | spyOn( WechatTokens, 'findOne' ).and.returnValue( undefined ); 44 | spyOn( WechatJs, 'refetchAccessToken' ).and.returnValue( undefined ); 45 | expect( WechatTokens.getCurrentToken() ).toEqual( undefined ); 46 | }); 47 | 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/server/lib/wechat_js_spec.js: -------------------------------------------------------------------------------- 1 | /* global WechatJs, expect, spyOn */ 2 | describe('WechatJs', function(){ 3 | 4 | xdescribe('refetchAccessToken', function(){ 5 | it('should return a new accessToken', function(){ 6 | var tokenResult = WechatJs.refetchAccessToken(); 7 | expect( tokenResult.access_token ).toBeDefined(); 8 | expect( tokenResult.expires_in ).toBeDefined(); 9 | }); 10 | }); 11 | 12 | xdescribe('refetchJsTicket', function(){ 13 | it('should return a new js ticket', function(){ 14 | var ticketResult = WechatJs.refetchJsTicket(); 15 | expect( ticketResult.ticket ).toBeDefined(); 16 | expect( ticketResult.expires_in ).toBeDefined(); 17 | }); 18 | }); 19 | 20 | describe('generateTicketSignature', function(){ 21 | it('should generate a signature for js ticket', function(){ 22 | var noncestr = 'Wm3WZYTPz0wzccnW'; 23 | var ticket = 'sM4AOVdWfPE4DxkXGEs8VMCPGGVi4C3VM0P37wVUCFvkVAy_90u5h9nbSlYy3-Sl-HhTdfl2fzFy1AOcHKP7qg'; 24 | var timestamp = '1414587457'; 25 | var url = 'http://mp.weixin.qq.com?params=value'; 26 | var result = '0f9de62fce790f9a083d5c99e95740ceb90c27ed'; 27 | expect( WechatJs.generateTicketSignature( noncestr, ticket, timestamp, url ) ).toEqual( result ); 28 | }); 29 | }); 30 | 31 | describe('generateJsConfig', function(){ 32 | it('should return a js config for given url', function(){ 33 | var signature = '12345678'; 34 | spyOn( WechatJs, 'generateTicketSignature' ).and.returnValue( signature ); 35 | var url = 'http://jizhi.co'; 36 | var config = WechatJs.generateJsConfig( url ); 37 | expect( config.appId ).toBeDefined(); 38 | expect( config.timestamp ).toBeDefined(); 39 | expect( config.nonceStr ).toBeDefined(); 40 | expect( config.signature ).toEqual( signature ); 41 | expect( config.jsApiList ).toBeDefined(); 42 | }); 43 | }); 44 | }); 45 | --------------------------------------------------------------------------------