wechat-bot 0.1.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,262 @@
1
+ require "wechat/bot/message_data/share_link"
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
+ ShareLink = :share_link
14
+ # RedPacage = :red_package
15
+ # BusinessCard = :business_card
16
+ # MusicLink = :music_link
17
+ System = :system
18
+ Unkown = :unkown
19
+ end
20
+
21
+ GROUP_MESSAGE_REGEX = /^(@\w+):<br\/>(.*)$/
22
+ AT_MESSAGE_REGEX = /@([^\s]+) (.*)/
23
+
24
+ # 原始消息
25
+ # @return [Hash<Object, Object>]
26
+ attr_reader :raw
27
+
28
+ # 事件列表
29
+ # @return [Array<Symbol>]
30
+ attr_reader :events
31
+
32
+ # @return [Core]
33
+ attr_reader :bot
34
+
35
+ # @return [Time]
36
+ attr_reader :time
37
+
38
+ # 消息类型
39
+ # @return [Message::Kind]
40
+ attr_reader :kind
41
+
42
+ # 消息来源
43
+ # @return [Contact::Kind]
44
+ attr_reader :source
45
+
46
+ # 消息发送者
47
+ #
48
+ # 用户或者群组
49
+ # @return [Contact]
50
+ attr_reader :from
51
+
52
+ # 消息正文
53
+ # @return [String]
54
+ attr_reader :message
55
+
56
+ attr_reader :media_id
57
+
58
+ attr_reader :meta_data
59
+
60
+ def initialize(raw, bot)
61
+ @raw = raw
62
+ @bot = bot
63
+
64
+ @events = []
65
+ @time = Time.now
66
+ @statusmsg_mode = nil
67
+
68
+ parse
69
+
70
+ @bot.logger.debug "Message Raw: #{@raw}"
71
+ end
72
+
73
+ # 回复消息
74
+ def reply(text, **args)
75
+ to_user = args[:username] || @from.username
76
+ to_user = @bot.contact_list.find(nickname: args[:nickname]) if args[:nickname]
77
+
78
+ message_type = args[:type] || :text
79
+ @bot.client.send(message_type, to_user, text)
80
+ end
81
+
82
+ # 解析微信消息
83
+ #
84
+ # @return [void]
85
+ def parse
86
+ parse_source
87
+ parse_kind
88
+
89
+ message = @raw["Content"].convert_emoji
90
+ message = CGI.unescape_html(message) if @kinde != Message::Kind::Text
91
+ if match = group_message(message)
92
+ from_username = match[0]
93
+ message = match[1]
94
+ end
95
+
96
+ @message = message
97
+ @from = @bot.contact_list.find(username: @raw["FromUserName"])
98
+ parse_emoticon if @kind == Message::Kind::Emoticon
99
+
100
+ case @kind
101
+ when Message::Kind::ShareLink
102
+ @meta_data = MessageData::ShareLink.parse(@message)
103
+ end
104
+
105
+ parse_events
106
+ end
107
+
108
+ # 消息匹配
109
+ #
110
+ # @param [String, Regex, Pattern] regexp 匹配规则
111
+ # @param [String, Symbol] type 消息类型
112
+ # @return [MatchData] 匹配结果
113
+ def match(regexp, type)
114
+ # text = ""
115
+ # case type
116
+ # when :ctcp
117
+ # text = ctcp_message
118
+ # when :action
119
+ # text = action_message
120
+ # else
121
+ # text = message.to_s
122
+ # type = :other
123
+ # end
124
+
125
+ # if strip_colors
126
+ # text = Cinch::Formatting.unformat(text)
127
+ # end
128
+
129
+ @message.match(regexp)
130
+ end
131
+
132
+ # 解析消息来源
133
+ #
134
+ # 特殊账户/群聊/公众号/用户
135
+ #
136
+ # @return [void]
137
+ def parse_source
138
+ @source = if @bot.config.special_users.include?(@raw["FromUserName"])
139
+ # 特殊账户
140
+ Contact::Kind::Special
141
+ elsif @raw["FromUserName"].include?("@@")
142
+ # 群聊
143
+ Contact::Kind::Group
144
+ elsif (@raw["RecommendInfo"]["VerifyFlag"] & 8) != 0
145
+ # 公众号
146
+ Contact::Kind::MP
147
+ else
148
+ # 普通用户
149
+ Contact::Kind::User
150
+ end
151
+ end
152
+
153
+ # 解析消息类型
154
+ #
155
+ # - 1: Text 文本消息
156
+ # - 3: Image 图片消息
157
+ # - 34: Voice 语言消息
158
+ # - 42: BusinessCard 名片消息
159
+ # - 47: Emoticon 微信表情
160
+ # - 49: ShareLink 分享链接消息
161
+ # - 62: ShortVideo 短视频消息
162
+ # - 1000: System 系统消息
163
+ # - Unkown 未知消息
164
+ #
165
+ # @return [void]
166
+ def parse_kind
167
+ @kind = case @raw["MsgType"]
168
+ when 1
169
+ Message::Kind::Text
170
+ when 3
171
+ Message::Kind::Image
172
+ when 34
173
+ Message::Kind::Voice
174
+ when 42
175
+ Message::Kind::BusinessCard
176
+ when 62
177
+ Message::Kind::ShortVideo
178
+ when 47
179
+ Message::Kind::Emoticon
180
+ when 49
181
+ Message::Kind::ShareLink
182
+ when 10000
183
+ Message::Kind::System
184
+ else
185
+ Message::Kind::Unkown
186
+ end
187
+ end
188
+
189
+ # 解析 Handler 的事件
190
+ #
191
+ # - `:message` 用户消息
192
+ # - `:text` 文本消息
193
+ # - `:group` 群聊消息
194
+ # - `:at_message` @ 消息
195
+ #
196
+ # @return [void]
197
+ def parse_events
198
+ @events << :message
199
+ @events << @kind
200
+ @events << @source
201
+
202
+ if @source == :group && @raw["Content"] =~ /@([^\s]+)\s+(.*)/
203
+ @events << :at_message
204
+ end
205
+ end
206
+
207
+ # 解析表情
208
+ #
209
+ # 表情分为两种:
210
+ # 1. 微信商店表情
211
+ # 1. 自定义表情
212
+ #
213
+ # @return [void]
214
+ def parse_emoticon
215
+ if @message.empty?
216
+ @media_id = @raw["MediaId"]
217
+ # TODO: 解决微信商店表情
218
+ # file = @bot.client.download_image(@raw["NewMsgId"])
219
+ else
220
+ data = MultiXml.parse(@message)
221
+ @media_id = data["msg"]["emoji"]["md5"]
222
+ end
223
+ end
224
+
225
+ def parse_share
226
+ data = MultiXml.parse(@message)
227
+
228
+ end
229
+
230
+ # 解析用户的群消息
231
+ #
232
+ # 群消息格式:
233
+ # @FromUserName:<br>Message
234
+ #
235
+ # @param [String] message 原始消息
236
+ # @return [Array<Object>] 返回两个值的数组
237
+ # - 0 from_username
238
+ # - 1 message
239
+ def group_message(message)
240
+ if match = GROUP_MESSAGE_REGEX.match(message)
241
+ return [match[1], at_message(match[2])]
242
+ end
243
+
244
+ false
245
+ end
246
+
247
+ # 尝试解析群聊中的 @ 消息
248
+ #
249
+ # 群消息格式:
250
+ # @ToNickNameUserName Message
251
+ #
252
+ # @param [String] message 原始消息
253
+ # @return [String] 文本消息,如果不是 @ 消息返回原始消息
254
+ def at_message(message)
255
+ if match = AT_MESSAGE_REGEX.match(message)
256
+ return match[2].strip
257
+ end
258
+
259
+ message
260
+ end
261
+ end
262
+ end
@@ -0,0 +1,46 @@
1
+ module WeChat::Bot::MessageData
2
+ class ShareLink
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.0.alpha"
4
+ end
5
+ end
@@ -0,0 +1 @@
1
+ require "wechat/bot"
@@ -0,0 +1,37 @@
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-bot"
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.add_development_dependency "bundler", "~> 1.14"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "rspec", "~> 3.0"
27
+ spec.add_development_dependency "webmock"
28
+ spec.add_development_dependency "awesome_print"
29
+
30
+ spec.add_dependency "colorize", "~> 0.8.1"
31
+ spec.add_dependency "http", "~> 2.2.2"
32
+ spec.add_dependency "rqrcode", "~> 0.10.1"
33
+ spec.add_dependency "multi_xml", "~> 0.6.0"
34
+ # spec.add_dependency "representable", "~> 3.0.4"
35
+ # spec.add_dependency "roxml", "~> 3.3.1"
36
+ # spec.add_dependency "gemoji"
37
+ end
metadata ADDED
@@ -0,0 +1,206 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: wechat-bot
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.alpha
5
+ platform: ruby
6
+ authors:
7
+ - icyleaf
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-06-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.14'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.14'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: webmock
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: awesome_print
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: colorize
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.8.1
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.8.1
97
+ - !ruby/object:Gem::Dependency
98
+ name: http
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 2.2.2
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 2.2.2
111
+ - !ruby/object:Gem::Dependency
112
+ name: rqrcode
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 0.10.1
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 0.10.1
125
+ - !ruby/object:Gem::Dependency
126
+ name: multi_xml
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 0.6.0
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 0.6.0
139
+ description: WeChat Bot for Ruby with personal account
140
+ email:
141
+ - icyleaf.cn@gmail.com
142
+ executables: []
143
+ extensions: []
144
+ extra_rdoc_files: []
145
+ files:
146
+ - ".gitignore"
147
+ - ".rspec"
148
+ - ".rubocop.yml"
149
+ - ".ruby-gemset"
150
+ - ".ruby-version"
151
+ - ".travis.yml"
152
+ - ".vscode/launch.json"
153
+ - ".yardopts"
154
+ - CODE_OF_CONDUCT.md
155
+ - Gemfile
156
+ - LICENSE.txt
157
+ - README.md
158
+ - Rakefile
159
+ - lib/wechat-bot.rb
160
+ - lib/wechat/bot.rb
161
+ - lib/wechat/bot/cached_list.rb
162
+ - lib/wechat/bot/callback.rb
163
+ - lib/wechat/bot/client.rb
164
+ - lib/wechat/bot/configuration.rb
165
+ - lib/wechat/bot/contact.rb
166
+ - lib/wechat/bot/contact_list.rb
167
+ - lib/wechat/bot/core.rb
168
+ - lib/wechat/bot/ext/wechat_emoji_string.rb
169
+ - lib/wechat/bot/handler.rb
170
+ - lib/wechat/bot/handler_list.rb
171
+ - lib/wechat/bot/helper.rb
172
+ - lib/wechat/bot/http/adapter/js.rb
173
+ - lib/wechat/bot/http/adapter/xml.rb
174
+ - lib/wechat/bot/http/session.rb
175
+ - lib/wechat/bot/logger.rb
176
+ - lib/wechat/bot/message.rb
177
+ - lib/wechat/bot/message_data/share_link.rb
178
+ - lib/wechat/bot/pattern.rb
179
+ - lib/wechat/bot/version.rb
180
+ - lib/wechat_bot.rb
181
+ - wechat-bot.gemspec
182
+ homepage: https://github.com/icyleaf/wechat-bot
183
+ licenses:
184
+ - MIT
185
+ metadata: {}
186
+ post_install_message:
187
+ rdoc_options: []
188
+ require_paths:
189
+ - lib
190
+ required_ruby_version: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ required_rubygems_version: !ruby/object:Gem::Requirement
196
+ requirements:
197
+ - - ">"
198
+ - !ruby/object:Gem::Version
199
+ version: 1.3.1
200
+ requirements: []
201
+ rubyforge_project:
202
+ rubygems_version: 2.6.12
203
+ signing_key:
204
+ specification_version: 4
205
+ summary: WeChat Bot for Ruby
206
+ test_files: []