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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +3 -0
- data/.rubocop.yml +14 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/.vscode/launch.json +65 -0
- data/.yardopts +3 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +23 -0
- data/Rakefile +39 -0
- data/lib/wechat-bot.rb +1 -0
- data/lib/wechat/bot.rb +11 -0
- data/lib/wechat/bot/cached_list.rb +15 -0
- data/lib/wechat/bot/callback.rb +15 -0
- data/lib/wechat/bot/client.rb +574 -0
- data/lib/wechat/bot/configuration.rb +68 -0
- data/lib/wechat/bot/contact.rb +214 -0
- data/lib/wechat/bot/contact_list.rb +56 -0
- data/lib/wechat/bot/core.rb +138 -0
- data/lib/wechat/bot/ext/wechat_emoji_string.rb +16 -0
- data/lib/wechat/bot/handler.rb +92 -0
- data/lib/wechat/bot/handler_list.rb +92 -0
- data/lib/wechat/bot/helper.rb +2 -0
- data/lib/wechat/bot/http/adapter/js.rb +33 -0
- data/lib/wechat/bot/http/adapter/xml.rb +29 -0
- data/lib/wechat/bot/http/session.rb +117 -0
- data/lib/wechat/bot/logger.rb +102 -0
- data/lib/wechat/bot/message.rb +262 -0
- data/lib/wechat/bot/message_data/share_link.rb +46 -0
- data/lib/wechat/bot/pattern.rb +63 -0
- data/lib/wechat/bot/version.rb +5 -0
- data/lib/wechat_bot.rb +1 -0
- data/wechat-bot.gemspec +37 -0
- metadata +206 -0
@@ -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
|