コンテンツにスキップ

2023

日記 - 1か月ぶりに自宅へ帰ってきた

  • 7月10日から妻実家に泊まっていました。約1か月もお世話になっていました。あっという間!
  • 本日は自宅へ帰る日でした。自分だけ先に帰って、妻&子どもは翌日に帰宅します
  • 電車で網棚に置いた荷物を取り忘れてしまった…JRから連絡があって郵送で届けてくれるとのこと。ラッキー!
  • 帰宅して速攻で大きいものの洗濯しました。ソファーカバーとか。ずっと洗ってなかったし子どもが使うので。
  • めっちゃ久しぶりにパソコン触った。1か月も使わなかった日って今までなかったんじゃないかな?
  • ビール飲みながら書いてます

Line Messaging APIを使って画像・動画を取得する

意外とMessaging APIについての情報が少ないので、メモとして残しておきます。

先にやっておくこと

  • Line DevelopersからMessging APIのチャネルを作っている
  • Webhook設定済みである
  • 私はLambdaの関数 URLを設定して確認した
  • そのため、サンプルコードもLambda前提である
  • チャネルアクセストークンを発行済みであること

サンプルコード書いていますが、そこではS3を使っています。必要に応じてその辺の設定もしてください。

手順

  1. Lineから作っておいたチャネルを友だち登録する
  2. チャネルに画像または動画を投稿する

そうすると、LineからWebhookへリクエストされます。

サンプルコード

以下は、Lineからデータを取得してS3にアップロードするLambda + Pythonのサンプルコードです。

import json
import os
import urllib.request

import boto3

CHANNEL_ACCESS_TOKEN = "your token"
S3_BUCKET = "your bucket"

def lambda_handler(event, context):
    print(json.dumps(event))

    for message_event in json.loads(event["body"])["events"]:
        message_type = message_event["message"]["type"]

        # 画像・動画以外は受け付けない
        if message_type not in ["image", "video"]:
            continue

        message_id = message_event["message"]["id"]

        # 画像・動画ファイルを取得する
        content = fetch_content(message_id=message_id)

        # 画像・動画ファイルをS3にアップロード
        original_filename = os.path.join("original", message_id)
        upload_s3(bin=content, filename=original_filename)

        # プレビュー画像を取得する
        preview_content = fetch_preview_content(message_id=message_id)

        # プレビュー画像をS3にアップロード
        preview_filename = os.path.join("preview", message_id)
        upload_s3(bin=preview_content, filename=preview_filename)

    return {"statusCode": 200, "body": json.dumps("Hello from Lambda!")}


def fetch_content(message_id: str) -> bytes:
    url = f"https://api-data.line.me/v2/bot/message/{message_id}/content"
    return request_get(url=url)


def fetch_preview_content(message_id: str) -> bytes:
    url = f"https://api-data.line.me/v2/bot/message/{message_id}/content/preview"
    return request_get(url=url)


def request_get(url: str) -> bytes:
    headers = {
        "Content-Type": "application/json; charset=UTF-8",
        "Authorization": f"Bearer {CHANNEL_ACCESS_TOKEN}",
    }
    req = urllib.request.Request(url, method="GET", headers=headers)
    with urllib.request.urlopen(req) as res:
        return res.read()


def upload_s3(bin: bytes, filename: str) -> str:
    s3 = boto3.resource("s3")
    bucket = s3.Bucket(S3_BUCKET)
    bucket.put_object(Body=bin, Key=filename)

eventログ

body部分だけ参考として載せます。

画像

{
  "body": "{\"destination\":\"U668a9c2c0b1469cd8d5e984672961913\",\"events\":[{\"type\":\"message\",\"message\":{\"type\":\"image\",\"id\":\"463074078431562241\",\"contentProvider\":{\"type\":\"line\"}},\"webhookEventId\":\"01H4VG23S8TRNE9XJVEEE3HHFP\",\"deliveryContext\":{\"isRedelivery\":false},\"timestamp\":1688844963119,\"source\":{\"type\":\"user\",\"userId\":\"U0b465adc30fb8ac1fc6a414d06a0b1c7\"},\"replyToken\":\"fc66ac7d07bb408aa0f272cfb384d0f8\",\"mode\":\"active\"}]}"
}

