wechat-bot 0.1.0.alpha
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.
- 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
|