LINEグループ名の取得方法

LINEグループ名の取得方法

投稿日: 2024年10月03日

学習振り返り
要約
  • 個人開発中のアプリでLINEグループ名取得機能を実装するため、DB設計からAPI連携までの過程を紹介。
  • LINE NotifyのAPIを使用してアクセストークンからグループ名を取得し、バックエンドでデータを保存する仕組みを構築。
  • 今後も外部連携やデータ表示機能の改良を進める予定で、詳細な実装情報の提供を検討中。

ここでご紹介するLINE Notifyは2025年3月を以てサービスが終了します。代替サービスとしてLINE MessagingAPIが提供されていますので、そちらでの実装をご検討ください。

はじめに

個人開発第二弾で実装中のアプリで、グループラインに通知する機能を付けたくて、頑張っています。

APIトークンがあれば通知は飛ばせるので絶対必要というわけではないものの、LINEグループ名でどのトークルームなのかわかるのであった方がよさそうだなと思い、やってみたので記録しておきます!!

アプリの詳細は開発しようと思った背景も含めて書きたいのですがまたの機会にします!!

私が実装するまでに検討したことを順に書いていきたいと思います。

手順1:アプリのDB設計考える

デザイン弱弱、figma弱弱人間なので、いきなりDB設計考え始めました。

こちらの方がとっつきやすく。頼むので真似しないでください。(じゃあ書くな)

一旦ER図載せときます。

roomのテーブルにあるgroupNameが今回取得したい値になります。

手順2:取得する方法調査

端折りますが、LINE通知機能などはすでに何度も実装してきたのでLINE Notifyを使うという事前情報は持っている状態から始まります。

上記のサイトからグループ名が得られそうなエンドポイントを探しました。

見つかりました。

連携状態を確認するAPIです。この API を使うと、使用したアクセストークンが有効かどうかを確認することができます。取得できる場合には関連付けられたユーザ・またはグループ名を取得することができます。

グループ名を取得したいのでこれで良さそうです。

レスポンスにtargetというプロパティがあるようですのでこれをとってきたらいいと理解しました。

手順3:実装する

はやくこの段階にきたいのでデザインとかも一応作りましたがかなり雑です。

全然この通りになってないですし。

最初にサインアップ、サインインはザっと作って、groupNameを取得する機能に取り掛かりました。

どこで取得するか

画面で入力させたい情報はAPIトークンのみです。

adminユーザーしか登録させないので、セッション情報からユーザー情報は得られますし、グループ名はトークンから取得するので。

roomテーブルにPOSTするエンドポイントでAPIトークンをリクエストボディに渡してLINEのAPI叩いてグループ名取得、そのままroomのデータを作る形にすることにしました。

没案

最初、APIトークンのInputタグからフォーカスが外れたタイミング(onBlur)でグループ名取得して画面に表示させようかなと思ってやっていました。

しかし「cors」エラーが返ってきて叶いませんでした。

フロントから外部のAPIを叩くとブラウザのデフォルト機能で別サービスのAPIははじくようになっているようです。

通常、APIトークンを使って外部API叩く場合は、フロントからだとトークンが見えるからバックエンドで行うようにすると思うのですが、今回はそのAPIトークンを画面に入力させているからフロントで直接叩こうとしました。

出来ませんでした。

完成したコード

バックエンド

import { type NextRequest } from "next/server";
import { buildPrisma } from "@/app/_utils/prisma";
import { supabase } from "@/app/_utils/supabase";
import { PostRequest } from "@/app/_types/admin/room/PostRequest";
import { getGroupName } from "./_utils/getGroupName";
export const POST = async (req: NextRequest) => {
  const prisma = await buildPrisma();
  const token = req.headers.get("Authorization") ?? "";
  const { data, error } = await supabase.auth.getUser(token);

  if (error) return Response.json({ status: 401, message: "Unauthorized" });
  const body: PostRequest = await req.json();
  const { lineToken } = body;
  try {
    const { groupName } = await getGroupName(lineToken);
    const adminUserData = await prisma.adminUser.findUnique({
      where: {
        supabaseUserId: data.user.id,
      },
    });
    if (!adminUserData) throw new Error("ユーザー情報なし");
    const roomData = await prisma.room.create({
      data: {
        adminUserId: adminUserData.id,
        apiToken: lineToken,
        groupName,
      },
    });

    return Response.json({ status: "OK", id: roomData.id }, { status: 200 });
  } catch (e) {
    if (e instanceof Error) {
      return Response.json({ message: e.message }, { status: 400 });
    }
  }
};
interface LineStatus {
  status: number;
  message: string;
  targetType: string;
  target: string;
}

