推しアイデア
Lineをつかうことで手軽に参加できる
Lineをつかうことで手軽に参加できる
Lineの手軽さから着想
全てLineで済ませることができる(できた!)
公式LineQRコード
メニュー欄から遊び方を参照して遊ぼう! いくつかの機能をワードを入れることで呼び出して遊びます 開催されているコンテストに早く答えた人からランキングに掲載されます! 上位を目指して
@さくらんぼ&わし
まず、Firebaseを使用してLIFFのデモ版を実装し、動きを確認した
LINE Developersから作成したプロバイダーにLINEログインチャネルを登録した
LINEログインチャネルのメニューから「LIFF」を選択し、「追加」を押した
LIFFアプリ追加のためのフォーム画面に必要事項を入力した。 「Size」ではFullを選択し、全画面にWebアプリの情報が表示されるようにした
「エンドポイントURL」に、表示させたいWebアプリの公開URLを入力した
すべてのフォームを入力したら、「追加」を押して登録を完了した
コンソールに表示している「LIFF URL」をコピーし、 botのトークルームに貼り付けた
「サービス提供者が次の許可をリクエストしています」という表示が出るため、それを許可するとWebアプリがLIFFアプリとして起動する
@さくらんぼ
リッチメニューの画像をPhotoshopで作成し、LINE上で表示できるようにした
画像の色は暖色のパステルカラーを用いて、目に優しいデザインにした
メニューは4項目作成しており、それぞれの項目を押すと、定型文が自動で送信され、それにbotが対応する仕組みになっている
「遊び方」をタップすると、あそびかたを尋ねることができる 「クイズを知りたい!」をタップすると、クイズを教えてもらうことができる 「問題を作りたい!」をタップするとクイズ作成をすることができる 「ランキングページを見たい!」をタップするとランキングページを見ることができる
リッチメニュー画像
リッチメニュー(ボディ)のソースコード
{ "size":{ "width":1920, "height":1080 }, "selected":false, "name":"メニュー", "chatBarText":"メニュー", "areas":[ { "bounds":{ "x":0, "y":0, "width":960, "height":540 }, "action":{ "type":"message", "text":"あそびかた" } }, { "bounds":{ "x":960, "y":0, "width":960, "height":540 }, "action":{ "type":"message", "text":"クイズ教えて" } }, { "bounds":{ "x":0, "y":540, "width":960, "height":540 }, "action":{ "type":"message", "text":"クイズ作成" } }, { "bounds":{ "x":960, "y":540, "width":960, "height":540 }, "action":{ "type":"message", "text":"ランキング" } } ] }
@わし 早く答えた人からランキングに掲載されるシステムにした
正解したらFirestoreにUserNameとTimesstampを保存
web上で早く答えた順になるようにtimestampを昇順にしてからフロントにデータを渡すようにした
async function getRankingData() { const snapshot = await collectionRef.orderBy('timestamp', 'asc').get(); const rankingData = []; snapshot.forEach(doc => { const data = doc.data(); rankingData.push({ id: doc.id, // ドキュメントIDを含める message: data.message, // タイトルフィールドを含める timestamp: data.timestamp, // タイムスタンプフィールドを含める name: data.userName, // ユーザーの名前を含める userid: data.userId }); }); return rankingData; }
正解した人を保存するが同じ問題で複数回回答が保存されないように重複を避ける処理を行う
const RankingData = await getRankingData(); let answered = false; RankingData.forEach((ranking)=> { if (ranking.userid === event.source.userId) { answered = true; } }); if (answered) { return client.replyMessage(event.replyToken, [{ type: 'text', text: '回答済みです!' }]); } else { ・・・
問題が更新されるたびにランキングデータ削除
// ランキングデータ削除処理 const deleteData = await collectionRef.get(); deleteData.forEach(async (doc) => { await doc.ref.delete(); });
@KOU
// 画像のピクセル単位での比較 function compareImages(image1, image2, width, height, channels) { let diff = 0; for (let i = 0; i < width * height * channels; i++) { if (image1[i] !== image2[i]) { diff++; } } return diff; }
@わし 音声問題を出題できるようにしました。
case 'クイズ教えて': // ランダムにクイズを選択 randomIndex = Math.floor(Math.random() * quizDataArray.length);//quizDataArrayからランダムなクイズデータを取得 quizQuestion = quizDataArray[randomIndex].question;// ランダムに選ばれたクイズの質問を取得 quizAnswer = quizDataArray[randomIndex].answer;// ランダムに選ばれたクイズの答えを取得 quiztype = quizDataArray[randomIndex].type;// ランダムに選ばれたクイズのタイプを取得 if(quiztype == 'audio') { const audioname =quizDataArray[randomIndex].audioUrl;// ランダムに選ばれた音声ファイルの名前を取得 const pestionaudio = 'https://4q79vmt0-3000.asse.devtunnels.ms/audio/' + audioname;// 音声ファイルの完全なURLを生成 // LINE APIを使って音声メッセージとテキストメッセージを返信 return client.replyMessage(event.replyToken, [ { type: 'audio', originalContentUrl: pestionaudio, // 変数を直接渡す duration: 3000 // 音声の長さ(ミリ秒) }, { type: 'text', text: 'なんのポケモンか当ててね☆' } ]); }
@KOU
データの形式を書く
基本的にテキストを扱うため手軽なFirestoreを使用
画像なのでもバイナリデータにして保存することで取り扱うことが出来るようにした
クイズデータ
**画像バイナリデータ **
@KOU
VercelのCron Jobsを使って定期的にコンテストの開催が終了したものを削除する(毎日午前5時に実行するようにvercel.jsonで定義)
// 定期実行用のエンドポイント app.get('/api/cron', async (req, res) => { // 問題文を現在時間に近い順で取得(過ぎたものは除く) const nextQuizData = await quiz.where('day', '>=', admin.firestore.Timestamp.now()).get(); // 時間が過ぎているものを削除する const deleteQuizData = await quiz.where('day', '<', admin.firestore.Timestamp.now()).get(); deleteQuizData.forEach(async (doc) => { await doc.ref.delete(); }); res.status(200).json({ message: `Cron job executed successfully${nextQuizData}` }); });
"crons": [ { "path": "/api/cron", "schedule": "0 5 * * *" } ]
https://zenn.dev/ryosuke_horie/articles/7374a83566c6b7
次に開催されるコンテストを取得
// 問題文を現在時間に近い順で取得(過ぎたものは除く) const nextQuizData = await quiz.where('day', '>=', admin.firestore.Timestamp.now()).orderBy('day').limit(1).get();
@KOU