1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ウイコレ身内リーグをLINE Bot + LIFF + Firebaseで管理できるようにした話

1
Posted at

IMG_3781.jpg
IMG_3778.jpg
IMG_3779.jpg
IMG_3775.jpg
IMG_3776.jpg
IMG_3777.jpg

はじめに

身内でウイコレの月例リーグをやっていると、意外と管理するものが多い。

  • 今月の縛りルール
  • 誰が誰と何試合したか
  • 試合結果のスクリーンショット
  • 月間順位
  • 年間ポイント
  • 未対戦ペア
  • 次に誰が試合すべきか

最初はLINEグループの会話でなんとなく回していたが、試合結果やルールが流れてしまうと後から探すのが大変だった。

そこで、LINEグループに常駐するBotとLIFFアプリを作り、ウイコレの身内リーグ運営を半自動化した。

この記事では、その構成と作った手順、ハマったところをまとめる。

作ったもの

作ったシステムは大きく3つある。

  1. LINE LIFFアプリ
  2. LINE Messaging API Bot
  3. GitHub Actionsで動く日次日記生成

LIFFアプリでは、月の縛りルールをルーレットで決めたり、試合結果を手動/OCRで登録したり、順位表を確認できる。

LINE Botでは、グループに投稿されたウイコレの試合結果スクリーンショットを読み取り、確認メッセージを出し、OKが押されたらFirebaseに登録する。

さらに、メンションすると順位、未対戦、縛りルール、システム状況なども返すようにした。

最近は次の機能も足した。

  • 自動OCRのON/OFF
  • OFF中に上がったスクショを「集計して」であとからまとめてOCR
  • メンバー別の口癖・成績・因縁の分析
  • 日記と連動した月間名場面まとめ
  • 都内写真で遊ぶジオゲーム
  • チンチロ・大小のミニゲーム
  • Gemini/OpenAIを使える任意AI会話
  • AI会話の無料枠ガードと自動復帰
  • Render/Firebase/GitHubの状態確認
  • はてなブログへの日次日記投稿

全体構成

構成は次のようにした。

使っている主な技術は以下。

用途 技術
フロント HTML / CSS / JavaScript
LINEアプリ LIFF
Bot Node.js / Express
LINE連携 LINE Messaging API
DB Firebase Realtime Database
認証 Firebase Anonymous Auth / Admin SDK
OCR Tesseract.js
Botホスティング Render
静的ホスティング GitHub Pages
日次処理 GitHub Actions
ブログ投稿 はてなブログ AtomPub API
ジオゲーム Wikimedia Commons / Nominatim / ローカル地点辞書
任意AI会話 Gemini API / OpenAI API

構築手順

実際には次の順番で作った。

1. Firebase Realtime Databaseを用意
2. LIFFアプリを作る
3. GitHub PagesでLIFFアプリを公開
4. LINE Messaging API Botを作る
5. RenderにBotをデプロイ
6. LINE WebhookをRenderに向ける
7. OCRでスクショ登録を作る
8. Botのコマンド返信を増やす
9. 無料枠・課金ガードを入れる
10. Firebaseルールを閉じて、LIFFだけ匿名Authで必要範囲を読めるようにする
11. 自動OCR ON/OFFと手動集計を入れる
12. ジオゲームやミニゲームを足す
13. GitHub Actionsで日次日記を作る

1. Firebase Realtime Databaseを用意する

まず、状態共有用にFirebase Realtime Databaseを用意した。

LIFFアプリとBotの両方から同じデータを見るため、DBは1つにまとめた。

フロント側はFirebase Web SDK、Bot側はFirebase Admin SDKを使う。

フロント側では以下を読み書きする。

config
monthlyRules
matchResults
spinHistory
playerAvatars

Bot側では以下を読み書きする。

config
monthlyRules
matchResults
pendingOcr
conversations
ocrAutomation
screenshotCandidates
geoGames
geoGameUsage
geoGameCache
aiChatUsage
diary

最初は開発を優先してルールが緩かったが、Firebaseから「安全でないルールがあります」と警告が来た。

そこで、Realtime Database Rulesは次の方針にした。

  • ルートの .read / .writefalse
  • LIFFアプリが必要な共有データだけ auth != null で許可
  • ブラウザ側はFirebase Anonymous Authでサインイン
  • Bot専用データはAdmin SDKだけが触る
  • pendingOcrconversationsdiarygeoGamesscreenshotCandidates などはクライアントから読めない

これで、アプリの挙動は変えずに「誰でもDBを直接読み書きできる」状態を避けられる。

