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.
@@ -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(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