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,117 @@
1
+ require "http"
2
+
3
+ module WeChat::Bot
4
+ module HTTP
5
+ # 可保存 Cookies 的 HTTP 请求类
6
+ #
7
+ # 简单实现 Python 版本 {http://docs.python-requests.org/zh_CN/latest/user/advanced.html#session-objects requests.Session()}
8
+ class Session
9
+ # @return [HTTP::CookieJar]
10
+ attr_reader :cookies
11
+
12
+ def initialize(bot)
13
+ @bot = bot
14
+
15
+ load_cookies(@bot.config.cookies)
16
+ end
17
+
18
+ # @return [HTTP::Response]
19
+ def get(url, options = {})
20
+ request(:get, url, options)
21
+ end
22
+
23
+ # @return [HTTP::Response]
24
+ def post(url, options = {})
25
+ request(:post, url, options)
26
+ end
27
+
28
+ # @return [HTTP::Response]
29
+ def put(url, options = {})
30
+ request(:put, url, options)
31
+ end
32
+
33
+ # @return [HTTP::Response]
34
+ def delete(url, options = {})
35
+ request(:delete, url, options)
36
+ end
37
+
38
+ # @return [HTTP::Response]
39
+ def request(verb, url, options = {})
40
+ prepare_request(url)
41
+
42
+ if options[:timeout]
43
+ connect_timeout, read_timeout = options.delete(:timeout)
44
+ @client = @client.timeout(connect: connect_timeout, read: read_timeout)
45
+ end
46
+
47
+ response = @client.request(verb, url, options)
48
+ update_cookies(response.cookies)
49
+
50
+ @bot.logger.verbose "[#{verb.upcase}] #{url}"
51
+ @bot.logger.verbose "Options: #{options}"
52
+ @bot.logger.verbose "Response: #{response.body}"
53
+
54
+ response
55
+ end
56
+
57
+ private
58
+
59
+ # 组装 request 基础请求参数
60
+ #
61
+ # - 设置 User-Agent
62
+ # - 设置 Cooklies
63
+ #
64
+ # @api private
65
+ # @param [String] url
66
+ # @return [HTTP::Request]
67
+ def prepare_request(url)
68
+ @client = ::HTTP.headers(user_agent: @bot.config.user_agent, "Range" => "bytes=0-")
69
+ return @client if @cookies.nil?
70
+ return @client = @client.cookies(@cookies)
71
+
72
+ # TODO: 优化处理同一顶级域名的 cookies
73
+ # uri = URI(url)
74
+ # unless @cookies.empty?(uri)
75
+ # cookies = @cookies.clone
76
+ # cookies.cookies.each do |cookie|
77
+ # cookies.delete(cookie) if uri.host != cookie.domain
78
+ # end
79
+
80
+ # unless cookies.empty?(uri)
81
+ # @client = @client.cookies(@cookies)
82
+ # end
83
+ # end
84
+
85
+ end
86
+
87
+ # 加载外部的 Cookies 数据
88
+ #
89
+ # @api private
90
+ # @param [String, HTTP::CooieJar] cookies
91
+ # @return [void]
92
+ def load_cookies(cookies)
93
+ @cookies = ::HTTP::CookieJar.new
94
+ return if cookies.nil?
95
+
96
+ if cookies.is_a?(String)
97
+ @cookies.load(cookies) if File.exist?(cookies)
98
+ else
99
+ @cookies.add(cookies)
100
+ end
101
+ end
102
+
103
+ # 请求后更新存储的 Cookies 数据
104
+ #
105
+ # @api private
106
+ # @param [String, HTTP::CooieJar] cookies
107
+ # @return [void]
108
+ def update_cookies(cookies)
109
+ return @cookies = cookies if @cookies.nil? || @cookies.empty?
110
+
111
+ cookies.cookies.each do |cookie|
112
+ @cookies.add(cookie)
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,102 @@
1
+ require "colorize"
2
+
3
+ module WeChat::Bot
4
+ class Logger
5
+ LEVELS = [:verbose, :debug, :info, :warn, :error, :fatal]
6
+
7
+ # @return [Symbol]
8
+ attr_accessor :level
9
+
10
+ # @return [Mutex]
11
+ attr_reader :mutex
12
+
13
+ # @return [IO]
14
+ attr_reader :output
15
+
16
+ def initialize(output, bot)
17
+ @output = output
18
+ @bot = bot
19
+ @mutex = Mutex.new
20
+ @level = :info
21
+ end
22
+
23
+ def verbose(message)
24
+ log(:verbose, message)
25
+ end
26
+
27
+ def debug(message)
28
+ log(:debug, message)
29
+ end
30
+
31
+ def info(message)
32
+ log(:info, message)
33
+ end
34
+
35
+ def warn(message)
36
+ log(:warn, message)
37
+ end
38
+
39
+ def error(message)
40
+ log(:error, message)
41
+ end
42
+
43
+ def fatal(exception)
44
+ message = ["#{exception.backtrace.first}: #{exception.message} (#{exception.class})"]
45
+ message.concat(exception.backtrace[1..-1].map {|s| "\t" + s})
46
+ log(:fatal, message.join("\n"))
47
+ end
48
+
49
+ def log(level, message)
50
+ return unless can_log?(level)
51
+ return if message.to_s.empty?
52
+
53
+ @mutex.synchronize do
54
+ message = format_message(format_general(message), level)
55
+ @output.puts message
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def can_log?(level)
62
+ @level = :verbose if @bot.config.verbose
63
+ LEVELS.index(level) >= LEVELS.index(@level)
64
+ end
65
+
66
+ def format_general(message)
67
+ message
68
+ end
69
+
70
+ def format_message(message, level)
71
+ send("format_#{level}", message)
72
+ end
73
+
74
+ def format_verbose(message)
75
+ "VERBOSE [#{timestamp}] #{message.colorize(:light_black)}"
76
+ end
77
+
78
+ def format_debug(message)
79
+ "DEBUG [#{timestamp}] #{message.colorize(:light_black)}"
80
+ end
81
+
82
+ def format_info(message)
83
+ "INFO [#{timestamp}] #{message}"
84
+ end
85
+
86
+ def format_warn(message)
87
+ "WRAN [#{timestamp}] #{message.colorize(:yellow)}"
88
+ end
89
+
90
+ def format_error(message)
91
+ "ERROR [#{timestamp}] #{message.colorize(:light_red)}"
92
+ end
93
+
94
+ def format_fatal(message)
95
+ "FATAL [#{timestamp}] #{message.colorize(:red)}"
96
+ end
97
+
98
+ def timestamp
99
+ Time.now.strftime("%Y-%m-%d %H:%M:%S.%2N")
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,282 @@
1
+ require "wechat/bot/message_data/share_card"
2
+
3
+ module WeChat::Bot
4
+ # 微信消息
5
+ class Message
6
+ # 消息类型
7
+ module Kind
8
+ Text = :text
9
+ Image = :image
10
+ Voice = :voice
11
+ ShortVideo = :short_video
12
+ Emoticon = :emoticon
13
+ ShareCard = :share_link
14
+ # RedPacage = :red_package
15
+ # BusinessCard = :business_card
16
+ # MusicLink = :music_link
17
+ Verify = :verify
18
+ System = :system
19
+ Unkown = :unkown
20
+ end
21
+
22
+ GROUP_MESSAGE_REGEX = /^(@\w+):<br\/>(.*)$/
23
+ AT_MESSAGE_REGEX = /@([^\s]+) (.*)/
24
+
25
+ # 原始消息
26
+ # @return [Hash<Object, Object>]
27
+ attr_reader :raw
28
+
29
+ # 事件列表
30
+ # @return [Array<Symbol>]
31
+ attr_reader :events
32
+
33
+ # @return [Core]
34
+ attr_reader :bot
35
+
36
+ # @return [Time]
37
+ attr_reader :time
38
+
39
+ # 消息类型
40
+ # @return [Message::Kind]
41
+ attr_reader :kind
42
+
43
+ # 消息来源
44
+ # @return [Contact::Kind]
45
+ attr_reader :source
46
+
47
+ # 消息发送者
48
+ #
49
+ # 用户或者群组
50
+ # @return [Contact]
51
+ attr_reader :from
52
+
53
+ # 消息正文
54
+ # @return [String]
55
+ attr_reader :message
56
+
57
+ attr_reader :media_id
58
+
59
+ attr_reader :meta_data
60
+
61
+ def initialize(raw, bot)
62
+ @raw = raw
63
+ @bot = bot
64
+
65
+ @events = []
66
+ @time = Time.now
67
+ @statusmsg_mode = nil
68
+
69
+ parse
70
+
71
+ @bot.logger.verbose "Message Raw: #{@raw}"
72
+ end
73
+
74
+ # 回复消息
75
+ def reply(text, **args)
76
+ to_user = args[:username] || @from.username
77
+ to_user = @bot.contact_list.find(nickname: args[:nickname]) if args[:nickname]
78
+
79
+ message_type = args[:type] || :text
80
+
81
+ # if @bot.config.special_users.include?(to_user) && to_user != 'filehelper'
82
+ # @bot.logger.error "特殊账户无法回复: #{to_user}"
83
+ # raise NoReplyException, "特殊账户无法回复: #{to_user}"
84
+ # end
85
+
86
+ @bot.client.send(message_type, to_user, text)
87
+ end
88
+
89
+ # 解析微信消息
90
+ #
91
+ # @return [void]
92
+ def parse
93
+ parse_source
94
+ parse_kind
95
+
96
+ message = @raw["Content"].convert_emoji
97
+ message = CGI.unescape_html(message) if @kinde != Message::Kind::Text
98
+ if match = group_message(message)
99
+ # from_username = match[0]
100
+ message = match[1]
101
+ end
102
+
103
+ @message = message
104
+ # TODO: 来自于特殊账户无法获取联系人信息,需要单独处理
105
+ @from = @bot.contact_list.find(username: @raw["FromUserName"])
106
+ parse_emoticon if @kind == Message::Kind::Emoticon
107
+
108
+ case @kind
109
+ when Message::Kind::ShareCard
110
+ @meta_data = MessageData::ShareCard.parse(@message)
111
+ end
112
+
113
+ parse_events
114
+ end
115
+
116
+ # 消息匹配
117
+ #
118
+ # @param [String, Regex, Pattern] regexp 匹配规则
119
+ # @param [String, Symbol] type 消息类型
120
+ # @return [MatchData] 匹配结果
121
+ def match(regexp, type)
122
+ # text = ""
123
+ # case type
124
+ # when :ctcp
125
+ # text = ctcp_message
126
+ # when :action
127
+ # text = action_message
128
+ # else
129
+ # text = message.to_s
130
+ # type = :other
131
+ # end
132
+
133
+ # if strip_colors
134
+ # text = Cinch::Formatting.unformat(text)
135
+ # end
136
+
137
+ @message.match(regexp)
138
+ end
139
+
140
+ def at_message?
141
+ @at_mesage == true
142
+ end
143
+
144
+ # 解析消息来源
145
+ #
146
+ # 特殊账户/群聊/公众号/用户
147
+ #
148
+ # @return [void]
149
+ def parse_source
150
+ @source = if @bot.config.special_users.include?(@raw["FromUserName"])
151
+ # 特殊账户
152
+ Contact::Kind::Special
153
+ elsif @raw["FromUserName"].include?("@@")
154
+ # 群聊
155
+ Contact::Kind::Group
156
+ elsif (@raw["RecommendInfo"]["VerifyFlag"] & 8) != 0
157
+ # 公众号
158
+ Contact::Kind::MP
159
+ else
160
+ # 普通用户
161
+ Contact::Kind::User
162
+ end
163
+ end
164
+
165
+ # 解析消息类型
166
+ #
167
+ # - 1: Text 文本消息
168
+ # - 3: Image 图片消息
169
+ # - 34: Voice 语言消息
170
+ # - 37: 验证消息
171
+ # - 42: BusinessCard 名片消息
172
+ # - 47: Emoticon 微信表情
173
+ # - 49: ShareCard 分享链接消息
174
+ # - 62: ShortVideo 短视频消息
175
+ # - 1000: System 系统消息
176
+ # - Unkown 未知消息
177
+ #
178
+ # @return [void]
179
+ def parse_kind
180
+ @kind = case @raw["MsgType"]
181
+ when 1
182
+ Message::Kind::Text
183
+ when 3
184
+ Message::Kind::Image
185
+ when 34
186
+ Message::Kind::Voice
187
+ when 37
188
+ Message::Kind::Verify
189
+ when 42
190
+ Message::Kind::BusinessCard
191
+ when 62
192
+ Message::Kind::ShortVideo
193
+ when 47
194
+ Message::Kind::Emoticon
195
+ when 49
196
+ Message::Kind::ShareCard
197
+ when 10000
198
+ Message::Kind::System
199
+ else
200
+ Message::Kind::Unkown
201
+ end
202
+ end
203
+
204
+ # 解析 Handler 的事件
205
+ #
206
+ # - `:message` 用户消息
207
+ # - `:text` 文本消息
208
+ # - `:image` 图片消息
209
+ # - `:voice` 语音消息
210
+ # - `:short_video` 短视频消息
211
+ # - `:group` 群聊消息
212
+ # - `:at_message` @ 消息
213
+ #
214
+ # @return [void]
215
+ def parse_events
216
+ @events << :message
217
+ @events << @kind
218
+ @events << @source
219
+
220
+ @at_mesage = false
221
+ if @source == :group && @raw["Content"] =~ /@([^\s]+)\s+(.*)/
222
+ @events << :at_message
223
+ @at_mesage = true
224
+ end
225
+ end
226
+
227
+ # 解析表情
228
+ #
229
+ # 表情分为两种:
230
+ # 1. 微信商店表情
231
+ # 1. 自定义表情
232
+ #
233
+ # @return [void]
234
+ def parse_emoticon
235
+ if @message.empty?
236
+ @media_id = @raw["MediaId"]
237
+ # TODO: 解决微信商店表情
238
+ # file = @bot.client.download_image(@raw["NewMsgId"])
239
+ else
240
+ data = MultiXml.parse(@message)
241
+ @media_id = data["msg"]["emoji"]["md5"]
242
+ end
243
+ end
244
+
245
+ def parse_share
246
+ # TODO: 完成解析
247
+ data = MultiXml.parse(@message)
248
+ end
249
+
250
+ # 解析用户的群消息
251
+ #
252
+ # 群消息格式:
253
+ # @FromUserName:<br>Message
254
+ #
255
+ # @param [String] message 原始消息
256
+ # @return [Array<Object>] 返回两个值的数组
257
+ # - 0 from_username
258
+ # - 1 message
259
+ def group_message(message)
260
+ if match = GROUP_MESSAGE_REGEX.match(message)
261
+ return [match[1], at_message(match[2])]
262
+ end
263
+
264
+ false
265
+ end
266
+
267
+ # 尝试解析群聊中的 @ 消息
268
+ #
269
+ # 群消息格式:
270
+ # @ToNickNameUserName Message
271
+ #
272
+ # @param [String] message 原始消息
273
+ # @return [String] 文本消息,如果不是 @ 消息返回原始消息
274
+ def at_message(message)
275
+ if match = AT_MESSAGE_REGEX.match(message)
276
+ return match[2].strip
277
+ end
278
+
279
+ message
280
+ end
281
+ end
282
+ end
@@ -0,0 +1,46 @@
1
+ module WeChat::Bot::MessageData
2
+ class ShareCard
3
+ def self.parse(raw)
4
+ self.new(raw)
5
+ end
6
+
7
+ # @return [String]
8
+ attr_reader :title
9
+
10
+ # @return [String]
11
+ attr_reader :link
12
+
13
+ # @return [String]
14
+ attr_reader :description
15
+
16
+ # @return [String, Nil]
17
+ attr_reader :thumb_image
18
+
19
+ # @return [String]
20
+ attr_reader :from_user
21
+
22
+ # @return [String, Nil]
23
+ attr_reader :app
24
+
25
+ # @return [Hash<Symbol, String>, Hash<Symbol, Nil>]
26
+ attr_reader :mp
27
+
28
+ def initialize(raw)
29
+ @raw = MultiXml.parse(raw.gsub("<br/>", ""))
30
+ parse
31
+ end
32
+
33
+ def parse
34
+ @title = @raw["msg"]["appmsg"]["title"]
35
+ @link = @raw["msg"]["appmsg"]["url"]
36
+ @description = @raw["msg"]["appmsg"]["des"]
37
+ @thumb_image = @raw["msg"]["appmsg"]["thumb_url"]
38
+ @from_user = @raw["msg"]["fromusername"]
39
+ @app = @raw["msg"]["appname"]
40
+ @mp = {
41
+ username: @raw["msg"]["sourceusername"],
42
+ nickname: @raw["msg"]["sourcedisplayname"],
43
+ }
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,63 @@
1
+ module WeChat::Bot
2
+ # 消息匹配
3
+ class Pattern
4
+ # @param [String, Regexp, NilClass, Proc, #to_s] obj 匹配规则
5
+ # @return [Regexp, nil]
6
+ def self.obj_to_r(obj, anchor = nil)
7
+ case obj
8
+ when Regexp, NilClass
9
+ return obj
10
+ else
11
+ escaped = Regexp.escape(obj.to_s)
12
+ case anchor
13
+ when :start
14
+ return Regexp.new("^" + escaped)
15
+ when :end
16
+ return Regexp.new(escaped + "$")
17
+ when nil
18
+ return Regexp.new(Regexp.escape(obj.to_s))
19
+ end
20
+ end
21
+ end
22
+
23
+ def self.resolve_proc(obj, msg = nil)
24
+ if obj.is_a?(Proc)
25
+ return resolve_proc(obj.call(msg), msg)
26
+ else
27
+ return obj
28
+ end
29
+ end
30
+
31
+ def self.generate(type, argument)
32
+ case type
33
+ when :ctcp
34
+ Pattern.new(/^/, /#{Regexp.escape(argument.to_s)}(?:$| .+)/, nil)
35
+ else
36
+ raise ArgumentError, "Unsupported type: #{type.inspect}"
37
+ end
38
+ end
39
+
40
+ attr_reader :prefix
41
+ attr_reader :suffix
42
+ attr_reader :pattern
43
+
44
+ def initialize(prefix, pattern, suffix)
45
+ @prefix, @pattern, @suffix = prefix, pattern, suffix
46
+ end
47
+
48
+ def to_r(msg = nil)
49
+ pattern = Pattern.resolve_proc(@pattern, msg)
50
+
51
+ case pattern
52
+ when Regexp, NilClass
53
+ prefix = Pattern.obj_to_r(Pattern.resolve_proc(@prefix, msg), :start)
54
+ suffix = Pattern.obj_to_r(Pattern.resolve_proc(@suffix, msg), :end)
55
+ /#{prefix}#{pattern}#{suffix}/
56
+ else
57
+ prefix = Pattern.obj_to_r(Pattern.resolve_proc(@prefix, msg))
58
+ suffix = Pattern.obj_to_r(Pattern.resolve_proc(@suffix, msg))
59
+ /^#{prefix}#{Pattern.obj_to_r(pattern)}#{suffix}$/
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,5 @@
1
+ module WeChat
2
+ module Bot
3
+ VERSION = "0.1.1"
4
+ end
5
+ end
data/lib/wechat/bot.rb ADDED
@@ -0,0 +1,12 @@
1
+ require "wechat/bot/version"
2
+ require "wechat/bot/ext/wechat_emoji_string"
3
+
4
+ require "wechat/bot/core"
5
+ require "wechat/bot/client"
6
+ require "wechat/bot/exception"
7
+
8
+ module WeChat::Bot
9
+ def self.new(&block)
10
+ WeChat::Bot::Core.new(&block)
11
+ end
12
+ end
data/lib/wechat-bot.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'wechat/bot'
data/lib/wechat_bot.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'wechat/bot'
@@ -0,0 +1,40 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'wechat/bot/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "wechat-bot2"
8
+ spec.version = WeChat::Bot::VERSION
9
+ spec.authors = ["icyleaf"]
10
+ spec.email = ["icyleaf.cn@gmail.com"]
11
+
12
+ spec.summary = "WeChat Bot for Ruby"
13
+ spec.description = "WeChat Bot for Ruby with personal account"
14
+ spec.homepage = "https://github.com/icyleaf/wechat-bot"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.required_ruby_version = ">= 2.1.0"
25
+
26
+ spec.add_development_dependency "bundler", "~> 1.14"
27
+ spec.add_development_dependency "rake", "~> 10.0"
28
+ spec.add_development_dependency "rspec", "~> 3.0"
29
+ spec.add_development_dependency "rubocop", "~> 0.53.0"
30
+ spec.add_development_dependency "webmock"
31
+ spec.add_development_dependency "awesome_print"
32
+
33
+ spec.add_dependency "colorize", "~> 0.8.1"
34
+ spec.add_dependency "http", "~> 2.2.2"
35
+ spec.add_dependency "rqrcode", "~> 0.10.1"
36
+ spec.add_dependency "multi_xml", "~> 0.6.0"
37
+ # spec.add_dependency "representable", "~> 3.0.4"
38
+ # spec.add_dependency "roxml", "~> 3.3.1"
39
+ # spec.add_dependency "gemoji"
40
+ end