2. LIFFアプリを作る

LIFFアプリは素のHTML/CSS/JavaScriptで作った。

Reactなどを使わなかったのは、スマホのLINE内で軽く動けばよく、画面数もそこまで多くなかったから。

LIFFではLINEプロフィールを取得し、表示名やアイコンを履歴・順位表に使っている。

主な実装ファイルは以下。

index.html
css/style.css
js/app.js
js/sync.js
js/ocr.js
js/roulette.js
js/liff.js

3. GitHub Pagesで公開する

LIFFのエンドポイントURLとして使うため、静的ファイルはGitHub Pagesで公開した。

main ブランチをGitHub Pagesの公開対象にする。

git push origin main

キャッシュで古いJS/CSSが残ることがあるので、index.html の読み込みURLには ?v=51 のようなバージョンを付けた。

<script src="js/app.js?v=51"></script>

4. LINE Messaging API Botを作る

BotはExpressでWebhookを受けるだけのシンプルな構成にした。

linebot/
  server.js
  src/webhook.js
  src/firebase-admin.js
  src/ocr-node.js
  src/image-guard.js
  src/ocr-control.js
  src/geo-game.js
  src/dice-games.js
  src/ai-chat.js

server.js では /webhook/health を用意する。

/health はRenderやUptimeRobotからの死活監視用。

GET /health
POST /webhook

Webhookでは、イベント種別ごとに処理を分ける。

message:image
  → 自動OCR ONならOCR、OFFなら当日候補として保存

message:text
  → メンション付きコマンド

postback
  → OCR登録のOK/キャンセル

5. RenderにBotをデプロイする

BotはRenderのWeb Serviceにデプロイした。

設定は以下のようなイメージ。

Root Directory: linebot
Build Command: npm install
Start Command: node server.js
Plan: Free

環境変数には、LINEとFirebaseの認証情報を入れる。

LINE_CHANNEL_SECRET
LINE_CHANNEL_ACCESS_TOKEN
FIREBASE_SERVICE_ACCOUNT
FIREBASE_DATABASE_URL

ここに実際の値は絶対に記事へ載せない。

任意機能を使う場合は以下も入れる。

GEMINI_API_KEY
OPENAI_API_KEY
AI_CHAT_ENABLED
AI_PROVIDER
AI_COST_GUARD_ENABLED
GITHUB_TOKEN
BATCH_OCR_MAX_IMAGES
GEOGAME_DAILY_LIMIT

無料枠で運用するなら、AI系は最初からONにしない。

使う場合も日次・月次上限を先に決めてからONにした。

6. LINE WebhookをRenderに向ける

LINE Developers ConsoleでWebhook URLを設定する。

https://your-render-service.onrender.com/webhook

LINE Official Account Manager側では、自動応答がWebhookの邪魔をしないように設定を確認した。

Webhook: ON
応答メッセージ: OFF
あいさつメッセージ: 必要に応じてOFF

7. OCRでスクショ登録を作る

最初にブラウザ版でOCRを作り、その後Bot用にNode.jsへ移植した。

ブラウザ版:

js/ocr.js

Bot版:

linebot/src/ocr-node.js

どちらも基本ロジックは同じ。

  • スコア領域を切り出す
  • チーム名領域を切り出す
  • Tesseract.jsで読む
  • Firebaseの config/players と照合する

8. Botのコマンド返信を増やす

最初はOCR登録だけだったが、使っているうちに「LINEで聞けた方が早い」ものが増えた。

そこで、以下をメンション付きで聞けるようにした。

順位
年間順位
未対戦
進捗
来月の縛り
今月の縛り
自動OCR OFF
自動OCR ON
集計して
ジオゲーム
チンチロ
大小 大
メンバー分析
今月の名場面
システム
課金
ヘルプ

基本は @秘書 のようにメンションされた時だけ反応する。

9. 無料枠・課金ガードを入れる

最後に、無料枠を守るためのガードを入れた。

特に注意したのは以下。

  • LINEのpushMessageを使いすぎない
  • OCRキューを詰まらせない
  • AI会話はデフォルトOFF
  • AI会話の回数・トークン上限をFirebaseで管理
  • 課金リスクを検知したらAI会話を自動停止
  • 日次/月次上限で止まった場合だけ自動復帰

便利にしすぎて課金事故になるのは避けたかったので、ここはかなり安全寄りにした。

データ設計

FirebaseはRealtime Databaseを使った。

主なパスはこんな感じ。

config/
  players
  restrictMonths
  matchSchedule
  uicolleNews

