Closed34

Pinata の SDK を使って IPFS を操作する

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

ワークスペースの準備

コマンド
mkdir hello-pinata-sdk
cd hello-pinata-sdk
npm init -y
npm install --save-dev @pinata/sdk dotenv @types/node ts-node
touch .env test-authentication.ts
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

My Files の表示には時間がかかる

ファイル一覧が表示されるまでに 5 秒くらいの時間がかかるみたい

反映されない!と焦らないように気をつけよう

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

API キーの作成

Settings > Developers タブ > New Key で追加できる

Admin にすると全ての API Endpoint Access が有効になるみたい

Limit Max Uses は API キーの使用回数を設定できるようだ

Key Name はわかりやすい名前にする、今回は hello-pinata-sdk にしよう

キーが作成されると下記 3 点が作成される

  • API Key
  • API Secret
  • JWT

API Secret と JWT は二度と表示されないかも知れないので .env に忘れずに控えておく

.env
PINATA_API_KEY="00000000000000000000"
PINATA_API_SECRET="0000000000000000000000000000000000000000000000000000000000000000"
PINATA_JWT="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

認証テスト

test-authentication.ts
import PinataClient from "@pinata/sdk";

async function main() {
  const pinata = new PinataClient({
    pinataApiKey: process.env.PINATA_API_KEY!,
    pinataSecretApiKey: process.env.PINATA_API_SECRET!,
  });

  const { authenticated } = await pinata.testAuthentication();

  console.log(JSON.stringify({ authenticated }, null, 2));
}

main().catch((err) => console.error(err));
コマンド
npx ts-node -r dotenv/config test-authentication.ts
実行結果
{
  "authenticated": true
}

認証は成功している様子だ

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Pinata SDK を使ってできること

Pinning

  • hashMetadata
  • pinByHash
  • pinFileToIPFS
  • pinFromFS
  • pinJobs
  • pinJSONToIPFS
  • unpin
  • userPinPolicy

Data

  • testAuthentication
  • pinList
  • getFilesByCount
  • userPinnedDataTotal
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

ファイルをピンする / pinFileToIPFS

ファイルというか Node.js のストリームをピンする

Pinata で「ピン」は IPFS にコンテンツをアップロードする+ダッシュボードから管理できるようにすることのようだ

準備コマンド
touch pin-file.ts
pin-file.ts
import PinataClient from "@pinata/sdk";
import { Readable } from "stream";

async function main() {
  const pinata = new PinataClient({
    pinataApiKey: process.env.PINATA_API_KEY!,
    pinataSecretApiKey: process.env.PINATA_API_SECRET!,
  });

  const dataToPin = Readable.from('Hello, Pinata');
  const pinResponse = await pinata.pinFileToIPFS(dataToPin, {
    pinataMetadata: {
      name: "myFirstPinnedData",
      keyvalues: {
        myKey: 'myValue',
      } as any,
    },
  })

  console.log(JSON.stringify({ pinResponse }, null, 2));
}

main().catch((err) => console.error(err));
実行コマンド
npx ts-node -r dotenv/config pin-file.ts
実行結果
{
  "pinResponse": {
    "IpfsHash": "QmeTv51GpP6cMF3KoAPDYTrEqB9NUvqHygafgM3bNM9Row",
    "PinSize": 21,
    "Timestamp": "2023-01-19T07:52:55.793Z"
  }
}

Pinata のダッシュボードを見ても追加されていることがわかる、反映するにはリロードが必要

Name 列をクリックするとピンしたファイルの内容を閲覧できる

URL は https://gateway.pinata.cloud/ipfs/QmeTv51GpP6cMF3KoAPDYTrEqB9NUvqHygafgM3bNM9Row

とりあえず今日はここまで、スムーズに進んで良かった

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

pinFromFS

  • ファイルのパスを指定してピンする
  • ファイルだけではなくてディレクトリもできるようだ
  • せっかくなのでディレクトリを試してみよう
準備コマンド
mkdir directory-to-pin
echo 'Hello, pinFromFS!' > directory-to-pin/message.txt
touch pin-directory.ts
pin-directory.ts
import PinataClient, { PinataMetadataFilter } from "@pinata/sdk";
import { join } from "path";
import { cwd } from "process";

