トップ 最新 追記   RSS 1.0 FEED  

Journal InTime


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
Tags: Ruby

_ 採用エントリーフォームを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
Tags: Ruby

2023-11-18 (Sat) [長年日記]

_ NANKANG AW-1

NANKANG AW-1

ちょっと前にスタッドレスに換えていたが、今回ははじめて台湾製タイヤにしてみた。4本で39,864円なり。

換えた直後は接地感が無でちょっと怖かったが 、馴染んだのか馴れたのかそれほど気にならなくなった。

雪とか凍結路で早く試してみたい。

Tags: MAZDA3

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);
    }
  });
Tags: JavaScript