monthlyRules/{year}/{month}
matchResults/{year}/{month}/{resultId}
pendingOcr/{messageId}
ocrAutomation/{sourceId}
screenshotCandidates/{sourceId}/{date}/{messageId}
conversations/{groupId}/messages
diary/{date}
geoGames/{sourceId}
geoGameUsage/{date}
geoGameCache/geocodes/{key}
aiChatUsage/{year}/{month}

players

プレイヤー名とゲーム内のチーム名を紐づける。

[
  {
    "name": "Aさん",
    "lineId": "LINE表示名",
    "charName": "ゲーム内チーム名"
  }
]

OCRではゲーム内チーム名を読み取り、プレイヤー名へ変換する。

matchResults

試合結果は月ごとに保存する。

{
  "date": "2026-04-21",
  "away": "Aさん",
  "home": "Bさん",
  "awayScore": 2,
  "homeScore": 1,
  "awayPK": null,
  "homePK": null,
  "addedBy": "登録者名",
  "addedAt": 1710000000000
}

これをもとに、月間順位と年間順位ポイントを集計する。

ocrAutomation / screenshotCandidates

画像OCRは便利だが、関係ないスクリーンショットに反応してほしくない場面もある。

そこでグループ単位で自動OCRのON/OFFを持たせた。

{
  "autoEnabled": false,
  "updatedBy": "Aさん",
  "updatedAt": 1710000000000
}

自動OCRがOFFのときに上がった端末スクリーンショットは、screenshotCandidates に控える。

{
  "messageId": "LINE_MESSAGE_ID",
  "senderName": "Aさん",
  "createdAt": 1710000000000,
  "width": 1170,
  "height": 2532,
  "status": "queued"
}

あとから @秘書 集計して と呼ぶと、その日の候補を順番にOCRする。

読めたものだけ確認Flex Messageを出し、違う画像や読み切れなかった画像は最後に件数でまとめる。

matchSchedule

最近追加したのが対戦スケジュール設定。

{
  "matchesPerPair": 2,
  "weeks": [2, 4]
}

初期値は「各ペア月2回、第2週・第4週」。

この設定を使って、

  • カレンダー上部の表示
  • 未対戦ペア
  • 進捗
  • Botの「あと誰と誰?」

を連動させている。

geoGames

ジオゲームは、LINEグループごとの進行状態を geoGames/{sourceId}/current に保存する。

{
  "status": "active",
  "startedBy": "Aさん",
  "expiresAt": 1710000000000,
  "photo": {
    "title": "Tokyo Station",
    "imageUrl": "https://..."
  },
  "answerLat": 35.681236,
  "answerLng": 139.767125,
  "answers": {}
}

写真は無料データの範囲でWikimedia Commonsから取得し、回答の地名はNominatimとローカル地点辞書で座標化する。

無料サービスに負荷をかけないよう、1日の出題回数やジオコーディング間隔に上限を入れている。

LIFFアプリ側

LIFFアプリはスマホで使う前提にした。

主な画面は以下。

  • ゲーム
  • カレンダー
  • 集計
  • 履歴
  • 設定

ルーレット

月の縛りルールを決めるために、12択を2回、6択を1回まわす。

結果はFirebaseに先に保存し、その後にアニメーションする。

これにより、スピン後に画面を戻したり再読み込みしたりしても、結果を引き直せないようにした。

カレンダー

カレンダーには、各月が「縛り」か「フリー」かを表示する。

上部には現在の設定を出す。

縛り月: 5・6・8・9・11月 / 対戦: 各ペア月2回(第2・第4週)

ここはFirebaseの config/restrictMonthsconfig/matchSchedule に連動させた。

集計

試合結果を登録すると、月次順位表と年間順位表を自動計算する。

月次順位は試合ポイントで並べる。

勝ち: 3pt
PK勝ち: 1pt
引き分け: 0pt
負け: 0pt

年間順位は、月間順位ごとに順位ポイントを加算する。

1位: 5pt
2位: 3pt
3位: 2pt
4位: 1pt
5位: 0pt

LINE Bot側

LINE BotはNode.js + Expressで作った。

Webhookを受け取り、イベント種別ごとに処理する。

画像メッセージ
  → OCR処理

テキストメッセージ
  → メンション付きコマンド判定

Postback
  → OCR結果のOK/キャンセル処理

画像OCRの流れ

画像処理は次の流れ。

普通の写真や関係ない画像は、グループの邪魔にならないように基本的には無視する。

ただしウイコレっぽいのに読み取りが不完全な場合は、再送を促すようにした。

