wechat-bot 0.1.0.alpha
Sign up to get free protection for your applications and to get access to all the features.
- 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,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,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(obj)
|
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(obj)
|
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
|
@@ -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
|