wechat-bot2 0.1.1

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,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