ここで大事だったのは、画像サイズ判定だけでは限界があること。

スマホのスクリーンショットならウイコレ以外でも条件に入ってしまう。

そこで、グループ単位で自動OCRをOFFにできるようにした。

OFFにすると、Botはその場では反応せず、当日のスクリーンショット候補として控える。

必要なタイミングで、

@秘書 集計して

と呼ぶと、その日の候補だけをまとめてOCRする。

これで、雑談中に関係ない画像へBotが出てきてしまう問題をかなり抑えられる。

OCRでハマったところ

一番ハマったのはスコア読み取り。

単純にTesseract.jsに画像を渡すだけだと、背景やエフェクトの影響で数字が安定しない。

そこで、複数の前処理を試した。

  • 背景差分
  • 白黒反転
  • 数字専用の切り出し
  • 左右それぞれを複数領域で読み、投票で決定

チーム名は完全一致ではなく、ファジーマッチにした。

理由は、OCRでは濁点や長音、カタカナが崩れることがあるから。

そこで、

  • 正規化
  • Levenshtein距離
  • 部分一致
  • bigram Jaccard

を組み合わせて、登録済みのチーム名に寄せるようにした。

LINEの返信期限問題

OCRは重い。

特にRender無料枠の寝起きやTesseract.jsの初回起動では、LINEのreply tokenの有効時間に間に合わないことがあった。

そこで画像OCRの結果返信は、必要に応じて pushMessage を使うようにした。

また、関係ない画像が複数投稿されたときにOCR処理が詰まらないよう、画像OCRはキューで1件ずつ処理し、待ちが多すぎる場合はスキップするようにした。

Botのテキストコマンド

Botは基本的にメンションされたときだけ反応する。

例:

@秘書 ヘルプ
@秘書 順位
@秘書 未対戦
@秘書 来月の縛り
@秘書 自動OCR OFF
@秘書 集計して
@秘書 ジオゲーム
@秘書 回答 吉祥寺駅
@秘書 チンチロ
@秘書 大小 大
@秘書 メンバー分析
@秘書 今月の名場面
@秘書 システム
@秘書 課金

グループの通常会話を邪魔しないため、メンションなしの雑談には反応しない設計にした。

「ヘルプ」と言うと、反応できる言葉をBot自身が説明する。

人間が覚えるコマンド表を別途メンテするより、Botに聞けば分かる方がグループ運用では楽だった。

未対戦ペアの計算

プレイヤー数をもとに全ペアを作り、試合結果を集計する。

各ペアの予定試合数は config/matchSchedule.matchesPerPair から読む。

たとえば5人で各ペア月2回なら、

10ペア × 2試合 = 20試合

Botに「未対戦」と聞くと、

2026年4月、各ペア月2回(第2・第4週)の予定で、あと残ってる対戦は8試合分だよ。
・Aさん vs Bさん: あと1試合
・Cさん vs Dさん: あと2試合

のように返す。

ジオゲームとミニゲーム

せっかくLINEグループにBotがいるので、試合管理以外の遊びも少し足した。

ジオゲーム

@秘書 ジオゲーム と言うと、都内の位置情報付き写真を無料データから出題する。

回答は次のように送る。

@秘書 回答 吉祥寺駅
@秘書 回答 35.70272,139.57962

地名回答はNominatimで座標化する。

ただし外部ジオコーディングは不安定なことがあるので、東京の主要駅・ランドマークはローカル辞書も持たせた。

これにより、「吉祥寺駅」「吉祥寺」「高円寺」「新宿駅」のような回答は、外部APIが失敗しても最低限判定できる。

チンチロ・大小

軽い雑談用に、サイコロゲームも追加した。

@秘書 チンチロ
@秘書 大小 大
@秘書 大小 小

これは外部APIもDBも使わない。

完全にBot内でランダムに出目を作り、LINEに返すだけなので無料枠への影響がほぼない。

秘書キャラ化

ただの管理Botだと味気ないので、秘書キャラとして返答するようにした。

実装としては2段階にしている。

  1. 無料のテンプレ返答
  2. 任意の外部AI自然会話

普段はテンプレでも返せるようにしておき、APIキーがあるときだけGeminiやOpenAIに渡す。

キャラクター設定は character-memory.jsai-chat.js に寄せた。

年齢や来歴、秘書として雇われた経緯、話し方のトーンを持たせ、AI会話がOFFでも定型文だけになりすぎないように、直近会話の流れを少し見て返すようにしている。

