wechat-bot 0.1.0.alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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