export const getGroupName = async (token: string) => {
  const headers = {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${token}`,
    },
  };
  const resp = await fetch("https://notify-api.line.me/api/status", headers);
  const data: LineStatus = await resp.json();
  return { groupName: data.target };
};

フロント

"use client";
import { useAdminUser } from "../_hooks/useAdminUser";
import { RoomIndex } from "./_components/RoomIndex";
import { useNewRoom } from "./_hooks/useNewRoom";
import { Button } from "@/app/_components/Button";
export default function Page() {
  const { isLoading, email } = useAdminUser();
  const { register, errors, handleSubmit, isSubmitting } = useNewRoom();
  return (
    <div className="h-screen relative py-5 px-3">
      <h1 className="text-center text-4xl">ルーム一覧</h1>
      <div className="flex justify-end pr-5 py-2">
        <form onSubmit={handleSubmit} className="flex gap-2">
          <input
            disabled={isSubmitting}
            id="token"
            inputMode="text"
            placeholder="APIトークン"
            type="text"
            className="border-[1px] px-2 rounded-lg"
            {...register("token")}
          />
          {errors.token && (
            <span className="text-red-500">{errors.token.message}</span>
          )}
          <div className="w-24">
            <Button>登録する</Button>
          </div>
        </form>
      </div>
      <RoomIndex></RoomIndex>
      <div className="absolute bottom-0">
        ログイン名 : {isLoading ? "" : email}
      </div>
    </div>
  );
}

import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { useApi } from "@/app/_hooks/useApi";
import { PostRequest } from "@/app/_types/admin/room/PostRequest";
import { PostResponse } from "@/app/_types/admin/room/PostResponse";
// import toast from "react-hot-toast"; 追加予定

interface NewRoom {
  token: string;
}

export const useNewRoom = () => {
  const { post } = useApi();
  const schema = z.object({
    token: z.string().min(1, { message: "APIトークンは必須です" }),
  });
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<NewRoom>({
    resolver: zodResolver(schema),
    mode: "onSubmit",
  });

  const onSubmit = async (formdata: NewRoom) => {
    const resp = await post<PostRequest, PostResponse>("/api/admin/room", {
      lineToken: formdata.token,
    });
    alert(resp);
  };

  return {
    register,
    handleSubmit: handleSubmit(onSubmit),
    errors,
    isSubmitting,
  };
};

こちらでグループ名取得してroomのデータを登録までは動きます。

寝る前までやって出来たから寝ようの状態から何もリファクタ等していないので、とりあえず動くという目線で見ていただければ幸いです・・

バリデーションとかちょっと違うなと思っているのでフロント側はこれから修正たくさん加えて行こうと思います。

次はreact-table縛りでデータ表示をするRoomIndexコンポーネント作っていきます・・!

おわりに

外部連携楽しいと感じるタイプです。

見る側にあまり配慮していない書きっぱなしのコードをペタペタ貼ったので、ツッコミどころ沢山あると思いますが温かい目でみていただければ幸いです。

LINE連携は需要ありそうだと偏見を持っていることと、息子が起きる前に書きあげたいということと、ブログ1つとりあえず書いてみたいという思いで、相当端折ったのでもう少しここ詳しく!等ありましたらご連絡いただければ幸いです。

また別記事か編集するかで端折ったところ詳しく書いていければと思っております!

以上です!

シェア!

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