PokéAPIで取得したポケモン名(合言葉)を取得してDB保存するまで

PokéAPIで取得したポケモン名(合言葉)を取得してDB保存するまで

投稿日: 2024年10月28日

Tips
学習振り返り
要約
  • LINEのWebhookを使って参加イベントを受け取り、部屋情報をデータベースに保存する実装を行った。
  • 合言葉はPokeAPIからランダムにポケモン名を取得し、セキュアなURL識別子も生成している。
  • 次回はLINE通知機能を実装予定で、音楽会のため一旦作業を中断する。

はじめに

ダッシュでPokeAPI叩いて合言葉生成、room登録するまでやりました。

環境変数の設定漏れ、いつの間にかリレーション設定しているデータが消えていた・・(これはホントなぜ案件)で時間めちゃくちゃ溶かしました。

私の睡眠時間返せと思いながら、実装出来たところまで共有していきます。

ぐちゃぐちゃだったらすみません。正直頭の中「眠い」しかないけど今やらないともうやらない気がしてw

ではいきます・・・!

コード

webhookでPOSTリクエスト受けてからroomテーブル保存するまでです。

早速!!

import { NextRequest, NextResponse } from "next/server";
import { WebhookRequest } from "./_types/WebhookRequest";
import { buildPrisma } from "@/app/_utils/prisma";

import { pokeApi } from "./_utils/pokeApi";
import { randomBytes } from "crypto";
export const POST = async (req: NextRequest) => {
  const prisma = await buildPrisma();

  try {
    const body: WebhookRequest = await req.json();
    const events = body.events;

    const joinEvent = events.find(event => event.type === "join");

    if (!joinEvent)
      return NextResponse.json(
        {
          message: "参加イベントじゃない",
        },
        { status: 204 }
      );

    const { groupId, roomId, userId } = joinEvent.source;
    const lineId = groupId || roomId || userId;
    if (!lineId) throw new Error("IDの取得が出来ませんでした");
    //合言葉の生成をする
    const pokeName = await pokeApi();

    //URLの生成
    const buffer = randomBytes(16);
    const roomUrlId = buffer.toString("hex");

    const roomData = await prisma.room.create({
      data: {
        adminUserId: process.env.ADMIN_USER as string,
        lineId,
        roomUrlId,
        password: pokeName,
      },
    });

    return NextResponse.json(
      {
        message: "success",
      },
      { status: 200 }
    );
  } catch (e) {
    if (e instanceof Error) {
      console.log(e.message);
      return NextResponse.json({ error: e.message }, { status: 400 });
    }
  }
};

簡単に解説

最初に、処理対象のイベントかどうか判断します。

const body: WebhookRequest = await req.json();
    const events = body.events;

    const joinEvent = events.find(event => event.type === "join");

    if (!joinEvent)
      return NextResponse.json(
        {
          message: "参加イベントじゃない",
        },
        { status: 204 }
      );

ここです。

const joinEvent = events.find(event => event.type === "join");

そして参加イベントじゃない場合は何もしないということでリターンする処理を入れます。

const { groupId, roomId, userId } = joinEvent.source;
    const lineId = groupId || roomId || userId;
    if (!lineId) throw new Error("IDの取得が出来ませんでした");

ここで、参加したルームに応じでいずれかのIDが入ります。複数はあり得ないので順番はどうでもいいです。

どれも入っていないということはあり得ないですが念のため、そういったことがおきた時に備えてエラーをスローする処理入れました。

絶対ないから要らんかも。

//合言葉の生成をする
const pokeName = await pokeApi();

出てきました。ここでポケモン名を取得します。

呼び出している関数の詳細こちらです。

utils/pokeApi.ts

import { fetcher } from "../../_utils/fetcher";
import { PokemonResponse } from "../_types/PokemonResponse";
export const pokeApi = async () => {
  //10回以内に名前取得に成功しないとエラーをスローする
  let count = 0;
  const MAX_TRY = 10;

  while (count < MAX_TRY) {
    //1-1025の数字生成する
    const id = Math.floor(Math.random() * 1025) + 1;
    const endpoint = `https://pokeapi.co/api/v2/pokemon-species/${id}`;

    try {
      const resp = await fetcher<PokemonResponse>(endpoint);
      const name = resp.names.find(item => item.language.name === "ja-Hrkt");

      if (name) {
        return name.name;
      }
    } catch (error) {
      console.error(`Error fetching data for ID ${id}:`, error);
    } finally {
      count++;
    }
  }
  throw new Error("合言葉の生成に失敗しました");
};

1025はマジックナンバーにした方がよかったかも。(記事公開前,最後に見返す段階で気づくw)

PokemonResponseの方はこちら

