diff --git a/shard.yml b/shard.yml index 8bb7853..015c1cd 100644 --- a/shard.yml +++ b/shard.yml @@ -8,6 +8,6 @@ targets: bootstrap: main: src/main.cr -crystal: 1.11.2 +crystal: ">= 1.20.0" license: MIT diff --git a/src/error/usecase.cr b/src/error/usecase.cr index 178b6c8..b785e8c 100644 --- a/src/error/usecase.cr +++ b/src/error/usecase.cr @@ -12,7 +12,7 @@ module Error pretext: "<@#{ENV["SLACK_ID"]}> #{message}", color: "#EB4646", title: err.message, - text: err.backtrace.join('\n'), + text: err.backtrace?.try(&.join('\n')), footer: "github_notifications_slack (#{ENV["ENV"]})", footer_icon: "", ) diff --git a/src/github/models.cr b/src/github/models.cr index 685f1fb..d39f033 100644 --- a/src/github/models.cr +++ b/src/github/models.cr @@ -4,36 +4,34 @@ module Github class Notifications include JSON::Serializable - property subject : Subject - property reason : String - property repository : Repository - property subscription_url : String? + MENTION_REASONS = { + "assign", + "author", + "comment", + "invitation", + "mention", + "team_mention", + "review_requested", + # "ci_activity", + } + + getter subject : Subject + getter reason : String + getter repository : Repository + getter subscription_url : String? def mention? : Bool - [ - "assign", - "author", - "comment", - "invitation", - "mention", - "team_mention", - "review_requested", - # "ci_activity", - ].includes?(reason) + reason.in?(MENTION_REASONS) end end class Subject include JSON::Serializable - property type : String - property title : String? - - @[JSON::Field(emit_null: false)] - property url : String = "" - - @[JSON::Field(emit_null: false)] - property latest_comment_url : String = "" + getter type : String + getter title : String? + getter url : String = "" + getter latest_comment_url : String = "" module Type PULL_REQUEST = "PullRequest" @@ -42,13 +40,15 @@ module Github DISCUSSION = "Discussion" end + UPDATE_TYPES = { + Type::PULL_REQUEST, + Type::ISSUE, + Type::COMMIT, + Type::DISCUSSION, + } + def update? : Bool - [ - Type::PULL_REQUEST, - Type::ISSUE, - Type::COMMIT, - Type::DISCUSSION, - ].includes?(type) + type.in?(UPDATE_TYPES) end def color : String @@ -67,28 +67,24 @@ module Github end def comment_url : String - if !latest_comment_url.blank? - latest_comment_url - else - url - end + latest_comment_url.presence || url end end class Repository include JSON::Serializable - property full_name : String? - property html_url : String? - property owner : User + getter full_name : String? + getter html_url : String? + getter owner : User end class Comment include JSON::Serializable - property user : User - property html_url : String? - property body : String? + getter user : User + getter html_url : String? + getter body : String? def initialize(@body) @user = User.new @@ -98,9 +94,9 @@ module Github class User include JSON::Serializable - property login : String? - property avatar_url : String? - property html_url : String? + getter login : String? + getter avatar_url : String? + getter html_url : String? def initialize end @@ -109,7 +105,7 @@ module Github class Error include JSON::Serializable - property message : String - property documentation_url : String? + getter message : String + getter documentation_url : String? end end diff --git a/src/github/repository.cr b/src/github/repository.cr index 14012eb..57abcd0 100644 --- a/src/github/repository.cr +++ b/src/github/repository.cr @@ -16,20 +16,20 @@ module Github def find_notifications_unread : Array(Notifications) res = @github.get "/notifications" - if res.status_code >= 500 + if res.status.server_error? Serverless::Lambda.print_log "return 5xx error from notifications api" - return Array(Notifications).new - elsif res.status_code == 401 + return [] of Notifications + elsif res.status.unauthorized? # GitHub が断続的に 401 を返すことがあるため、毎分の次回実行に任せてスキップする。 # トークン失効などの恒久的な 401 までサイレントに握りつぶす点は本来リトライや # 連続失敗の監視で区別すべきだが、個人用途の通知ツールであり実装コストに # 見合わないため割り切る。 Serverless::Lambda.print_log "return 401 error from notifications api, skip" - return Array(Notifications).new - elsif res.status_code >= 400 + return [] of Notifications + elsif res.status.client_error? Serverless::Lambda.print_log "return 4xx error from notifications api" err = Error.from_json res.body - raise "notifications api retrun client error: #{err.message}" + raise "notifications api return client error: #{err.message}" end Serverless::Lambda.print_log "notifications body: #{res.body}" @@ -43,21 +43,21 @@ module Github end res = @github.get url - if res.status_code >= 500 + if res.status.server_error? Serverless::Lambda.print_log "return 5xx error from comments api" - return Comment.new "comments api retrun server error" - elsif res.status_code >= 400 + return Comment.new "comments api return server error" + elsif res.status.client_error? Serverless::Lambda.print_log "return 4xx error from comments api" err = Error.from_json res.body - return Comment.new "comments api retrun client error: #{err.message}" + return Comment.new "comments api return client error: #{err.message}" end begin Serverless::Lambda.print_log "comment body: #{res.body}" Comment.from_json res.body rescue - Serverless::Lambda.print_log "faild parse comment data" - Comment.new "faild parse comment data" + Serverless::Lambda.print_log "failed parse comment data" + Comment.new "failed parse comment data" end end diff --git a/src/github/usecase.cr b/src/github/usecase.cr index fd749a5..084430b 100644 --- a/src/github/usecase.cr +++ b/src/github/usecase.cr @@ -18,7 +18,7 @@ module Github title: notify.subject.title, title_link: comment.html_url, text: comment.body, - footer: !notify.repository.full_name.nil? ? notify.repository.full_name : "github", + footer: notify.repository.full_name || "github", footer_icon: notify.repository.owner.avatar_url, ) end diff --git a/src/notify/usecase.cr b/src/notify/usecase.cr index 84c059f..3b40fea 100644 --- a/src/notify/usecase.cr +++ b/src/notify/usecase.cr @@ -24,7 +24,7 @@ module Notify github_uc.to_slack_attachment item, pretext, message end - if notices.size != 0 + unless notices.empty? slack_repo = Slack::PostRepository.new ENV["WEBHOOK_URL"] slack_repo.send_attachments notices diff --git a/src/runtime/lambda.cr b/src/runtime/lambda.cr index 932fdae..ce70d76 100644 --- a/src/runtime/lambda.cr +++ b/src/runtime/lambda.cr @@ -1,11 +1,14 @@ require "json" +require "log" require "http/client" module Serverless module Lambda extend self - def handler(name : String) + Log = ::Log.for("lambda") + + def handler(name : String, &) return if name != ENV["_HANDLER"] ENV["SSL_CERT_FILE"] = "/etc/pki/tls/cert.pem" @@ -32,10 +35,15 @@ module Serverless end end + # CloudWatch では改行ごとにログエントリが分割されるため、改行を除去して + # 1 エントリにまとめつつ、長い本文は適度なチャンクに分割して出力する。 + # 中間の Array(Char) を作らずに済むよう文字列スライスで分割する。 def print_log(log : String) - log.split(//).each_slice(50000) do |line| - puts `echo '#{line.join.gsub(/(\r\n|\r|\n|\f)/, "")}'` - STDOUT.flush + cleaned = log.gsub(/(\r\n|\r|\n|\f)/, "") + offset = 0 + while offset < cleaned.size + Log.info { cleaned[offset, 50000] } + offset += 50000 end end end diff --git a/src/slack/models.cr b/src/slack/models.cr index 4711b2a..107777d 100644 --- a/src/slack/models.cr +++ b/src/slack/models.cr @@ -4,17 +4,17 @@ module Slack class Attachment include JSON::Serializable - property fallback : String? - property author_name : String? - property author_icon : String? - property author_link : String? - property pretext : String? - property color : String? - property title : String? - property title_link : String? - property text : String? - property footer : String? - property footer_icon : String? + getter fallback : String? + getter author_name : String? + getter author_icon : String? + getter author_link : String? + getter pretext : String? + getter color : String? + getter title : String? + getter title_link : String? + getter text : String? + getter footer : String? + getter footer_icon : String? def initialize( @fallback = nil, @@ -27,7 +27,7 @@ module Slack @title_link = nil, @text = nil, @footer = nil, - @footer_icon = nil + @footer_icon = nil, ) end end @@ -35,7 +35,7 @@ module Slack class Post include JSON::Serializable - property attachments : Array(Attachment) + getter attachments : Array(Attachment) def initialize(@attachments) end