wechat-bot2 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,4 @@
1
+ module WeChat::Bot
2
+ class NoReplyException < RuntimeError
3
+ end
4
+ end
@@ -0,0 +1,16 @@
1
+ module WeChat::Bot
2
+ module WeChatEmojiString
3
+ def convert_emoji
4
+ emoji_regex = /<span class="emoji emoji(\w+)"><\/span>/
5
+ if match = self.match(emoji_regex)
6
+ return self.gsub(emoji_regex, [match[1].hex].pack("U"))
7
+ end
8
+
9
+ self
10
+ end
11
+ end
12
+ end
13
+
14
+ class String
15
+ include WeChat::Bot::WeChatEmojiString
16
+ end
@@ -0,0 +1,92 @@
1
+ module WeChat::Bot
2
+ # Handler
3
+ class Handler
4
+ # @return [Core]
5
+ attr_reader :bot
6
+
7
+ # @return [Symbol]
8
+ attr_reader :event
9
+
10
+ # @return [String]
11
+ attr_reader :pattern
12
+
13
+ # @return [Array]
14
+ attr_reader :args
15
+
16
+ # @return [Proc]
17
+ attr_reader :block
18
+
19
+ # @return [Symbol]
20
+ attr_reader :group
21
+
22
+ # @return [ThreadGroup]
23
+ # @api private
24
+ attr_reader :thread_group
25
+
26
+ def initialize(bot, event, pattern, options = {}, &block)
27
+ options = {
28
+ :group => nil,
29
+ :execute_in_callback => false,
30
+ :strip_colors => false,
31
+ :args => []
32
+ }.merge(options)
33
+
34
+ @bot = bot
35
+ @event = event
36
+ @pattern = pattern
37
+ @block = block
38
+ @group = options[:group]
39
+ @execute_in_callback = options[:execute_in_callback]
40
+ @args = options[:args]
41
+
42
+ @thread_group = ThreadGroup.new
43
+ end
44
+
45
+ # 执行 Handler
46
+ #
47
+ # @param [Symbol] message
48
+ # @param [String] captures
49
+ # @param [Array] arguments
50
+ # @return [Thread]
51
+ def call(message, captures, arguments)
52
+ bargs = captures + arguments
53
+
54
+ thread = Thread.new {
55
+ @bot.logger.debug "[New thread] For #{self}: #{Thread.current} -- #{@thread_group.list.size} in total."
56
+ begin
57
+ if @execute_in_callback
58
+ @bot.callback.instance_exec(message, *@args, *bargs, &@block)
59
+ else
60
+ @block.call(message, *@args, *bargs)
61
+ end
62
+ rescue => e
63
+ @bot.logger.error "[Thread error] #{e.message} -> #{e.backtrace.join("\n")}"
64
+ ensure
65
+ @bot.logger.debug "[Thread done] For #{self}: #{Thread.current} -- #{@thread_group.list.size - 1} remaining."
66
+ end
67
+ }
68
+
69
+ @thread_group.add(thread)
70
+ thread
71
+ end
72
+
73
+ # @return [void]
74
+ def stop
75
+ @bot.logger.debug "[Stopping handler] Stopping all threads of handler #{self}: #{@thread_group.list.size} threads..."
76
+ @thread_group.list.each do |thread|
77
+ Thread.new do
78
+ @bot.logger.debug "[Ending thread] Waiting 10 seconds for #{thread} to finish..."
79
+ thread.join(10)
80
+ @bot.logger.debug "[Killing thread] Killing #{thread}"
81
+ thread.kill
82
+ end
83
+ end
84
+ end
85
+
86
+ # @return [String]
87
+ def to_s
88
+ # TODO maybe add the number of running threads to the output?
89
+ "#<Cinch::Handler @event=#{@event.inspect} pattern=#{@pattern.inspect}>"
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,92 @@
1
+ module WeChat::Bot
2
+ # Handler 列表
3
+ class HandlerList
4
+ include Enumerable
5
+
6
+ def initialize
7
+ @handlers = Hash.new {|h,k| h[k] = []}
8
+ @mutex = Mutex.new
9
+ end
10
+
11
+ # 注册 Handler
12
+ #
13
+ # @param [Handler] handler
14
+ # @return [void]
15
+ def register(handler)
16
+ @mutex.synchronize do
17
+ handler.bot.logger.debug "[on handler] Registering handler with pattern `#{handler.pattern}`, reacting on `#{handler.event}`"
18
+ @handlers[handler.event].push(handler)
19
+ end
20
+ end
21
+
22
+ # 取消注册 Handler
23
+ #
24
+ # @param [Array<Handler>] handlers
25
+ # @return [void]
26
+ def unregister(*handlers)
27
+ @mutex.synchronize do
28
+ handlers.each do |handler|
29
+ @handlers[handler.event].delete(handler)
30
+ end
31
+ end
32
+ end
33
+
34
+ # 分派执行 Handler
35
+ #
36
+ # @param [Symbol] event
37
+ # @param [String] message
38
+ # @param [Array<Object>] args
39
+ # @return [Array<Thread>]
40
+ def dispatch(event, message = nil, *args)
41
+ threads = []
42
+
43
+ if handlers = find(event, message)
44
+ already_run = Set.new
45
+ handlers.each do |handler|
46
+ next if already_run.include?(handler.block)
47
+ already_run.add(handler.block)
48
+
49
+ if message
50
+ captures = message.match(handler.pattern.to_r(message), event).captures
51
+ else
52
+ captures = []
53
+ end
54
+
55
+ threads.push(handler.call(message, captures, args))
56
+ end
57
+ end
58
+
59
+ threads
60
+ end
61
+
62
+ # 查找匹配 Handler
63
+ #
64
+ # @param [Symbol] type
65
+ # @param [String] message
66
+ # @return [Hander]
67
+ def find(type, message = nil)
68
+ if handlers = @handlers[type]
69
+ if message.nil?
70
+ return handlers
71
+ end
72
+
73
+ handlers = handlers.select { |handler|
74
+ message.match(handler.pattern.to_r(message), type)
75
+ }.group_by {|handler| handler.group}
76
+
77
+ handlers.values_at(*(handlers.keys - [nil])).map(&:first) + (handlers[nil] || [])
78
+ end
79
+ end
80
+
81
+ def each(&block)
82
+ @handlers.values.flatten.each(&block)
83
+ end
84
+
85
+ # 停止运行所有 Handler
86
+ #
87
+ # @return [void]
88
+ def stop_all
89
+ each { |h| h.stop }
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,2 @@
1
+ module WeChat::Bot
2
+ end
@@ -0,0 +1,33 @@
1
+ require "http/mime_type"
2
+ require "http/mime_type/adapter"
3
+
4
+ module WeChat::Bot
5
+ module HTTP
6
+ module MimeType
7
+ # Javascript 代码解析
8
+ # 用于解析微信 API 返回的部分 JS 代码,
9
+ # 提示:不可逆转
10
+ class JS < ::HTTP::MimeType::Adapter
11
+ # Encodes object to js
12
+ def encode(_)
13
+ "" # NO NEED encode
14
+ end
15
+
16
+ # 转换 JS 代码为 Hash
17
+ #
18
+ # @return [Hash]
19
+ def decode(str)
20
+ str.split("window.").each_with_object({}) do |item, obj|
21
+ key, value = item.split(/\s*=\s*/, 2)
22
+ next unless key || value
23
+ key = key.split(".")[-1]
24
+ obj[key] = eval(value)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ ::HTTP::MimeType.register_adapter "text/javascript", WeChat::Bot::HTTP::MimeType::JS
32
+ ::HTTP::MimeType.register_alias "text/javascript", :js
33
+ end
@@ -0,0 +1,29 @@
1
+ require "http/mime_type"
2
+ require "http/mime_type/adapter"
3
+ require "multi_xml"
4
+ # require "roxml"
5
+
6
+ module WeChat::Bot
7
+ module HTTP
8
+ module MimeType
9
+ # XML 代码解析
10
+ # 提示:不可逆转
11
+ class XML < ::HTTP::MimeType::Adapter
12
+ # Encodes object to js
13
+ def encode(_)
14
+ "" # NO NEED encode
15
+ end
16
+
17
+ # 转换 XML 代码为 Hash
18
+ #
19
+ # @return [Hash]
20
+ def decode(str)
21
+ MultiXml.parse(str)
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ ::HTTP::MimeType.register_adapter "text/xml", WeChat::Bot::HTTP::MimeType::XML
28
+ ::HTTP::MimeType.register_alias "text/xml", :xml
29
+ end