export interface PokemonResponse {
  base_happiness: number;
  capture_rate: number;
  color: Color;
  egg_groups: EggGroup[];
  evolution_chain: EvolutionChain;
  evolves_from_species: null | Species;
  flavor_text_entries: FlavorTextEntry[];
  forms_switchable: boolean;
  gender_rate: number;
  genera: Genus[];
  generation: Generation;
  growth_rate: GrowthRate;
  habitat: Habitat;
  has_gender_differences: boolean;
  hatch_counter: number;
  id: number;
  is_baby: boolean;
  is_legendary: boolean;
  is_mythical: boolean;
  name: string;
  names: Name[];
}

interface Color {
  name: string;
  url: string;
}

interface EggGroup {
  name: string;
  url: string;
}

interface EvolutionChain {
  url: string;
}

interface Species {
  name: string;
  url: string;
}

interface FlavorTextEntry {
  flavor_text: string;
  language: Language;
  version: Version;
}

interface Language {
  name: string;
  url: string;
}

interface Version {
  name: string;
  url: string;
}

interface Genus {
  genus: string;
  language: Language;
}

interface Generation {
  name: string;
  url: string;
}

interface GrowthRate {
  name: string;
  url: string;
}

interface Habitat {
  name: string;
  url: string;
}

interface Name {
  language: Language;
  name: string;
}

AIにお願いしました。

utils/fetcher.ts

type FetcherOptions<RequestType> = {
  method?: string;
  headers?: Record<string, string>;
  body?: RequestType;
};

export const fetcher = async <ResponseType, RequestType = undefined>(
  url: string,
  options: FetcherOptions<RequestType> = {}
): Promise<ResponseType> => {
  const { method = "GET", headers = {}, body } = options;

  try {
    const response = await fetch(url, {
      method,
      headers: {
        "Content-Type": "application/json",
        ...headers,
      },
      body: body ? JSON.stringify(body) : undefined,
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const data: ResponseType = await response.json();
    return data;
  } catch (error) {
    console.error(error);
    throw error;
  }
};

多分LINEに通知するときにも使うと思ったので、汎用性はあるようにと思って作りました。

pokeApiに戻りますが、悩みつつ、10回試して上手く名前取得出来ないならエラーをスローすることにしました。

絶対できるとは思うのですが念のため・・

    const id = Math.floor(Math.random() * 1025) + 1;

ここで1-1025までの乱数を生成します。

Math.random()で0以上1未満のランダムな小数を生成し整数にしたいので、1025倍してMath.floor()で小数点以下を切り捨てています。

このままだと0-1024までになるので最後に+1します。

そこで得られた値をIDとしてPokeAPIにGETリクエストします。

const endpoint = `https://pokeapi.co/api/v2/pokemon-species/${id}`;

    try {
      const resp = await fetcher<PokemonResponse>(endpoint);
      const name = resp.names.find(item => item.language.name === "ja-Hrkt");

      if (name) {
        return name.name;
      }
    } catch (error) {
      console.error(`Error fetching data for ID ${id}:`, error);
    } finally {
      count++;
    }

言語が日本語の名前に絞り込んで返しますが、もし値が存在しなければ再度乱数の生成をしてリクエストして・・と繰り返します。

次にURLの生成をします。

//URLの生成
    const buffer = randomBytes(16);
    const roomUrlId = buffer.toString("hex");

ここはセキュアなURLを生成するための処理です。

const buffer = randomBytes(16);

これで16バイトのランダムなデータを生成します。

一意の識別子などに使用されるらしいです。

const roomUrlId = buffer.toString("hex");

バッファ内のバイナリデータを16進数(hex)形式の文字列に変換します。

この処理によって32文字のランダムな16進数文字列を生成できます。

これをURLに含めて、URLを知っている人だけがアクセスできるページを作ろうという意図です。

今回は更にセキュリティレベルを上げるために合言葉を生成したというわけです。

最後、

const roomData = await prisma.room.create({
      data: {
        adminUserId: process.env.ADMIN_USER as string,
        lineId,
        roomUrlId,
        password: pokeName,
      },
    });

roomテーブルに各データを登録します。

LINE Notifyを用いてトークンで紐づける方法が使えなくなり、LINE Messaging APIを使うことになったことで管理者の扱いに悩んでおり、supabaseのユーザー登録した人が管理者としてroomの管理をするイメージをして作っていたのですが、公式アカウントに対して紐づけになったので一旦adminUserは私のアカウントに限定しようと思い、環境変数にsupabaseIdを設定しました。

また改めてどうするか考えますw

ここまでできて一旦200返して終わってますが、レスポンス返す前にLINEでURLと合言葉を返信しないといけないので明日また続きをやります!!

おわりに

うーん、順調とは言えないペース。本当はLINE通知するところまでやりたかったですが

明日は娘の音楽会なので朝から学校行かなきゃで無理せずこのへんで・・おやすみなさーい!!

シェア!

Threads
user
吉本茜
山口在住/二児の母
Loading...
記事一覧に戻る
Threads
0