diff --git a/.gitignore b/.gitignore index 2b288c2a3fd15349bd2bcf2f559d1c579ae3476b..dfd85f2a639f8b3f579d5b4ed6e852c09e70eafb 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ t socket.io.min.js .*.swp +0 diff --git a/.travis.yml b/.travis.yml index 1cc51c65c68c598ebb115128a4d581a33bf3e7a0..a868d7fd493d00d1902e22ac61b21036f21d912e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,6 @@ language: node_js node_js: - "6.1" - "6" -sudo: required dist: trusty before_install: - export DISPLAY=:99.0 diff --git a/README.md b/README.md index 9eee2d8eb1bc3d3fb2595c368e2297519ccfb776..c4e0ec16342a97c54b99b681f45416cba5d0ada1 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ ![Wechaty](https://raw.githubusercontent.com/zixia/wechaty/master/images/wechaty-logo-en.png) # Wechaty [![Circle CI](https://circleci.com/gh/zixia/wechaty.svg?style=svg)](https://circleci.com/gh/zixia/wechaty) [![Build Status](https://travis-ci.org/zixia/wechaty.svg?branch=master)](https://travis-ci.org/zixia/wechaty) -Wechaty is Wechat for Bot.(Personal Account Robot, NOT Official Account) +Wechaty is a Bot-Enable Framework/Library for Personal Account of Wechat. -> Easy creating wechat robot code in 10 lines. +> Easy creating personal account wechat robot code in 10 lines. + +**Connecting Bots** [![Join the chat at https://gitter.im/zixia/wechaty](https://badges.gitter.im/zixia/wechaty.svg)](https://gitter.im/zixia/wechaty?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![node](https://img.shields.io/node/v/wechaty.svg?maxAge=2592000)](https://nodejs.org/) @@ -10,19 +12,19 @@ Wechaty is Wechat for Bot.(Personal Account Robot, NOT Official Account) [![Downloads][downloads-image]][downloads-url] # Why -My daily life/work depends on too much wechat. -* I almost have 14,000 wechat friends till May 2014, before wechat restricting total number of friends to 5,000. +My daily life/work depends on too much chat on wechat. +* I almost have 14,000 wechat friends till May 2014, before wechat restricts a total number of friends to 5,000. * I almost have 400 wechat groups that most of them have more than 400 members. Can you image that? I'm dying... -So a tireless bot working for me 24x7 on wechat, moniting/filtering the most important messages is badly needed. For example: highlights discusstion which contains the KEYWORDS I want to follow up(especialy in a noisy group). ;-) +So a tireless bot working for me 24x7 on wechat, moniting/filtering the most important message is badly needed. For example: highlights discusstion which contains the KEYWORDS I want to follow up(especially in a noisy group). ;-) # Examples Wechaty is super easy to use: 10 lines of javascript is enough for your first wechat robot. ## 1. Basic: 10 lines -The following 10 lines of code will implement a bot who will reply message automaticaly for you: +The following 10 lines of code will implement a bot that will reply a message automatically to you: ```javascript const Wechaty = require('wechaty') @@ -44,24 +46,58 @@ bot.on('message', m => { }) ``` -Notice that you need to wait a moment while bot trying to get the login QRCode from Wechat. As soon as the bot got login QRCode url, he will print url out. You need to scan the qrcode in wechat, and confirm login. +Notice that you need to wait a moment while bot trys to get the login QRCode from Wechat. As soon as the bot gets login QRCode url, he will print url out. You need to scan the qrcode on wechat, and confirm login. -After that, bot will on duty. (roger-bot source can be found at [here](https://github.com/zixia/wechaty/blob/master/example/roger-bot.js)) +After that, bot will be on duty. (roger-bot source can be found at [here](https://github.com/zixia/wechaty/blob/master/example/roger-bot.js)) ## 2. Advanced: 50 lines -There's another basic usage demo bot named [ding-dong-bot](https://github.com/zixia/wechaty/blob/master/example/ding-dong-bot.js), who can reply `dong` when receive a message `ding`. +There's another basic usage demo bot named [ding-dong-bot](https://github.com/zixia/wechaty/blob/master/example/ding-dong-bot.js), who can reply _dong_ when bot receives a message _ding_. ## 3. Hardcore: 100 lines -To Be Wroten. +To Be Written. -Plan to glued with Machine Learning/Deep Learning/Neural Network/Natural Language Processing. +Plan to glue with Machine Learning/Deep Learning/Neural Network/Natural Language Processing. -# Installation -The recommended installation method is a local NPM install for your project: +# Installation & Usage +Use NPM is recommended to install Wechaty for you: ```bash $ npm install --save wechaty ``` +## Start from strach +In case that you do not know anything about nodejs, the follow instructions would help you to run Wechaty bot on your machine. + +## 1. Install NodeJS +NodeJS Version 6.0 & above is required. +1. Visit [NodeJS](https://nodejs.org) +1. Download NodeJS Installer(i.e. "v6.2.0 Current") +1. Run Installer to install NodeJS to your machine + +## 2. Checkout Wechaty +Use `git` to checkout Wechaty source code from [Github.com](https://github.com) +```shell +git clone git@github.com:zixia/wechaty.git +# git clone https://github.com/zixia/wechaty.git +``` + +## 3. Install Dependents +```shell +cd wechaty +npm install +``` + +## 4. Run Demo Bot +```shell +npm start +# node example/ding-dong-bot.js +``` + +# Trouble Shooting +If wechaty is not run as expected, run unit test maybe help to find some useful message. +```shell +npm test +``` + # Requirement ECMAScript2015/ES6. I develop and test wechaty under nodejs6.0. @@ -100,17 +136,18 @@ Emit when there's a new message. ```javascript bot.on('message', callback) ``` -Callback will get a instance of Message Class. (see `Class Message`) +Callback will get an instance of Message Class. (see `Class Message`) ### Event: `login` & `logout` -To-Be-Supported +1. After the bot login full successful, the event `login` will be emitted. +1. After the bot logout, the event `logout` will be emitted. ## Class Message All messages will be encaped in Message. ### Message.ready() -A message may be not fully initialized yet. Call `ready()` to confirm we get all the data needed. +A message may be not fully initialized yet. Call `ready()` to confirm we get all the data needed. Return a Promise, will be resolved when all data is ready. @@ -121,7 +158,7 @@ message.ready() }) ``` ### Message.get(prop) -Get prop from a message. +Get prop from a message. Supported prop list: @@ -147,7 +184,7 @@ message.set('content', 'Hello, World!') ## Class Contact ### Contact.ready() -A Contact may be not fully initialized yet. Call `ready()` to confirm we get all the data needed. +A Contact may be not fully initialized yet. Call `ready()` to confirm we get all the data needed. Return a Promise, will be resolved when all data is ready. @@ -158,7 +195,7 @@ contact.ready() }) ``` ### Contact.get(prop) -Get prop from a contact. +Get prop from a contact. Supported prop list: @@ -178,7 +215,7 @@ contact.get('name') ## Class Group ### Group.ready() -A group may be not fully initialized yet. Call `ready()` to confirm we get all the data needed. +A group may be not fully initialized yet. Call `ready()` to confirm we get all the data needed. Return a Promise, will be resolved when all data is ready. @@ -190,7 +227,7 @@ group.ready() ``` ### Group.get(prop) -Get prop from a group. +Get prop from a group. Supported prop list: @@ -217,11 +254,11 @@ Know more about tape: [Why I use Tape Instead of Mocha & So Should You](https:// # Version History ## v0.0.5 (2016/5/11) -1. receive & send message -1. show contacts info -1. show groups info +1. Receive & send message +1. Show contacts info +1. Show groups info 1. 1st usable version -1. start coding from 1st May 2016 +1. Start coding from May 1st 2016 # Todo List 1. Deal with friend request @@ -230,9 +267,6 @@ Know more about tape: [Why I use Tape Instead of Mocha & So Should You](https:// Everybody is welcome to issue your needs. # Known Issues & Support -1. phantomjs not work(no socket.io connect from browser) -2. firefox need to use unstable mode(or inject will be blocked almost forever) - Github Issue - https://github.com/zixia/wechaty/issues # Contributing @@ -241,7 +275,11 @@ Github Issue - https://github.com/zixia/wechaty/issues ```bash $ npm lint ``` -* Create a issue, fork, then send a pull request(with unit test please). +* Create an issue, fork, then send a pull request(with unit test please). + +# See Also +* [wxBot](https://github.com/liuwons/wxBot): Wechat Bot API in Python +* [ItChat](https://github.com/littlecodersh/ItChat): Command line talks through Wechat in Python Author ----------------- diff --git a/circle.yml b/circle.yml index ad52db0157202a8df106d977b75a8003548e24f8..94953245962167800a1642b7102f10a666b9356e 100644 --- a/circle.yml +++ b/circle.yml @@ -1,15 +1,6 @@ machine: node: - version: 6.0.0 - post: - - wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - - - sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' - - sudo apt-get update - - sudo apt-get install google-chrome-stable - -dependencies: - post: - + version: 6.1.0 notify: webhooks: # A list of hook hashes, containing the url field diff --git a/doc/webwxapp.js b/doc/webwxapp.js new file mode 100644 index 0000000000000000000000000000000000000000..fbd94e7ebea210179612b82890e8e0d967388727 --- /dev/null +++ b/doc/webwxapp.js @@ -0,0 +1,8465 @@ +!function() { + var e, t = function() {} + , o = ["assert", "clear", "count", "debug", "dir", "dirxml", "error", "exception", "group", "groupCollapsed", "groupEnd", "info", "log", "markTimeline", "profile", "profileEnd", "table", "time", "timeEnd", "timeStamp", "trace", "warn"], n = o.length; + 0 == /mmdebug/.test(location.search) && location.href.indexOf("dev.web.weixin") < 0 && (window.console = {}); + for (var r = window.console = window.console || {}; n--; ) + e = o[n], + r[e] || (r[e] = t) +}(), +angular.module("Controllers", []), +!function() { + "use strict"; + location.href.indexOf("dev.web") < 0 ? angular.module("exceptionOverride", []).factory("$exceptionHandler", [function() { + return function(e) { + throw window._errorHandler && window._errorHandler(e), + console.log(e), + e + } + } + ]) : angular.module("exceptionOverride", []), + angular.module("Controllers").controller("appController", ["$rootScope", "$scope", "$timeout", "$log", "$state", "$window", "ngDialog", "mmpop", "appFactory", "loginFactory", "contactFactory", "accountFactory", "chatFactory", "confFactory", "contextMenuFactory", "notificationFactory", "utilFactory", "reportService", "actionTrack", "surviveCheckService", "subscribeMsgService", "stateManageService", function( + e // $rootScope + , t // $scope + , o // $timeout + , n // $log + , r // $state + , a // $window + , i // ngDialog + , c // mmpop + , s // appFactory + , l // loginFactory + , u // contactFactory + , f // accountFactory + , d // chatFactory + , g // confFactory + , m // contextMenuFactory + , p // notificationFactory + , h // utilFactory + , M // reportService + , y // actionTrack + , C // surviveCheckService + , v // subscribeMsgService + , w // stateManageService + ) { + function S() { + return u.pickContacts(["friend", "chatroom"], { + chatroom: { + keyword: t.keyword, + isNewArray: !0 + }, + friend: { + keyword: t.keyword, + isNewArray: !0, + isWithoutBrand: !0, + showFriendHeader: !0 + } + }, !0).result + } + function b() { + var e = k; + e && setTimeout(function() { + var t = (e[0].clientHeight - e.find(".ngdialog-content").height()) / 2; + e.css("paddingTop", t) + }, 20) + } + function T() { + t.isLoaded = !0, + t.isUnLogin = !1, + M.report(M.ReportType.timing, { + timing: { + initStart: Date.now() + } + }), + s.init().then(function(n) { + if (h.log("initData", n), + n.BaseResponse && "0" != n.BaseResponse.Ret) + return console.log("BaseResponse.Ret", n.BaseResponse.Ret), + void (l.timeoutDetect(n.BaseResponse.Ret) || i.openConfirm({ + className: "default ", + templateUrl: "comfirmTips.html", + controller: ["$scope", function(e) { + e.title = MM.context("02d9819"), + e.content = MM.context("0d2fc2c"), + M.report(M.ReportType.initError, { + text: "程序初始化失败,点击确认刷新页面", + code: n.BaseResponse.Ret, + cookie: document.cookie + }), + e.callback = function() { + document.location.reload(!0) + } + } + ] + })); + f.setUserInfo(n.User), + f.setSkey(n.SKey), + f.setSyncKey(n.SyncKey), + u.addContact(n.User), + u.addContacts(n.ContactList), + d.initChatList(n.ChatSet), + d.notifyMobile(f.getUserName(), g.StatusNotifyCode_INITED), + v.init(n.MPSubscribeMsgList), + e.$broadcast("root:pageInit:success"), + h.setCheckUrl(f), + h.log("getUserInfo", f.getUserInfo()), + t.$broadcast("updateUser"), + M.report(M.ReportType.timing, { + timing: { + initEnd: Date.now() + } + }); + var r = n.ClickReportInterval || 3e5; + setTimeout(function a() { + y.report(), + setTimeout(a, r) + }, r), + o(function() { + function e(o) { + u.initContact(o).then(function(o) { + u.addContacts(o.MemberList), + M.report(M.ReportType.timing, { + timing: { + initContactEnd: Date.now() + }, + needSend: !0 + }), + 16 >= t && o.Seq && 0 != o.Seq && (t++, + e(o.Seq)) + }) + } + M.report(M.ReportType.timing, { + timing: { + initContactStart: Date.now() + } + }); + var t = 1; + e(0) + }, 0), + t.account = u.getContact(f.getUserName()), + E() + }) + } + function E() { + t.debug && (F && o.cancel(F), + C.start(4e4), + F = o(function() { + s.syncCheck().then(function(e) { + return C.start(5e3), + e + }, function(e) { + return C.start(2e3), + e + }).then(N, P) + }, g.TIMEOUT_SYNC_CHECK)) + } + function N(e) { + h.log("syncCheckHasChange", e); + try { + f.setSyncKey(e.SyncKey), + f.updateUserInfo(e.Profile, function() {}), + angular.forEach(e.DelContactList, function(t) { + d.deleteChatList(t.UserName), + d.deleteChatMessage(t.UserName), + u.deleteContact(t), + d.getCurrentUserName() == t.UserName && d.setCurrentUserName(""), + console.log("DelContactList", e.DelContactList) + }), + angular.forEach(e.ModContactList, function(t) { + u.addContact(t), + console.log("ModContactList", e.ModContactList) + }), + angular.forEach(e.AddMsgList, function(e) { + d.messageProcess(e) + }) + } catch (t) { + t.other = { + reason: "throw err when syncChackHasChange" + }, + window._errorHandler && window._errorHandler(t) + } finally { + E() + } + } + function P() { + E() + } + window._appTiming = {}, + r.go("chat"), + e.CONF = g, + t.isUnLogin = !window.MMCgi.isLogin, + t.debug = !0, + t.isShowReader = /qq\.com/gi.test(location.href), + window.MMCgi.isLogin && (T(), + h.browser.chrome && !MMDEV && (window.onbeforeunload = function(e) { + return e = e || window.event, + e && (e.returnValue = "关闭浏览器聊天内容将会丢失。"), + "关闭浏览器聊天内容将会丢失。" + } + )), + t.$on("newLoginPage", function(e, t) { + console.log("newLoginPage", t), + f.setSkey(t.SKey), + f.setSid(t.Sid), + f.setUin(t.Uin), + f.setPassticket(t.Passticket), + T() + }); + var A, I; + t.search = function() { + A && o.cancel(A), + A = o(function() { + return t.keyword ? (I && I.close(), + void (I = c.open({ + templateUrl: "searchList.html", + controller: ["$rootScope", "$scope", "$state", function(e, t, o) { + t.$watch(function() { + return u.contactChangeFlag + }, function() { + t.allContacts.length = 0, + t.allContacts.push.apply(t.allContacts, S()) + }), + t.clickUserCallback = function(n) { + n.UserName && (o.go("chat", { + userName: n.UserName + }), + t.closeThisMmPop(), + e.$broadcast("root:searchList:cleanKeyWord")) + } + } + ], + scope: { + keyword: t.keyword, + allContacts: S(), + heightCalc: function(e) { + return "header" === e.type ? 31 : 60 + } + }, + className: "recommendation", + autoFoucs: !1, + container: angular.element(document.querySelector("#search_bar")) + }))) : void (I && I.close()) + }, 200) + } + , + t.searchKeydown = function(t) { + switch (t.keyCode) { + case g.KEYCODE_ARROW_UP: + I && I.isOpen() && e.$broadcast("root:searchList:keyArrowUp"), + t.preventDefault(), + t.stopPropagation(); + break; + case g.KEYCODE_ARROW_DOWN: + I && I.isOpen() && e.$broadcast("root:searchList:keyArrowDown"), + t.preventDefault(), + t.stopPropagation(); + break; + case g.KEYCODE_ENTER: + I && I.isOpen() && e.$broadcast("root:searchList:keyEnter"), + t.preventDefault(), + t.stopPropagation() + } + } + , + t.$on("root:searchList:cleanKeyWord", function() { + t.keyword = "" + }); + var k; + t.$on("ngDialog.opened", function(e, t) { + w.change("dialog:open", !0), + k = t, + b() + }), + t.$on("ngDialog.closed", function() { + w.change("dialog:open", !1), + k = null + }), + $(window).on("resize", function() { + b() + }), + t.appClick = function(e) { + t.$broadcast("app:contextMenu:hide", e) + } + , + t.showContextMenu = function(e) { + t.$broadcast("app:contextMenu:show", e) + } + , + t.toggleSystemMenu = function() { + c.toggleOpen({ + templateUrl: "systemMenu.html", + top: 60, + left: 85, + container: angular.element(document.querySelector(".panel")), + controller: "systemMenuController", + singletonId: "mmpop_system_menu", + className: "system_menu" + }) + } + , + t.showProfile = function(e) { + if (t.account) { + var o = t.account + , n = e.pageY + 25 + , a = e.pageX + 6; + c.open({ + templateUrl: "profile_mini.html", + className: "profile_mini_wrap scale-fade", + top: n, + left: a, + blurClose: !0, + singletonId: "mmpop_profile", + controller: ["$scope", function(e) { + e.contact = o, + e.addUserContent = "", + e.isShowSendBox = !1, + e.chat = function(t) { + r.go("chat", { + userName: t + }), + e.closeThisMmPop() + } + } + ] + }) + } + } + , + t.dblclickChat = function() { + t.$broadcast("app:chat:dblclick") + } + , + t.requestPermission = function() { + p.requestPermission(function() { + h.log("请求权限了...") + }) + } + , + C.callback(E); + var F + } + ]) +}(), +!function() { + "use strict"; + angular.module("Controllers").controller("loginController", ["$scope", "loginFactory", "utilFactory", "reportService", function( + e // $scope + , t // loginFactory + , o // utilFactory + , n // reportService + ) { + $(".lang .lang-item").click(function(e) { + $("script").remove(), + location.href = e.target.href, + e.preventDefault() + }), + window.MMCgi.isLogin || t.getUUID().then(function(r) { + function a(i) { + switch (i.code) { + case 200: + t.newLoginPage(i.redirect_uri).then(function(t) { + var r = t.match(/(.*)<\/ret>/) + , a = t.match(/