2023-11-14 (Tue) [長年日記]
_ esaのwebhookでJekyllのページを更新する
会社のサイトをリニューアルしたので、esaのGitHub webhookでニュースの更新をしようとしたけど、ファイル名やfrontmatterが決め打ちでJekyllではうまく行かなかったので、AWS Lambdaで自前のWebhookを実装して更新するようにした。
雑だけど画像の添付にも対応している(弊社は外部からesaの画像を見えない設定にしているので、webhookでダウンロードしてリポジトリにcommitするようにした)。
GitHubの認証はFine-grained personal access tokenを使ったけど、有効期限が最長1年なので更新を忘れそう。
require "openssl"
require "json"
require "rack"
require "octokit"
require "esa"
def lambda_handler(event:, context:)
body = event["body"]
sig = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), ENV["ESA_SECRET"], body)
header_sig = event["headers"]["x-esa-signature"].delete_prefix("sha256=")
unless Rack::Utils.secure_compare(sig, header_sig)
STDERR.puts("Invalid signature: #{sig} != #{header_sig}")
return { statusCode: 400, body: "Invalid signature" }
end
json = JSON.parse(body)
case json["kind"]
when "post_create", "post_update"
if json["post"]["wip"]
puts("WIP: do nothing")
else
year, month, day, title =
json["post"]["name"].scan(%r"Public/News/(\d+)/(\d+)/(\d+)/(.*)")[0]
number = json["post"]["number"]
filename = "news/_posts/#{year}-#{month}-#{day}-#{number}.md"
post_news_entry(filename, title, json["post"]["body_md"])
end
end
{ statusCode: 200, body: "Posted a news entry" }
end
def post_news_entry(filename, title, content)
github = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"])
esa = Esa::Client.new(access_token: ENV["ESA_TOKEN"], current_team: "nacl")
repo = "NaCl-Ltd/www.netlab.jp"
ref = "heads/main"
sha_latest_commit = github.ref(repo, ref).object.sha
sha_base_tree = github.commit(repo, sha_latest_commit).commit.tree.sha
img_files = []
s = content.gsub(%r'src="(https://files.esa.io/uploads/.*?)"') {
basename = File.basename($1)
path = "img/news/#{basename}"
url = esa.signed_url(URI.parse($1).path).body["url"]
data = Net::HTTP.get(URI(url))
sha = github.create_blob(repo, [data].pack("m"), "base64")
img_files << {
:path => path,
:mode => "100644",
:type => "blob",
:sha => sha
}
"src=\"/#{path}\""
}
body = <<EOF
---
title: #{title}
layout: news
---
#{s}
EOF
blob_sha = github.create_blob(repo, [body].pack("m"), "base64")
sha_new_tree = github.create_tree(
repo,
[
{
:path => filename,
:mode => "100644",
:type => "blob",
:sha => blob_sha
},
*img_files
],
{:base_tree => sha_base_tree }
).sha
commit_message = "Committed via esa webhook"
sha_new_commit = github.create_commit(repo, commit_message, sha_new_tree, sha_latest_commit).sha
github.update_ref(repo, ref, sha_new_commit)
end
_ 採用エントリーフォームをAWS Lambda + Amazon SESで作った
採用エントリーフォームをデザイン会社さんがPHPで作ろうとされていたので、自分で作りますんで……ということでAWS Lambda (Ruby 3.2) + Amazon SESで作った。
gemを使うとメンテナンスが面倒だなと思ってmultipart/form-dataのパースやMIMEメールの整形を雑に自前で書いたけど、esaのwebhookの方で結局gemを使うことになってしまったので、こちらもgemを使ってもよかったかもしれない。
require 'json'
require 'uri'
require 'securerandom'
require 'aws-sdk-ses'
Part = Data.define(:name, :value)
Attachment = Data.define(:filename, :body)
def lambda_handler(event:, context:)
form = parse_multipart(event["body"].unpack1("m"))
validate(form)
result = send_email(form)
{ statusCode: 302, headers: { Location: ENV["SUCCESS_URL"] } }
rescue => e
STDERR.puts({
errorMessage: e.message,
errorType: e.class.name,
stackTrace: e.backtrace
}.to_json)
{ statusCode: 302, headers: { Location: ENV["ERROR_URL"] } }
end
def parse_multipart(s)
boundary = s.slice(/\A--.*?\r\n/)
parts = s.byteslice(boundary.bytesize, s.bytesize - 2 - 2 * boundary.bytesize - 2).split("\r\n" + boundary)
parts.each_with_object({}) { |s, h|
part = parse_part(s)
h[part.name] = part.value
}
end
def parse_part(s)
header, value = s.split(/^\r\n/)
cd_params = header.slice(/^Content-Disposition: *form-data;([^\r\n]*)\r\n/)
name = cd_params.slice(/name="(.*?)"/, 1)
filename = cd_params.slice(/filename="(.*?)"/, 1)
if filename
filename.force_encoding("utf-8")
v = Attachment.new(filename, value)
else
value.force_encoding("utf-8")
v = value
end
Part.new(name, v)
end
def validate(form)
%w(name email message).each do |name|
if form[name].nil? || form[name].empty?
raise "#{name} is missing"
end
end
if !form["file"].is_a?(Attachment)
raise "file should be an attachment"
end
end
def send_email(form)
sender = ENV["MAIL_SENDER"]
recipient = ENV["MAIL_RECIPIENT"]
subject = '=?UTF-8?B?5o6h55So5b+c5Yuf?='
boundary = SecureRandom.hex
encoded_file, encoded_filename, file_type = encode_file(form["file"])
raw_message = <<EOF
From: #{sender}
To: #{recipient}
Subject: =?UTF-8?B?5o6h55So5b+c5Yuf?=
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary=#{boundary}
--#{boundary}
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
お名前: #{form["name"]}
メールアドレス: #{form["email"]}
メッセージ・自己PR:
#{form["message"]}
--#{boundary}
Content-Type: #{file_type}
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename*=#{encoded_filename}
#{encoded_file}
EOF
encoded_file2, encoded_filename2, file_type2 = encode_file(form["file2"])
if encoded_file2
raw_message << <<EOF
--#{boundary}
Content-Type: #{file_type2}
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename*=#{encoded_filename2}
#{encoded_file2}
EOF
end
raw_message << "--#{boundary}--\n"
raw_message.gsub!(/\n/, "\r\n")
ses = Aws::SES::Client.new(region: 'ap-northeast-1')
ses.send_raw_email(
destinations: [recipient],
raw_message: {
data: raw_message
},
source: sender
)
puts 'Email sent to ' + recipient
end
def encode_file(file)
return nil if file.nil? || file.body.nil? || file.filename.nil?
encoded_file = [file.body].pack("m")
encoded_filename = "utf8''" + URI.encode_www_form_component(file.filename)
file_type = file.filename.match?(/.pdf\z/i) ?
"application/pdf" : "appliation/octet-stream"
return encoded_file, encoded_filename, file_type
end
2023-11-18 (Sat) [長年日記]
_ NANKANG AW-1
ちょっと前にスタッドレスに換えていたが、今回ははじめて台湾製タイヤにしてみた。4本で39,864円なり。
換えた直後は接地感が無でちょっと怖かったが 、馴染んだのか馴れたのかそれほど気にならなくなった。
雪とか凍結路で早く試してみたい。
2023-11-21 (Tue) [長年日記]
_ jquery.validate.jsのfocusInvalidが動作しない
jquery.validate.jsのfocusInvalidが動作しないケース(もう一回submitボタンを押すとフォーカスが当たる)があったので調べていたら、Lenisの影響だったようで、以下のように一時的にLenisを無効化するようにしたら直った。
$('#entry-form').validate({
...
invalidHandler: function(event, validator) {
lenis.stop();
setTimeout(() => {
lenis.start();
}, 100);
}
});