wechat-bot 0.1.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,68 @@
1
+ module WeChat::Bot
2
+ class Configuration < OpenStruct
3
+ # 默认配置
4
+ #
5
+ # @return [Hash]
6
+ def self.default_config
7
+ {
8
+ # Bot Configurations
9
+ verbose: false,
10
+ fireman: 'filehelper',
11
+
12
+ # WeChat Configurations
13
+ app_id: "wx782c26e4c19acffb",
14
+ auth_url: "https://login.weixin.qq.com",
15
+ servers: [
16
+ {
17
+ index: "wx.qq.com",
18
+ file: "file.wx.qq.com",
19
+ push: "webpush.wx.qq.com",
20
+ },
21
+ {
22
+ index: "wx2.qq.com",
23
+ file: "file.wx2.qq.com",
24
+ push: "webpush.wx2.qq.com",
25
+ },
26
+ {
27
+ index: "wx8.qq.com",
28
+ file: "file.wx8.qq.com",
29
+ push: "webpush.wx8.qq.com",
30
+ },
31
+ {
32
+ index: "wechat.com",
33
+ file: "file.web.wechat.com",
34
+ push: "webpush.web.wechat.com",
35
+ },
36
+ {
37
+ index: "web2.wechat.com",
38
+ file: "file.web2.wechat.com",
39
+ push: "webpush.web2.wechat.com",
40
+ },
41
+ ],
42
+ cookies: "wechat-bot-cookies.txt",
43
+ special_users: [
44
+ 'newsapp', 'filehelper', 'weibo', 'qqmail',
45
+ 'fmessage', 'tmessage', 'qmessage', 'qqsync',
46
+ 'floatbottle', 'lbsapp', 'shakeapp', 'medianote',
47
+ 'qqfriend', 'readerapp', 'blogapp', 'facebookapp',
48
+ 'masssendapp', 'meishiapp', 'feedsapp', 'voip',
49
+ 'blogappweixin', 'brandsessionholder', 'weixin',
50
+ 'weixinreminder', 'officialaccounts', 'wxitil',
51
+ 'notification_messages', 'wxid_novlwrv3lqwv11',
52
+ 'gh_22b87fa7cb3c', 'userexperience_alarm',
53
+ ],
54
+ user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.86 Safari/537.36",
55
+ }
56
+ end
57
+
58
+ def initialize(defaults = nil)
59
+ defaults ||= self.class.default_config
60
+ super(defaults)
61
+ end
62
+
63
+ # @return [Hash]
64
+ def to_h
65
+ @table.clone
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,214 @@
1
+ module WeChat::Bot
2
+ # 微信联系人
3
+ #
4
+ # 可以是用户、公众号、群组等
5
+ class Contact
6
+ # 联系人分类
7
+ module Kind
8
+ User = :user
9
+ Group = :group
10
+ MP = :mp
11
+ Special = :special
12
+ end
13
+
14
+ def self.parse(obj, bot)
15
+ self.new(bot).parse(obj)
16
+ end
17
+
18
+ def initialize(bot)
19
+ @bot = bot
20
+ @data = {}
21
+ end
22
+
23
+ # 用户唯一 ID
24
+ def username
25
+ attr(:username)
26
+ end
27
+
28
+ # 用户昵称
29
+ def nickname
30
+ attr(:nickname)
31
+ end
32
+
33
+ # 备注名
34
+ def remarkname
35
+ attr(:remarkname)
36
+ end
37
+
38
+ # 群聊显示名
39
+ def displayname
40
+ attr(:displayname)
41
+ end
42
+
43
+ # 性别
44
+ def sex
45
+ attr(:sex)
46
+ end
47
+
48
+ # 个人签名
49
+ def signature
50
+ attr(:signature)
51
+ end
52
+
53
+ # 用户类型
54
+ def kind
55
+ attr(:kind)
56
+ end
57
+
58
+ # 省份
59
+ def province
60
+ attr(:province)
61
+ end
62
+
63
+ # 城市
64
+ def city
65
+ attr(:city)
66
+ end
67
+
68
+ # 是否特殊账户
69
+ def special?
70
+ kind == Kind::Special
71
+ end
72
+
73
+ # 是否群聊
74
+ def group?
75
+ kind == Kind::Group
76
+ end
77
+
78
+ # 是否公众号
79
+ def mp?
80
+ kind == Kind::MP
81
+ end
82
+
83
+ # 群组成员列表
84
+ #
85
+ # 只有群组才有内容,根据 {#kind} 或 {#group?} 来判断
86
+ # 不是群组类型的返回空数组
87
+ #
88
+ # @return [Hash]
89
+ def members
90
+ attr(:members)
91
+ end
92
+
93
+ # 联系人解析
94
+ #
95
+ # @param [Hash<Object, Object>] raw
96
+ # @return [Contact]
97
+ def parse(raw, update = false)
98
+ @raw = raw
99
+
100
+ parse_kind
101
+ parse_members
102
+
103
+ @raw.each do |key, value|
104
+ if attribute = mapping[key]
105
+ next if value.to_s.empty? && update
106
+
107
+ sync(attribute, value)
108
+ end
109
+ end
110
+
111
+ self
112
+ end
113
+
114
+ def update(raw)
115
+ @raw = raw
116
+ parse(@raw, true)
117
+ end
118
+
119
+ def to_s
120
+ "#<#{self.class}:#{object_id.to_s(16)} username='#{username}' nickname='#{nickname}' kind='#{kind}'>"
121
+ end
122
+
123
+ private
124
+
125
+ # 更新或新写入变量值
126
+ #
127
+ # @param [Symbol] attribute
128
+ # @param [String, Integer, Hash] value
129
+ # @param [Boolean] data
130
+ # @return [void]
131
+ def sync(attribute, value, data = false)
132
+ value = value.convert_emoji if attribute.to_sym == :nickname
133
+
134
+ # 满足群组类型且 nickname 为空时补充一个默认的群组名(参考微信 App 设计)
135
+ if attribute.to_sym == :nickname && value.to_s.empty? && @kind == Kind::Group
136
+ value = members.map {|m| m.nickname }.join("、")
137
+ end
138
+
139
+ if data
140
+ @data[attribute.to_sym] = value
141
+ else
142
+ instance_variable_set("@#{attribute}", value)
143
+ end
144
+ end
145
+
146
+ # 获取属性
147
+ #
148
+ # @param [Symbol] attribute
149
+ # @param [Boolean] data 默认 false
150
+ # @return [String, Integer, Hash]
151
+ def attr(attribute, data = false)
152
+ if data
153
+ @data[attribute.to_sym]
154
+ else
155
+ instance_variable_get("@#{attribute}")
156
+ end
157
+ end
158
+
159
+ # 解析联系人类型
160
+ #
161
+ # 详见 {Contact::Kind} 成员变量
162
+ # @return [void]
163
+ def parse_kind
164
+ kind = if @bot.config.special_users.include?(@raw["UserName"])
165
+ # 特殊账户
166
+ Kind::Special
167
+ elsif @raw["UserName"].include?("@@")
168
+ # 群聊
169
+ Kind::Group
170
+ elsif @raw["VerifyFlag"] && (@raw["VerifyFlag"] & 8) != 0
171
+ # 公众号
172
+ Kind::MP
173
+ else
174
+ # 普通用户
175
+ Kind::User
176
+ end
177
+
178
+ sync(:kind, kind)
179
+ end
180
+
181
+ # 解析群组成员列表
182
+ #
183
+ # 只有群组才有内容,根据 {#kind} 或 {#group?} 来判断
184
+ #
185
+ # @return [void]
186
+ def parse_members
187
+ members = []
188
+
189
+ if @raw["MemberList"]
190
+ @raw["MemberList"].each do |m|
191
+ members.push(Contact.parse(m, @bot))
192
+ end
193
+ end
194
+
195
+ sync(:members, members)
196
+ end
197
+
198
+ # 字段映射
199
+ #
200
+ # @return [Hash<String, String>]
201
+ def mapping
202
+ {
203
+ "NickName" => "nickname",
204
+ "UserName" => "username",
205
+ "RemarkName" => "remarkname",
206
+ "DisplayName" => "displayname",
207
+ "Signature" => "signature",
208
+ "Sex" => "sex",
209
+ "Province" => "province",
210
+ "City" => 'city'
211
+ }
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,56 @@
1
+ module WeChat::Bot
2
+ # 微信联系人列表
3
+ class ContactList < CachedList
4
+ # 批量同步联系人数据
5
+ #
6
+ # 更多查看 {#sync} 接口
7
+ # @param [Array<Hash>] list 联系人数组
8
+ # @return [ContactList]
9
+ def batch_sync(list)
10
+ list.each do |item|
11
+ sync(item)
12
+ end
13
+
14
+ self
15
+ end
16
+
17
+ def size
18
+ @cache.size
19
+ end
20
+
21
+ # 创建用户或更新用户数据
22
+ #
23
+ # @param [Hash] data 微信接口返回的单个用户数据
24
+ # @return [Contact]
25
+ def sync(data)
26
+ @mutex.synchronize do
27
+ contact = Contact.parse(data, @bot)
28
+ if @cache[contact.username]
29
+ @cache[contact.username].update(data)
30
+ else
31
+ @cache[contact.username] = contact
32
+ end
33
+
34
+ contact
35
+ end
36
+ end
37
+
38
+ # 查找用户
39
+ #
40
+ # @param [Hash] args 接受两个参数:
41
+ # - :nickname 昵称
42
+ # - :username 用户ID
43
+ # @return [Contact]
44
+ def find(**args)
45
+ @mutex.synchronize do
46
+ return @cache[args[:username]] if args[:username]
47
+
48
+ if args[:nickname]
49
+ @cache.each do |_, contact|
50
+ return contact if contact.nickname == args[:nickname]
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,138 @@
1
+ require "wechat/bot/configuration"
2
+ require "wechat/bot/cached_list"
3
+ require "wechat/bot/contact_list"
4
+ require "wechat/bot/contact"
5
+ require "wechat/bot/logger"
6
+
7
+ require "wechat/bot/handler_list"
8
+ require "wechat/bot/handler"
9
+ require "wechat/bot/message"
10
+ require "wechat/bot/pattern"
11
+ require "wechat/bot/callback"
12
+
13
+ require "wechat/bot/http/adapter/js"
14
+ require "wechat/bot/http/adapter/xml"
15
+ require "wechat/bot/http/session"
16
+
17
+ require "logger"
18
+
19
+ module WeChat::Bot
20
+ # 机器人的核心类
21
+ class Core
22
+ # 微信 API 客户端
23
+ #
24
+ # @return [Client]
25
+ attr_reader :client
26
+
27
+ # 当前登录用户信息
28
+ #
29
+ # @return [Contact]
30
+ attr_reader :profile
31
+
32
+ # 联系人列表
33
+ #
34
+ # @return [ContactList]
35
+ attr_reader :contact_list
36
+
37
+ # @return [Logger]
38
+ attr_accessor :logger
39
+
40
+ # @return [HandlerList]
41
+ attr_reader :handlers
42
+
43
+ # @return [Configuration]
44
+ attr_reader :config
45
+
46
+ # @return [Callback]
47
+ # @api private
48
+ attr_reader :callback
49
+
50
+ def initialize(&block)
51
+ # defaults_logger
52
+ @logger = Logger.new(STDOUT, self)
53
+ @config = Configuration.new
54
+ @handlers = HandlerList.new
55
+ @callback = Callback.new(self)
56
+
57
+ @client = Client.new(self)
58
+ @profile = Contact.new(self)
59
+ @contact_list = ContactList.new(self)
60
+
61
+ instance_eval(&block) if block_given?
62
+ end
63
+
64
+ # 消息触发器
65
+ #
66
+ # @param [String, Symbol, Integer] event
67
+ # @param [Regexp, Pattern, String] regexp
68
+ # @param [Array<Object>] args
69
+ # @yieldparam [Array<String>]
70
+ # @return [Handler]
71
+ def on(event, regexp = //, *args, &block)
72
+ event = event.to_s.to_sym
73
+
74
+ pattern = case regexp
75
+ when Pattern
76
+ regexp
77
+ when Regexp
78
+ Pattern.new(nil, regexp, nil)
79
+ else
80
+ if event == :ctcp
81
+ Pattern.generate(:ctcp, regexp)
82
+ else
83
+ Pattern.new(/^/, /#{Regexp.escape(regexp.to_s)}/, /$/)
84
+ end
85
+ end
86
+
87
+ handler = Handler.new(self, event, pattern, {args: args, execute_in_callback: true}, &block)
88
+ @handlers.register(handler)
89
+
90
+ handler
91
+ end
92
+
93
+ # 用于设置 WeChat::Bot 的配置
94
+ # 默认无需配置,需要定制化 yield {Core#config} 进行配置
95
+ #
96
+ # @yieldparam [Struct] config
97
+ # @return [void] 没有返回值
98
+ def configure
99
+ yield @config
100
+ end
101
+
102
+ # 运行机器人
103
+ #
104
+ # @return [void]
105
+ def start
106
+ @client.login
107
+ @client.contacts
108
+
109
+ @contact_list.each do |c|
110
+ @logger.debug "Contact: #{c}"
111
+ end
112
+
113
+ while true
114
+ break unless @client.logged? || @client.alive?
115
+ sleep 1
116
+ end
117
+ rescue Interrupt => e
118
+ message = "你使用 Ctrl + C 终止了运行"
119
+ @logger.warn(message)
120
+ @client.send_text(@config.fireman, "[告警] 意外下线\n#{message}\n#{e.backtrace.join("\n")}") if @client.logged? && @client.alive?
121
+ rescue Exception => e
122
+ message = e.message
123
+ @logger.fatal(e)
124
+ @client.send_text(@config.fireman, "[告警] 意外下线\n#{message}\n#{e.backtrace.join("\n")}") if @client.logged? && @client.alive?
125
+ ensure
126
+ @client.logout if @client.logged? && @client.alive?
127
+ end
128
+
129
+ private
130
+
131
+ # def defaults_logger
132
+ # @logger = Logger.new($stdout)
133
+ # @logger.formatter = proc do |severity, datetime, progname, msg|
134
+ # "#{severity}\t[#{datetime.strftime("%Y-%m-%d %H:%M:%S.%2N")}]: #{msg}\n"
135
+ # end
136
+ # end
137
+ end
138
+ end