動画

{
  "body": "{\"destination\":\"U668a9c2c0b1837cd1d5e984672961913\",\"events\":[{\"type\":\"message\",\"message\":{\"type\":\"video\",\"id\":\"463076447542233697\",\"duration\":34208,\"contentProvider\":{\"type\":\"line\"}},\"webhookEventId\":\"01H4VHD7F932NQFT58QE4FCB13\",\"deliveryContext\":{\"isRedelivery\":false},\"timestamp\":1688846375921,\"source\":{\"type\":\"user\",\"userId\":\"U0b465adc30fb8ac1fc6a414d06a0b1c7\"},\"replyToken\":\"e2b900a4dbef4f088a70d0097873dc60\",\"mode\":\"active\"}]}"
}

ドキュメント

  • https://developers.line.biz/ja/reference/messaging-api/#getting-content

育児休暇をとった

2023年6月某日に娘が産まれました。結婚してから約6年、2人にとっては念願の子供です。

長かった。私も妻も結婚したら何事もなく子供が産まれると思っていました。でも、うまくいかない時期が続きました。とくに妻は相当大変だったと思います。

  • いくつかの大学病院で検査した
  • 漢方を試した
  • 不妊不育治療のクリニックに通った

など、いろいろ試行錯誤しました。

また、2022年には東京から岐阜に引っ越しました。周りに友だちがいない環境です。しかし、いい影響を与えたと2人とも感じています。

こんなことをしながら、ようやくの出産です。本当に良かった。

7、8月の2か月の間、育児休暇を取ります。良き父親になれるよう精進したいと思います。

ChatGPTにMarkdown記法の例を書いてもらった

このサイトのスタイルを確認したかったので、ChatGPTに確認用のテキストをMarkdownで生成してもらいました。

生成時に指示した内容はこれです。適当です。 「Markdown記法をふんだんに使った文章を作成してください。内容は問いません。」

自分で考えるのがめんどくさいときにGPTを利用するのは非常に楽でいいですね!


見出し

これは見出しの例です。Markdown では、# の数で見出しのレベルを指定します。

リスト

以下はリストの例です。

  • 項目1
  • 項目2
  • 項目3

番号付きリストの例もあります。

  1. 1つ目の項目
  2. 2つ目の項目
  3. 3つ目の項目

強調

斜体太字 のテキストを使用することもできます。

リンク

リンクのテキストを作成するには、角かっこ [ ] と丸かっこ ( ) を使用します。

