この投稿は、Crystal Advent Calendar 2015の20日目のもの。
ひとまず 0.9.1で動くようにする
Crystalはまだまだ開発中のため破壊的変更が随所で行われている。その影響で昔作ったこのbotがいろいろと動かなかったため、動くところまで変更に追従した。
新しいバージョンへの移行例としてちょうど良い題材だったから残しておく。
実行結果
Crystal側の変更点
修正diff
というスタイルで記述していく。
CGIモジュールが無くなっていた
Error in ./src/main.cr:1: while requiring "./crybot"
require "./crybot"
^
in ./src/crybot.cr:2: while requiring "cgi": can't find file 'cgi' relative to '/Users/mihyaeru/pj/crystal/crybot/src'
require "cgi"
^
0.9.0
で変わったらしい。
(breaking change) The CGI module's funcionality has been moved to URI and HTTP::Params
これでOK。
diff --git a/src/crybot.cr b/src/crybot.cr
index 0fca99d..05487ae 100644
--- a/src/crybot.cr
+++ b/src/crybot.cr
@@ -1,5 +1,5 @@
require "./crybot/*"
-require "cgi"
+require "uri"
class Crybot
def initialize
@@ -26,7 +26,7 @@ class Crybot
name = user["screen_name"]?
text = (text as String).gsub("@crybot21 ", "")
tweet_test = "@#{name} #{text}"
- p self.tweet(CGI.escape(tweet_test))
+ p self.tweet(URI.escape(tweet_test))
p tweet_test
end
end
Timeのインタフェースが変わっていた
Error in ./src/main.cr:6: instantiating 'Crybot#user_stream()'
bot.user_stream
^~~~~~~~~~~
in ./src/crybot.cr:18: instantiating 'Client#start_stream(String, String)'
@client.start_stream "GET", "/user.json", do |body|
^~~~~~~~~~~~
in ./src/crybot/client.cr:32: instantiating 'make_request(String, String, String)'
request = make_request(method, STREAM_HOST, path)
^~~~~~~~~~~~
instantiating 'make_request(String, String, String, Nil)'
in ./src/crybot/client.cr:68: undefined method 'to_i' for Time (did you mean 'to_s'?)
request.headers["Authorization"] = @signature.authorization_header(request, true, Time.utc_now.to_i.to_s, SecureRandom.hex(32))
^~~~
================================================================================
Time trace:
/Users/mihyaeru/.anyenv/envs/crenv/versions/0.9.1/src/time/time.cr:224
def self.utc_now
^~~~~~~
/Users/mihyaeru/.anyenv/envs/crenv/versions/0.9.1/src/time/time.cr:225
new utc_ticks, Kind::Utc
^~~
0.7.6
で変わったらしい。
(breaking change) Renamed Time.at to Time.epoch, and Time#to_i and Time#to_f to Time#epoch and Time#epoch_f
これでOK。
diff --git a/src/crybot/client.cr b/src/crybot/client.cr
index 07a7799..c6d7868 100644
--- a/src/crybot/client.cr
+++ b/src/crybot/client.cr
@@ -65,7 +65,7 @@ class Client
request = HTTP::Request.new(method, API_VERSION + path, body: body)
request.headers["Host"] = host
request.headers["Content-type"] = "application/x-www-form-urlencoded" if method == "POST"
- request.headers["Authorization"] = @signature.authorization_header(request, true, Time.utc_now.to_i.to_s, SecureRandom.hex(32))
+ request.headers["Authorization"] = @signature.authorization_header(request, true, Time.utc_now.epoch.to_s, SecureRandom.hex(32))
return request
end
IOのインタフェースが変わっていた
Error in ./src/main.cr:6: instantiating 'Crybot#user_stream()'
bot.user_stream
^~~~~~~~~~~
in ./src/crybot.cr:18: instantiating 'Client#start_stream(String, String)'
@client.start_stream "GET", "/user.json", do |body|
^~~~~~~~~~~~
in ./src/crybot/client.cr:47: no overload matches 'OpenSSL::SSL::Socket#read' with types Int32
Overloads are:
- OpenSSL::SSL::Socket#read(slice : Slice(UInt8))
- OpenSSL::SSL::Socket#read(slice : Slice(UInt8))
line += io.read(chunk_size)
^~~~
0.9.0
で変わったらしい。
(breaking change) IO#read() is now IO#gets_to_end. Removed IO#read(count), added IO#skip(count)
これでOK...
diff --git a/src/crybot/client.cr b/src/crybot/client.cr
index c6d7868..94ae2a6 100644
--- a/src/crybot/client.cr
+++ b/src/crybot/client.cr
@@ -44,7 +44,7 @@ class Client
# read body
line = ""
while (chunk_size = io.gets.not_nil!.to_i(16)) > 0
- line += io.read(chunk_size)
+ line += io.gets(chunk_size) as String
io.gets # Read \r\n
if line =~ /^\s+$/
と思いきや、実行はできたけども実行時エラーに。
{"friends" => [198177363]}
invalid Int32:
(ArgumentError)
[4347956178] *CallStack::unwind:Array(Pointer(Void)) +82
[4347956081] *CallStack#initialize<CallStack>:Array(Pointer(Void)) +17
[4347956040] *CallStack::new:CallStack +40
[4347955297] *Exception@Exception#initialize<ArgumentError, String>:CallStack +33
[4347955229] *ArgumentError#initialize<ArgumentError, String>:CallStack +29
[4347955169] *ArgumentError::new<String>:ArgumentError +97
[4347945122] *String#to_i32<String, Int32, Bool, Bool, Bool, Bool>:Int32 +226
[4347944885] *String#to_i<String, Int32, Bool, Bool, Bool, Bool>:Int32 +37
[4347950319] *String#to_i<String, Int32>:Int32 +79
[4348087281] *Crybot#user_stream<Crybot>:Nil +625
[4347913599] __crystal_main +24159
[4347923200] main +32
どうやらread(count)
とgets(count)
では読み取り時の挙動が若干違うらしく、io.gets
で"\r\n"
を読み飛ばす辺りで読み飛ばせず、次のループのio.gets.not_nil!.to_i(16)
にて"\r\n".to_i(16)
になるように変わったようだ。
これで今度こそ動いた。
diff --git a/src/crybot/client.cr b/src/crybot/client.cr
index 94ae2a6..1fa6282 100644
--- a/src/crybot/client.cr
+++ b/src/crybot/client.cr
@@ -43,9 +43,12 @@ class Client
# read body
line = ""
- while (chunk_size = io.gets.not_nil!.to_i(16)) > 0
+ while true
+ chunk_size_str = io.gets as String
+ next if chunk_size_str == "\r\n"
+ chunk_size = chunk_size_str.to_i(16)
+
line += io.gets(chunk_size) as String
- io.gets # Read \r\n
if line =~ /^\s+$/
line = ""
Streamingを使う
このbotを書いた2015年7月29日時点ではHTTP::Client
にStreaming機能が存在しておらずSocket周りを頑張って自前で実装した。東京 Crystal 勉強会 #1でこのbotの制作をLTした同日、7月30日に公開された0.7.5でStreamingがサポートされるというタイミングの悪さを発揮した。
発表以降、とくに何も手を加えていなかったが、Advent Calendarを機にStreamingの機能を使った版に書き換えてみた。
長々と自前で頑張っていた辛さあふれるこのメソッドが、
def start_stream(method, path)
request = make_request(method, STREAM_HOST, path)
io = make_socket(request)
request.to_io(io)
# read header
headers = HTTP::Headers.new
while line = io.gets
break if line == "\r\n" || line == "\n"
name, value = HTTP.parse_header(line)
headers.add(name, value)
end
# read body
line = ""
while true
chunk_size_str = io.gets as String
next if chunk_size_str == "\r\n"
chunk_size = chunk_size_str.to_i(16)
line += io.gets(chunk_size) as String
if line =~ /^\s+$/
line = ""
next
end
begin
yield JSON.parse(line) as Hash
line = ""
rescue e
p e # FIXME: !!!
end
end
end
private def make_socket(request)
socket = TCPSocket.new(request.headers["Host"], 443)
socket.sync = false
return OpenSSL::SSL::Socket.new(socket)
end
こんな感じですっきりと書けた。すごい!
def start_stream(method, path)
request = make_request(method, STREAM_HOST, path)
@stream_client.exec(request) do |response|
while true
line = response.body_io.gets as String
next if line =~ /^\s+$/
yield JSON.parse(line) as Hash
end
end
end
終わりに
当初の予定ではStreamingの機能を使う変更だけで記事を書こうと思っていたけども、Crystalのバージョンアップで動かなくなった部分が多々あり併記する形にした。新旧バージョンの比較ができて良い感じに収まったと個人的に思っている。
明日の担当は@5t111111さんの予定。
参考
- CrystalのChange Log: https://github.com/manastech/crystal/blob/master/CHANGELOG.md