また、課金事故を避けるために以下のガードを入れた。

  • 日次リクエスト上限
  • 月次リクエスト上限
  • 日次トークン上限
  • 月次トークン上限
  • quota / billing 系エラー検知時の自動停止
  • 日次/月次枠が戻ったときの自動復帰

無料運用したい場合、ここはかなり大事。

日次日記生成

Botとは別に、GitHub Actionsで毎朝7時JSTに日記を生成している。

日記では、過去日記と同じ話題をなるべく避けるようにしている。

たとえばウイコレYouTube検索結果が前回と同じなら、無理に同じ話を書かず別の話題を広げる。

生活ネタは海外副業系ではなく、JMOOC開講中講座を優先して深掘りするようにした。

音楽ネタも前回と被る場合は、1990年代に流行っていたものを25歳の秘書キャラから見た世界観で紹介する。

生成した日記は、はてなブログへ投稿しつつ、Firebaseの diary/{date} に保存する。

LINE Botからは、

@秘書 日記読んで
@秘書 今月の名場面

のように呼び出せる。

なお、LINE VOOMへの自動投稿はやらない。

VOOMはOfficial Account Managerから手動投稿はできるが、Messaging APIで安全に自動投稿する口は見当たらなかったため、この記事の構成では対象外にした。

システム状況確認

Botに「システム」「レンダー」「ファイアベース」「ギットハブ」と聞くと、見える範囲で状態を返すようにした。

確認できるものは以下。

  • Render上でBotが返事できているか
  • Firebaseが読めるか
  • GitHubの最新コミット
  • 画像OCRキューの状態
  • AI会話のON/OFFと課金ガード状態
  • LINEのチャネルシークレット/アクセストークンが環境変数として見えているか

運用中に「今止まってる?」と聞けるのはかなり便利だった。

無料枠で運用するために気をつけたこと

無料枠運用では、便利さよりも暴走しないことを優先した。

やったことは以下。

  • 画像OCRはウイコレっぽいスクショだけ処理
  • 普通の画像には基本返信しない
  • 自動OCRをOFFにできる
  • OFF中のスクショは「集計して」で必要な時だけまとめて処理
  • 一括OCRの処理枚数に上限を設定
  • OCRキューの上限を設定
  • AI会話はデフォルトOFF
  • AI会話には日次/月次上限を設定
  • 課金リスクを検知したらFirebaseに停止フラグを保存
  • 日次/月次のAI枠が戻ったら自動復帰
  • ジオゲームは1日の出題回数とジオコーディング間隔を制限
  • pushMessageの使いすぎに注意

特にLINE Messaging APIは、reply messageとpush messageで扱いが変わるため、運用前に必ず最新の公式料金表を確認した方がいい。

また、Firebaseは「全員が読み書きできる」状態にしない。

LIFFアプリから必要な共有データだけ匿名認証で開き、Bot専用の会話ログ・OCR候補・日記・ジオゲーム状態はAdmin SDKだけが触れるようにした。

作ってよかったこと

一番よかったのは、身内リーグの会話がそのまま運用UIになったこと。

誰かがスクショを投げる。

Botが読み取る。

誰かがOKを押す。

順位が更新される。

未対戦も聞ける。

これだけで、試合結果の管理がかなり楽になった。

また、Botに少しキャラクターを持たせたことで、単なる事務処理ではなく、グループ内のちょっとした盛り上げ役にもなった。

今後やりたいこと

今後やりたいのはこのあたり。

  • 月間MVP
  • 逆転王
  • 最大得点試合
  • 鉄壁賞
  • 未消化警察
  • 週報自動投稿
  • 自動OCR OFF時の候補一覧プレビュー
  • ジオゲームの月間ランキング
  • 日記からQiitaやX向け要約を作る
  • 他ゲームや草スポーツへの横展開

身内リーグ、オンライン対戦、麻雀、ダーツ、草サッカーなど、「少人数で毎月対戦して順位を出す」コミュニティには応用できそうだと思っている。

まとめ

LINE Bot、LIFF、Firebase、OCRを組み合わせると、身内グループのちょっとした運営をかなり楽にできる。

最初は「試合結果を登録できればいい」くらいだったが、作っていくうちに、

  • ルール決め
  • カレンダー
  • 順位表
  • 未対戦管理
  • 自動OCR ON/OFF
  • ジオゲーム
  • ミニゲーム
  • 日次日記
  • システム監視
  • 課金ガード
  • キャラクターBot

まで広がった。

小さなコミュニティのための専用ツールは、作っていてかなり楽しい。

そして、身内が実際に使ってくれると、普通のTODOアプリを作るよりずっと改善点が見つかる。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?