async function main() {
  const pinata = new PinataClient({
    pinataJWTKey: process.env.PINATA_JWT!,
  });

  const directoryToPin = join(cwd(), "directory-to-pin");
  const pinResponse = await pinata.pinFromFS(directoryToPin, {
    pinataMetadata: {
      name: "myFirstPinnedDirectory",
      keyvalues: {
        customKey: "customValue",
      } as any,
    },
  });

  console.log(JSON.stringify({ pinResponse }, null, 2));
}

main().catch((err) => console.error(err));
実行コマンド
npx ts-node -r dotenv/config pin-directory.ts
実行結果
{
  "pinResponse": {
    "IpfsHash": "QmZn26VASeHsaE8KUxd1dZWp2QUF9t3oL5oii5AdWbzs8V",
    "PinSize": 83,
    "Timestamp": "2023-01-20T00:34:51.184Z"
  }
}

  • Pinata のダッシュボードからも確認できる
  • ピンされたディレクトリの URL は下記の通り

https://gateway.pinata.cloud/ipfs/QmZn26VASeHsaE8KUxd1dZWp2QUF9t3oL5oii5AdWbzs8V

  • アクセスするとディレクトリ一覧が表示される
  • ファイル名をクリックするとファイル内容が表示される

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

クライアントサイドだけで使えるのだろうか

今のところ Node.js のストリームやファイルシステムを前提としているので難しいかな?

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Pinata SDK は TypeScript 対応しているけど

  • 微妙に整合性が取れていなくてもやもやする
  • 例えば pinataMetadata には as any が必要
  const pinResponse = await pinata.pinFromFS(directoryToPin, {
    pinataMetadata: {
      name: "myFirstPinnedDirectory",
      keyvalues: {
        customKey: "customValue",
      } as any,
    },
  });
  • pinataMetadata の型は PinataMetadata で定義は下記の通り
  • これでは keyvalues にオブジェクトを指定するとエラーが出ても仕方がない
export interface PinataMetadata {
    [key: string]: string | number | null;
}
  • PinataMetadataFilter というのもあって似ているけど valueop が微妙に違う
export interface PinataMetadataFilter {
    name?: string | undefined;
    keyvalues: {
        [key: string]: {
            value: string | number | null;
            op: string;
        };
    };
}
  • もしかして自分の使い方が間違っている?
  • 正しいと確信できればプルリクエストを送りたい
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

pin-directory.ts を再度実行してみる

  • 同じ内容でピンしたらどうなるのか気になったので再度実行してみた
  • 結果としては問題なくピンできた、ハッシュも同じでダッシュボードにも変化がない様子
実行結果
{
  "pinResponse": {
    "IpfsHash": "QmZn26VASeHsaE8KUxd1dZWp2QUF9t3oL5oii5AdWbzs8V",
    "PinSize": 83,
    "Timestamp": "2023-01-20T00:34:51.184Z",
    "isDuplicate": true
  }
}

次は name を変更して試してみる

    pinataMetadata: {
      name: "myFirstPinnedData2",
      keyvalues: {
        myKey: 'myValue',
      } as any,
    },

先ほどは気がつかなかったが "isDuplicate": true が追加されている

実行結果
{
  "pinResponse": {
    "IpfsHash": "QmZn26VASeHsaE8KUxd1dZWp2QUF9t3oL5oii5AdWbzs8V",
    "PinSize": 83,
    "Timestamp": "2023-01-20T00:34:51.184Z",
    "isDuplicate": true
  }
}

ダッシュボード上のファイル名に変化はない、最初の名前が優先されるようだ

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

hashMetadata / メタデータ変更

準備コマンド
touch change-metadata.ts
change-metadata.ts
import "@pinata/sdk";
import PinataClient from "@pinata/sdk";

async function main() {
  const pinata = new PinataClient({
    pinataApiKey: process.env.PINATA_API_KEY!,
    pinataSecretApiKey: process.env.PINATA_API_SECRET!,
  });

  const ipfsPinHash = "QmeTv51GpP6cMF3KoAPDYTrEqB9NUvqHygafgM3bNM9Row";
  const changeMetadataResponse = await pinata.hashMetadata(ipfsPinHash, {
    name: "myFirstPinnedData2",
    keyvalues: {
      customKey: null,
      customKey2: "customValue2",
    } as any,
  });

  console.log(JSON.stringify({ changeMetadataResponse }, null, 2));
}