[リンクのテキスト](https://www.example.com)

画像

画像を表示するには、次のように書きます。

![代替テキスト](https://blog.mah.blue/next.svg)

代替テキスト

引用

引用を表すためには、行の先頭に > を追加します。

引用の例です。引用文はこのように表示されます。

コード

コードのブロックを表示するには、バッククォート ` を使用します。

print("Hello, World!")

表を作成するには、パイプ | とハイフン - を使用します。

| 名前 | 年齢 | | ---- | ---- | | 田中 | 25 | | 山田 | 30 | | 佐藤 | 28 |

名前 年齢
田中 25
山田 30
佐藤 28

これらは Markdown 記法の一部の例です。Markdown を使用すると、テキストを簡単に整形して見やすくできます。


これは追加のパラグラフです。Markdown はテキストの書式設定や整形を行うための便利な方法です。シンプルで読みやすく、さまざまな要素を組み合わせることができます。見出しやリスト、強調、引用、コードブロック、表など、さまざまな要素を活用できます。また、リンクや画像の挿入も簡単に行うことができます。Markdown を使えば、文書を見やすく整理し、情報をわかりやすく伝えることができます。

さらにもう 1 つのパラグラフです。Markdown は多くのプラットフォームやツールで広くサポートされています。メモやドキュメント、ブログ記事、README ファイルなど、さまざまな場面で活用できます。記法も比較的シンプルで覚えやすく、初心者にも取り組みやすいです。Markdown を使って情報を整理し、魅力的な文章を作成しましょう。

CypressでE2Eテストのカバレッジを測定する

Next.js で作成したアプリケーションに対して Cypress を使って E2E テスト実施時にカバレッジを測定するようにしました。

ライブラリの追加

npm install -D @cypress/code-coverage babel-plugin-istanbul

インストール後

package.json

  "dependencies": {
    "next": "13.0.6",
    "react": "18.2.0",
    "react-dom": "18.2.0",
  },
  "devDependencies": {
+   "@cypress/code-coverage": "^3.10.7",
+   "babel-plugin-istanbul": "^6.1.1",
    "cypress": "^12.5.1",
  }

ライブラリを使うための設定

ファイルを作成する

以下にファイルを作成してカバレッジを測定するための設定をします。

cypress/support/e2e.js

import "@cypress/code-coverage/support";

.babelrc

{
  "presets": ["next/babel"],
  "plugins": ["istanbul"]
}

pages/api/__coverage.js

module.exports = require("@cypress/code-coverage/middleware/nextjs");

Server Side のコードカバレッジを取得するために、@cypress/code-coverageプラグインはエンドポイントを必要とします。このエンドポイントは、Next.js の API 規約にしたがって pages/api/coverage.js ファイルに実装されています。このエンドポイントは、既存のグローバルカバレッジオブジェクトを返すか、NULL を返すだけです。

設定ファイルの追記

Cypress プラグインは、cypress.config.json ファイルから環境変数を使用して正しいエンドポイントを要求します。

cypress.config.js

module.exports = defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      require("@cypress/code-coverage/task")(on, config);
      return {
        ...config,
      };
    },
    baseUrl: "http://localhost:3000",
  },
  env: {
    codeCoverage: {
      url: "/api/__coverage__",
    },
  },
});

役に立ったドキュメント

CodeBuildでdocker-composeを立ててpytestを実施する

ユニットテストは書いているけど CI で回せていなかったので、ビルド時にユニットテストを実行する設定しました。

環境

  • ビルドは Chalice を使って書いたコード
  • パッケージ管理は pipenv
  • Lambda のランタイムは Python3.9
  • pytest でテスト
  • データベースとの接続部分は docker でテスト用の DB サーバを立てている

ディレクトリ構造

.
├── Pipfile
├── Pipfile.lock
├── app.py
├── buildspec.yaml
├── chalicelib
├── docker-compose.yml
├── pytest.ini
└── tests

buildspec.yaml

version: 0.2
phases:
  install:
    runtime-versions:
      python: 3.9
    commands:
      - export LANG=ja_JP.utf8 # warning が出てたので設定したけどなくてもよい
      - pipenv sync --dev
  pre_build:
    commands:
      - export VENV_HOME_DIR=$(pipenv --venv)
      - . $VENV_HOME_DIR/bin/activate
      - docker-compose up -d
  build:
    commands:
      - python3 -m pytest tests
    on-failure: ABORT
artifacts:
  files:
    - "**/*"

つまずいた点

docker-compose up -d でエラーになる

原因

コマンド実行権限がないため。CodeBuild の作成時の設定ミスです。

解決策

CodeBuild 設定の環境特権付与にチェックします。

docker-compose 時に too many requests が発生した

原因

Docker Hub の制限のため。

Step 1/2 : FROM postgres:13.3 toomanyrequests: You have reached your pull rate limit. You may increase the limit by authenticating and upgrading: https://www.docker.com/increase-rate-limit Service 'postgresql' failed to build : Build failed

classmethod さんの記事“Too Many Requests.” でビルドが失敗する…。AWS CodeBuild で IP ガチャを回避するために Docker Hub ログインしよう!という話が参考になります。

解決策

Amazon ECR Public Galleryからイメージを pull するように変更します。

ただし、以下の制限があるようです。あまり気になりませんが、注意しておきましょう。 https://aws.amazon.com/jp/blogs/news/docker-official-images-now-available-on-amazon-elastic-container-registry-public/

Amazon ECR Public から任意の AWS リージョンにイメージをプルするお客様は、事実上無制限にダウンロードできます。AWS 外で動作するワークロードについて、AWS 上で認証されていないユーザーは毎月 500 GB のデータダウンロードが可能です。さらにデータをダウンロードしたい場合は、AWS アカウントにサインアップまたはサインインすることで、毎月 5 TB までのデータをダウンロードすることができ、その後は 1 GB あたり 0.09 ドルの料金が掛かります。

Route53 + ALB + EC2でWebサーバを公開する

EC2 に Web サーバ立てて https でアクセスするように設定しようと思ったら完全に忘れてしまってたので残しておく。 キャプチャとか残すのはめんどうなので、大雑把な手順のみ記載する。

ところどころワードに誤りがあるかもしれません。いい感じに読み替えましょう。

前提

  • ドメイン取得済み
  • Route53 の DNS 設定とか済んでいる

手順

  1. ACM で証明書を発行する
  2. EC2 インスタンスを作成して Web サーバを立てる
  3. EC2 に設定しているセキュリティグループのインバウンドに Web サーバのポートを開ける
  4. ターゲットグループを作成する
  5. プロトコル: http
  6. ポート: Web サーバと同じ番号を指定する
  7. ALB を作成する
  8. プロトコル: https
  9. ポート: 443
  10. ターゲット: 4で作成したもの
  11. デフォルトの証明書: 1 で作成したやつ
  12. Route53 でレコードを作成する
  13. レコード名: ドメイン名
  14. レコードタイプ: A
  15. エイリアス: On
  16. トラフィックのルーティング先: 5の ALB

Google Apps ScriptでScrapboxのフィードからsitemap.xmlを生成した

Scrapbox の public プロジェクトを検索エンジンにのせたくて、sitemap.xml を作成してみました。

Google Apps Script をあまり使ったことがないので、ChatGPT に聞いたら実装してくれました。少しだけ手直ししてますが、90%ぐらいは ChatGPT が書いたものです。Google Apps Script で実行すると Google Drive にsitemap.xmlというファイルが作成されます

function myFunction() {
  // RSS feed URL
  const feedUrl = "https://scrapbox.io/api/feed/mahs-note";

  // Base URL of the site
  const baseUrl = "https://scrapbox.io/mahs-note";

  // Fetch RSS feed
  const feed = UrlFetchApp.fetch(feedUrl).getContentText();
  const document = XmlService.parse(feed);
  const root = document.getRootElement();
  const channel = root.getChild("channel");
  const items = channel.getChildren("item");

  // Create sitemap.xml
  var sitemap = '<?xml version="1.0" encoding="UTF-8"?>\n';
  sitemap += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n';

  // Add home page to sitemap
  sitemap += "<url>\n";
  sitemap += "<loc>" + baseUrl + "</loc>\n";
  sitemap += "<changefreq>daily</changefreq>\n";
  sitemap += "<priority>1.0</priority>\n";
  sitemap += "</url>\n";

  // Add entries to sitemap
  sitemap = items.reduce((accumulator, item) => {
    const link = item.getChildText("link");
    const updated = item.getChildText("pubDate");
    const updatedDate = new Date(updated);
    const updatedDateString = updatedDate.toISOString();

    accumulator += "<url>\n";
    accumulator += "<loc>" + link + "</loc>\n";
    accumulator += "<lastmod>" + updatedDateString + "</lastmod>\n";
    accumulator += "<changefreq>daily</changefreq>\n";
    accumulator += "<priority>0.8</priority>\n";
    accumulator += "</url>\n";
    return accumulator;
  }, sitemap);

  sitemap += "</urlset>";

  // Save sitemap.xml to Google Drive
  DriveApp.createFile("sitemap.xml", sitemap, "application/xml");
  Logger.log("Sitemap.xml created");
}

Python3.9 + AWS LambdaでRuntime.ImportModuleError

起きたこと

API Gateway + Lambda の構成でアプリを動かしていたら、API アクセス時にエラーとなってしまいました。

[ERROR] Runtime.ImportModuleError: Unable to import module 'app': cannot import name 'DEFAULT_CIPHERS' from 'urllib3.util.ssl_' (/var/task/urllib3/util/ssl_.py)
Traceback (most recent call last):

なんで?

urllib3 は標準ライブラリのため requirements.txt に記載していません。そのため、おそらく AWS 側で何かあったと考えられます。urllib3 のChangelogを見ると、最新バージョンが v1.x から v2.0 になっています。よくわかんないけど、おそらく AWS 側も v2.0 になったのが原因だと推測しました。

やったこと

requirements.txtに urllib3 を記載しました。

urllib3==1.26.15

これでとりあえず直りました。 そのうち Lambda で v2.0 で動くと思うので、定期的にチェックしていこうと思います。

Next.jsでYoutubeやTwitterのリンクを埋め込む

リンクだけあっても味気ないので、埋め込み対応をしてみました。

前提

  • remark, remark-rehype, rehype-stringify がインストール済みであること
  • npm i --save-dev remark remark-rehype rehype-stringify

対応方法

@remark-embedder/transformer-oembedを使って変換します。

設定方法

@remark-embedder/transformer-oembedをインストールします。

npm i --save-dev @remark-embedder/transformer-oembed

コンポーネント markdownToHtml.js が以下のようなものだとします:

```ts:markdownToHtml.js import { remark } from "remark"; import remarkRehype from "remark-rehype"; import rehypeStringify from "rehype-stringify"; import rehypeExternalLinks from "rehype-external-links"; import remarkEmbedder from "@remark-embedder/core"; import type { Config } from "@remark-embedder/transformer-oembed"; import oembedTransformer from "@remark-embedder/transformer-oembed";

export const markdownToHtml = async (markdown: string) => { const result = await remark() .use(remarkEmbedder, { transformers: [ [ oembedTransformer, { params: { maxwidth: 550, omit_script: true, lang: "ja", dnt: true, }, } as Config, ], ], }) .use(remarkRehype, { allowDangerousHtml: true }) .use(rehypeStringify) .process(markdown); return result.toString(); };

markdownToHtml.js を使ってページを表示します。

```ts:index.tsx
import { InferGetStaticPropsType, NextPage } from "next";
import { markdownToHtml } from "../../lib/markdownToHtml";

type Props = InferGetStaticPropsType<typeof getStaticProps>;

const Home: NextPage<Props> = ({ html }) => {
  return (
    <div
      dangerouslySetInnerHTML={{
        __html: html,
      }}
    ></div>
  );
};

export const getStaticProps = async () => {
  return {
    props: {
      html: await markdownToHtml("https://www.youtube.com/watch?v=dQw4w9WgXcQ"),
    },
  };
};

export default Home;

出力結果

YouTube の場合
<iframe
  width="356"
  height="200"
  src="https://www.youtube.com/embed/dQw4w9WgXcQ?feature=oembed"
  frameborder="0"
  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
  allowfullscreen
  title="Rick Astley - Never Gonna Give You Up (Official Music Video)"
></iframe>
Twitter の場合
<blockquote
  class="twitter-tweet"
  data-width="550"
  data-lang="ja"
  data-dnt="true"
>
  <p lang="en" dir="ltr">
    Creators can now sign up and earn a living directly on Twitter in the EU,
    UK, and EEA.<br /><br />Tap on “Monetization” in settings to apply today.<br /><br />For
    a full list of available countries see our Help Center:
    <a href="https://t.co/YbBw0EVKqJ">https://t.co/YbBw0EVKqJ</a>
  </p>
  — Twitter (@Twitter)
  <a
    href="https://twitter.com/Twitter/status/1649507477325488131?ref_src=twsrc%5Etfw"
    >2023年4月21日</a
  >
</blockquote>

ツイートを以下のような埋め込みにする場合は、widgets.js を使って<iframe>に変換します。

https://twitter.com/Twitter/status/1649507477325488131

```ts:index.tsx import { useEffect } from "react"; import { InferGetStaticPropsType, NextPage } from "next"; import { markdownToHtml } from "../../lib/markdownToHtml";

type Props = InferGetStaticPropsType;

const Home: NextPage = ({ html }) => { useTweetEmbed(); return (

); };

export const getStaticProps = async () => { return { props: { html: await markdownToHtml( "https://twitter.com/Twitter/status/1649507477325488131" ), }, }; };

export const useTweetEmbed = () => { useEffect(() => { const script = document.createElement("script"); script.src = "https://platform.twitter.com/widgets.js"; document.body.appendChild(script);

return () => {
  document.body.removeChild(script);
};

}, []); };

export default Home; ```