main().catch((err) => console.error(err));
実行コマンド
npx ts-node -r dotenv/config change-metadata.ts
実行結果
{
  "changeMetadataResponse": "OK"
}

Pinata のダッシュボードで確認すると名前やタグが更新されたことがわかる

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

pinByHash

準備コマンド
touch pin-by-hash.ts
pin-by-hash.ts
import PinataClient from "@pinata/sdk";

async function main() {
  const pinata = new PinataClient({
    pinataApiKey: process.env.PINATA_API_KEY!,
    pinataSecretApiKey: process.env.PINATA_API_SECRET!,
  });

  const ipfsHashToPin = 'QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq';
  const pinResponse = await pinata.pinByHash(ipfsHashToPin, {
    pinataMetadata: {
      name: "pinnedByHash",
    },
  })

  console.log(JSON.stringify({ pinResponse }, null, 2));
}

main().catch((err) => console.error(err));
実行コマンド
npx ts-node -r dotenv/config pin-by-hash.ts
実行結果
{
  "pinResponse": {
    "id": "84fe30c6-20e8-4cb8-8cef-e90a0d752ffe",
    "ipfsHash": "QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq",
    "status": "prechecking",
    "name": "pinnedByHash"
  }
}
  • id はピンジョブの ID、ジョブとは IPFS 上でピンするコンテンツを探すジョブのことだろう
  • ipfsHash はピンする ipfsHash、ちなみに今回は Bored Ape Yacht Club にしてみた
  • status はピンジョブのステータス、リクエストが成功していれば searching になるようだ
  • name はリクエスト時に指定した Pinata 上での名前(今回は pinnedByHash

Pinata のダッシュボード上にもちゃんと表示された

tokenId = 0 のファイルを開いてみる

image にアクセスしてみる

お猿の画像が表示された、ちなみに URL は下記の通り

https://gateway.pinata.cloud/ipfs/QmRRPWG96cmgTn2qSzjwr2qvfNEuhunv6FNeMFGa9bx6mQ

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

BAYC のメタデータを見てて気がついたけど

NFT の tokenURI とか image とかに ipfs: のアドレスを指定しても大丈夫なんだ

BAYC のように世界的に有名な NFT だからかも知れないけど

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

ピンとは何か?

Pinata 公式ドキュメントにしっかり書いてあった

https://docs.pinata.cloud/what-can-i-learn-here/what-is-pinata/what-does-pinning-mean

When you “pin” data on an IPFS node, you are telling that node that the data is important and it should be saved. A node is a program that connects you to IPFS and stores files.
Pinning prevents important data from being deleted from your node. You and only you can control and pin data on your node(s)—you can not force other nodes on the IPFS network to pin your content for you. So, to guarantee your content stays pinned, you have to run your own IPFS nodes.
Once your file is pinned to IPFS, you have full control to share, distribute, monetize and share your files however you’d like.

せっかくなので自力で翻訳してみよう

IPFS ノードにデータをピンする時「このデータは重要なので保管してください」とノードに伝えています。ノードとはあなたを IPFS やファイルに接続するプログラムのことです。
ピンすることでノードから重要なデータが削除されることを防止できます。自分が管理するノードにデータをピンすることができますが、自分以外が管理するノードに対してデータをピンすることはできません。ピンしたいコンテンツが保管され続けるためには自分の IPFS ノードを運用する必要があります。
IPFS にファイルがピンされたら好きなようにファイルを共有・配布・収益化することができます。

Pinata は自分の代わりに IPFS ノードを運用してくれるサービスのようだ

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

pinJobs

未完了のピンジョブを一覧するための API

準備コマンド
touch list-pin-jobs.ts
list-pin-jobs.ts
import PinataClient from "@pinata/sdk";

async function main() {
  const pinata = new PinataClient({
    pinataApiKey: process.env.PINATA_API_KEY!,
    pinataSecretApiKey: process.env.PINATA_API_SECRET!,
  });

  const pinJobs = await pinata.pinJobs();

  console.log(JSON.stringify({ pinJobs }, null, 2));
}

main().catch((err) => console.error(err));
実行コマンド
npx ts-node -r dotenv/config list-pin-jobs.ts
実行結果
{
  "pinJobs": {
    "count": 0,
    "rows": []
  }
}

未完了のピンジョブが 0 件なのであまり面白くない結果になってしまった

ピンジョブが 1 件以上ある場合は次のようなフィールドが表示されるようだ

  • id
  • ipfs_pin_hash
  • date_queued
  • name
  • status

フィールド名がキャメルケースだったりスネークケースだったりするのが気になる

マイクロサービスとかで別々のチームが担当しているのだろうか

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

pinJSONToIPFS を試そうとしたら

面白いのが出てきた

{
  reason: 'FORBIDDEN',
  details: 'Account blocked due to plan usage limit'
}

もしかして BAYC をピンしたから?

Billing ページを見ると Number of items pinned が 10007 になっている

というかページ最上部にメッセージが表示されていた、何かあるなーとは思っていたけど

せっかくなので unpin を試してみよう

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

ピンされたアイテム数は増えているのにピンされたアイテムの合計サイズが増えていないのはなぜなのか気になる

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

unpin

準備コマンド
touch unpin.ts
unpin.ts
import PinataClient from "@pinata/sdk";

async function main() {
  const pinata = new PinataClient({
    pinataApiKey: process.env.PINATA_API_KEY!,
    pinataSecretApiKey: process.env.PINATA_API_SECRET!,
  });

  const ipfsHashToUnpin = 'QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq';
  const unpinResponse = await pinata.unpin(ipfsHashToUnpin)

  console.log(JSON.stringify({ unpinResponse }, null, 2));
}

main().catch((err) => console.error(err));
実行コマンド
npx ts-node -r dotenv/config unpin.ts
実行結果
{
  "unpinResponse": "OK"
}

My Files からは消えたけど赤いメッセージが出続けている

リロードしたら消えた、Billing ページにも反映されている

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

pinJSONToIPFS

準備コマンド
touch pin-json.ts
pin-json.ts
import PinataClient from "@pinata/sdk";

async function main() {
  const pinata = new PinataClient({
    pinataApiKey: process.env.PINATA_API_KEY!,
    pinataSecretApiKey: process.env.PINATA_API_SECRET!,
  });

  const jsonToPin = {
    message: "Hello, pinJSONToIPFS!",
  };

  const pinResponse = await pinata.pinJSONToIPFS(jsonToPin, {
    pinataMetadata: {
      name: "myFirstPinnedJSON",
    },
  });

  console.log(JSON.stringify({ pinResponse }, null, 2));
}

main().catch((err) => console.error(err));
実行コマンド
npx ts-node -r dotenv/config pin-json.ts
実行結果
{
  "pinResponse": {
    "IpfsHash": "QmZEYZ9fDkJDqmEteeYRdcRGgUrHv2ytxpsp4QTPEcSuNF",
    "PinSize": 43,
    "Timestamp": "2023-01-23T01:43:25.281Z"
  }
}

My Files を見るとピンされていることがわかる

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

userPinPolicy

GitHub ページには userPinPolicy という API もあるようなことが書かれているが実際には無いようだ

今日はここまでにして残りの Data 系の下記の API についても引き続き試していきたい

  • pinList
  • getFilesByCount
  • userPinnedDataTotal
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

pinList

  • ピンされたファイルやディレクトリの一覧を表示する
  • イメージとしてはダッシュボードの My Files に近い
準備コマンド
touch list-pins.ts
list-pins.ts
import PinataClient from "@pinata/sdk";

async function main() {
  const pinata = new PinataClient({
    pinataApiKey: process.env.PINATA_API_KEY!,
    pinataSecretApiKey: process.env.PINATA_API_SECRET!,
  });

  const listResponse = await pinata.pinList({
    pageLimit: 1,
  });

  console.log(JSON.stringify({ listResponse }, null, 2));
}

main().catch((err) => console.error(err));
実行コマンド
npx ts-node -r dotenv/config list-pins.ts
実行結果
{
  "listResponse": {
    "rows": [
      {
        "id": "1534de97-6d08-4356-a0bd-43c419b12419",
        "ipfs_pin_hash": "QmZEYZ9fDkJDqmEteeYRdcRGgUrHv2ytxpsp4QTPEcSuNF",
        "size": 43,
        "user_id": "ca1b5bfe-5679-4190-95ee-37786427ddf8",
        "date_pinned": "2023-01-23T01:43:25.281Z",
        "date_unpinned": null,
        "metadata": {
          "name": "myFirstPinnedJSON",
          "keyvalues": null
        },
        "regions": [
          {
            "regionId": "FRA1",
            "currentReplicationCount": 1,
            "desiredReplicationCount": 1
          },
          {
            "regionId": "NYC1",
            "currentReplicationCount": 1,
            "desiredReplicationCount": 1
          }
        ]
      }
    ]
  }
}

この API を使えば自前の My Files ページを作れそうだ

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

getFilesByCount

  • 色々頑張ってみたけど期待した結果が得られない
  • My Files のファイルを1つ1つ走査できることを期待したのだが
準備コマンド
touch iterate-files.ts
iterate-files.ts
import PinataClient from "@pinata/sdk";

async function main() {
  const pinata = new PinataClient({
    pinataApiKey: process.env.PINATA_API_KEY!,
    pinataSecretApiKey: process.env.PINATA_API_SECRET!,
  });

  const getFilesIterator = pinata.getFilesByCount({
    status: "all",
  });

  for await (const getFileResponse of getFilesIterator) {
    console.log(JSON.stringify({ getResponse: getFileResponse }, null, 2));
  }
}

main().catch((err) => console.error(err));
実行コマンド
npx ts-node -r dotenv/config iterate-files.ts
実行結果
(何も表示されない)

ソースコードを見てもただの便利メソッドのようなので必要なら自分で作ろうと思う

https://github.com/PinataCloud/Pinata-SDK/blob/master/src/commands/data/getFilesByCount/getFilesByCount.ts

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

userPinnedDataTotal

  • ピンされたファイルの合計バイト数を取得するメソッド
  • 戻り値の型は number だが実際に取得してみるとオブジェクトだった
  • 合計バイト数に加えてピンされたファイル数とレプリケーションを含めた合計バイト数も同時に取得できる
準備コマンド
touch get-total-bytes.ts
get-total-bytes.ts
import PinataClient from "@pinata/sdk";

async function main() {
  const pinata = new PinataClient({
    pinataApiKey: process.env.PINATA_API_KEY!,
    pinataSecretApiKey: process.env.PINATA_API_SECRET!,
  });

  const totalBytes = await pinata.userPinnedDataTotal();

  console.log(JSON.stringify({ totalBytes }, null, 2));
}

main().catch((err) => console.error(err));
実行コマンド
npx ts-node -r dotenv/config get-total-bytes.ts
実行結果
{
  "totalBytes": {
    "pin_count": 8,
    "pin_size_total": 926752,
    "pin_size_with_replications_total": 926752
  }
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Submarining について

特定の条件を満たす人にだけコンテンツを提供する機能、例えば下記

  • ある NFT を持っている人
  • リツイートしてくれた人
  • ある地域に住んでいる人

特に NFT を所有している人にコンテンツを提供したいというニーズは多そう

ドキュメントは下記

https://docs.pinata.cloud/what-can-i-learn-here/submarining

https://www.submarine.me/

下記の記事も面白い

https://medium.com/pinata/how-to-manage-nft-visibility-18e9b7a76b8c

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

CLI もある

公式ドキュメントを眺めていたら CLI があることを知った

https://www.npmjs.com/package/pinata-upload-cli

例えばアップロードするだけなどプログラムを書くまでもないなら CLI で良さそう

$ npm install --save-dev pinata-upload-cli
$ npx pinata-cli --help
Usage: pinata-cli [options]

A command line tool to upload files and folders to Pinata

Options:
  -V, --version                     output the version number
  -a, --auth [jwt]                  API jwt from Pinata
  -as --authSubmarine [api key]     V2 API Key from Pinata
  -u, --upload [file or folder]     Source folder or file to upload to IPFS
  -s, --submarine [file or folder]  Source folder or file to submarine on
                                    Pinata
  -h, --help                        display help for command
$ source .env && npx pinata-cli --auth $PINATA_JWT
Authenticated
$ npx pinata-cli --upload pin-file.ts
{ percent: 0, transferred: 0, total: 802 }
{ percent: 0.1882793017456359, transferred: 151, total: 802 }
{ percent: 0.9276807980049875, transferred: 744, total: 802 }
{ percent: 1, transferred: 802, total: 802 }
Pinning, please wait...
{
  IpfsHash: 'QmbEn3gdkj8HRBiC7o8iZFYBPhrEaWS152LRrh3mRKg22n',
  PinSize: 604,
  Timestamp: '2023-01-24T01:04:42.165Z'
}

このスクラップは2023/01/24にクローズされました