Discord ボットのインタラクティブな可能性を解き放ちます。このチャプターでは、魅力的なボタンメッセージを作成し、ユーザーのインタラクションによってタスクがトリガーされる設定方法に焦点を当てます。Skeet の機能を利用して、ボットを単なる機能的な存在から、インタラクティブでエンゲージングなものへとデザインする方法を学びます。
Firestore ユーザーモデルの設計 🛠️📊
最初の重要なステップとして、私たちはFirestoreデータベースにユーザーモデルを慎重に設計します。このモデルは、ユーザー情報の管理を効率化し、アプリケーションのパフォーマンスを最適化します。このモデルはユーザーID、名前、そしてアプリケーション固有のカスタムフィールドを含むことができます。
本書では、DiscordUser と StripeUser の2つの異なるモデルを使って、ユーザーデータを管理します。単一の User モデルでの実装が可能かもしれませんが、今回はメンバーシップのステータスを持つ二種類のユーザー、つまり一般メンバーと有料会員メンバーを別々に実装します。多数の一般ユーザーと限られた数の会員メンバーのデータを効率的に処理するため、分けて管理することでクエリのコストを削減し、スケーラビリティを向上させます。
このセクションでは、ユーザーのID、名前、アイコン、役割、レベル、HP、MP、経験値、作成日時、更新日時などを含めた総合的なユーザーモデルを構築します。ここでは、簡潔ながらも柔軟性に富んだモデルを紹介し、実践的なコーディング例を通してその構築方法を学びます。
functions/skeet/src/lib/models/discordUserModels.ts
における DiscordUser の型定義は以下の通りです:
import { Timestamp, FieldValue } from '@skeet-framework/firestore'
// CollectionId: DiscordUser
// DocumentId: auto
// Path: DiscordUser
export const DiscordUserCN = 'DiscordUser'
export type DiscordUser = {
id?: string
pubkey: string
username: string
email: string
iconUrl: string | null
roles: string[]
lv: number
hp: number
mp: number
exp: number
isMembership: boolean
createdAt?: Timestamp | FieldValue
updatedAt?: Timestamp | FieldValue
}
これで、Firestore データベースにユーザーモデルを作成する準備が整いました。
3つボタンを使ってユーザーのデータを Firestore と連携 🖲️💾
次に、Discord ボットにボタン機能を実装し、ユーザーがボタンをクリックすることで Firestore に自動的に登録されるようにします。これにより、ユーザーが簡単にボットの機能を利用できるようになります。
以下の3つのボタンを作成します。
- 登録ボタン
- ステータスを見るボタン
- ログインボーナスをもらうボタン
registerMessage.ts
を以下のように追加します。
functions/skeet/src/lib/discord/messages/registerMessage.ts
import {
ButtonStyleTypes,
MessageComponentTypes,
} from '@skeet-framework/discord-utils'
export const registerMessage = () => {
const body = {
content: 'ボタンを押してね!',
components: [
{
type: MessageComponentTypes.ACTION_ROW,
components: [
{
type: MessageComponentTypes.BUTTON,
style: ButtonStyleTypes.PRIMARY,
label: '📗 登録する',
custom_id: 'register-button',
},
],
},
{
type: MessageComponentTypes.ACTION_ROW,
components: [
{
type: MessageComponentTypes.BUTTON,
style: ButtonStyleTypes.PRIMARY,
label: '📕 ステータスを見る',
custom_id: 'view-button',
},
],
},
{
type: MessageComponentTypes.ACTION_ROW,
components: [
{
type: MessageComponentTypes.BUTTON,
style: ButtonStyleTypes.PRIMARY,
label: '📘 ログインボーナスをもらう',
custom_id: 'bonus-button',
},
],
},
],
}
return body
}
そして、3 つの custom_id
に対応するアクションを functions/skeet/src/lib/discord/actions/
に作成します。
registerAction.ts
viewAction.ts
bonusAction.ts
ボタンを押したら Firestore にユーザーを登録する 🖲️💾
まずは、registerAction.ts
を以下のように追加します。
functions/skeet/src/lib/discord/actions/registerAction.ts
import { DiscordUser, DiscordUserCN } from '@/models/discordUserModels'
import {
DiscordRouterParams,
deferResponse,
updateResponse,
} from '@skeet-framework/discord-utils'
import { add } from '@skeet-framework/firestore'
import { Response } from 'firebase-functions/v1'
import { inspect } from 'util'
export const registerAction = async (
res: Response,
db: FirebaseFirestore.Firestore,
discordToken: string,
body: DiscordRouterParams
) => {
console.log('helloAction')
console.log(inspect(body))
await deferResponse(discordToken, body.id, body.token)
const userParams: DiscordUser = {
pubkey: '',
username: body.member.user.username,
email: '',
iconUrl: body.member.user.avatar,
roles: body.member.roles,
lv: 1,
hp: 100,
mp: 30,
exp: 0,
isMembership: false,
}
const user = await add<DiscordUser>(
db,
DiscordUserCN,
userParams,
body.member.user.id
)
if (!user) {
await updateResponse(discordToken, body.application_id, body.token, {
content: `⚠️ Failed to create user.Please try again in a few seconds.`,
flags: 64,
})
return false
}
await updateResponse(discordToken, body.application_id, body.token, {
content: `Hello, ${userParams.username} created!\nDocumentId:${user.id}`,
flags: 64,
})
return true
}
ボタンを押したら Firestore のユーザーを表示する 🖲️👀
次に、viewAction.ts
を以下のように追加します。
functions/skeet/src/lib/discord/actions/viewAction.ts
import { DiscordUser, DiscordUserCN } from '@/models/discordUserModels'
import {
DiscordRouterParams,
deferResponse,
updateResponse,
} from '@skeet-framework/discord-utils'
import { add, get } from '@skeet-framework/firestore'
import { Response } from 'firebase-functions/v1'
export const viewAction = async (
res: Response,
db: FirebaseFirestore.Firestore,
discordToken: string,
body: DiscordRouterParams
) => {
console.log('viewAction')
await deferResponse(discordToken, body.id, body.token)
const user = await get<DiscordUser>(db, DiscordUserCN, body.member.user.id)
if (!user) {
await updateResponse(discordToken, body.application_id, body.token, {
content: `⚠️ Failed to get user.Please try again in a few seconds.`,
flags: 64,
})
return false
}
// Discord Embed Message
const emdbed = {
title: 'あなたのステータス🛡️',
description: `名前: ${user.username}\nLv:${user.lv}\nHP:${user.hp}\nMP:${user.mp}\nEXP:${user.exp}`,
color: 0x0099ff,
timestamp: new Date(),
footer: {
text: 'powered by Skeet Framework',
icon_url:
'https://storage.googleapis.com/skeet-assets/imgs/logo/SkeetLogoSquare.png',
},
}
await updateResponse(discordToken, body.application_id, body.token, {
embeds: [emdbed],
flags: 64,
})
return true
}
レスポンスに Discord Embed Message を追加することで、ユーザーにより魅力的なメッセージを送信することができます。
メッセージの詳細については、以下のドキュメントを参照してください。
ボタンを押したら Firestore のユーザーを更新する 🖲️🔄
次に、bonusAction.ts
を以下のように追加します。
functions/skeet/src/lib/discord/actions/bonusAction.ts
import { DiscordUser, DiscordUserCN } from '@/models/discordUserModels'
import {
DiscordRouterParams,
deferResponse,
updateResponse,
} from '@skeet-framework/discord-utils'
import { get, update } from '@skeet-framework/firestore'
import { Response } from 'firebase-functions/v1'
export const bonusAction = async (
res: Response,
db: FirebaseFirestore.Firestore,
discordToken: string,
body: DiscordRouterParams
) => {
console.log('bonusAction')
await deferResponse(discordToken, body.id, body.token)
const user = await get<DiscordUser>(db, DiscordUserCN, body.member.user.id)
if (!user) {
await updateResponse(discordToken, body.application_id, body.token, {
content: `⚠️ Failed to get user.Please try again in a few seconds.`,
flags: 64,
})
return false
}
const bonus = Math.floor(Math.random() * 100)
const newExp = user.exp + bonus
let lv = user.lv
const expToNextLevel = 100 * lv
if (newExp >= expToNextLevel) {
lv++
}
await update<DiscordUser>(db, DiscordUserCN, body.member.user.id, {
exp: newExp,
lv,
})
let description = `あなたは${bonus}の EXP を獲得しました!`
if (lv > user.lv) {
description += `\nレベルが${lv}に上がりました!`
}
// Discord Embed Message
const emdbed = {
description,
color: 0x0099ff,
}
await updateResponse(discordToken, body.application_id, body.token, {
embeds: [emdbed],
flags: 64,
})
return true
}
新しいボタンメッセージの送信 🖲️🔥
root
ディレクトリに registerMessage
を作成し、以下のように追加します。
import { onRequest } from 'firebase-functions/v2/https'
import { publicHttpOption } from '@/routings/options'
import { TypedRequestBody } from '@/types/http'
import { RootParams } from '@/types/http/rootParams'
import { messageChannel } from '@skeet-framework/discord-utils'
import { defineSecret } from 'firebase-functions/params'
import { registerMessage } from '@/lib/discord/messages/registerMessage'
const DISCORD_TOKEN = defineSecret('DISCORD_TOKEN')
export const root = onRequest(
{ ...publicHttpOption, secrets: [DISCORD_TOKEN] },
async (req: TypedRequestBody<RootParams>, res) => {
try {
// Define your logic here
const message = registerMessage()
const channelId = 'your-channel-id'
await messageChannel(DISCORD_TOKEN.value(), channelId, message)
res.json({ status: 'success' })
} catch (error) {
res.status(500).json({ status: 'error', message: String(error) })
}
}
)
skeet s
コマンドでローカルサーバーを起動し、
テスト用の root
エンドポイント、
http://127.0.0.1:5001/your-project-id/asia-northeast1/root
にアクセスすると、ボタンメッセージが表示されます。
DiscordRouter にボタンアクションを追加する 🖲️🔥
functions/skeet/src/routings/discordRouter.ts
にボタンアクションを追加します。
if 文の中に、ボタンの custom_id
に対応するアクションを追加します。
// Import new actions
import { viewAction } from '@/lib/discord/actions/viewAction'
import { registerAction } from '@/lib/discord/actions/registerAction'
import { bonusAction } from '@/lib/discord/actions/bonusAction'
~中略~
...
} else {
// Button action
const { custom_id } = req.body.data as ActionData
if (custom_id.match(/^register-button$/)) {
await registerAction(res, db, DISCORD_TOKEN.value(), req.body)
}
if (custom_id.match(/^view-button$/)) {
await viewAction(res, db, DISCORD_TOKEN.value(), req.body)
}
if (custom_id.match(/^bonus-button$/)) {
await bonusAction(res, db, DISCORD_TOKEN.value(), req.body)
}
}
...
これで、ボタンを押すと Firestore にユーザーが登録され、ユーザーのステータスを見ることができ、ログインボーナスをもらうことができるようになりました。
デプロイして Discord ボットを更新する 🖲️🔥
それでは、デプロイして Discord ボットを更新しましょう。
skeet deploy --function skeet:discordRouter
ボタンアクションを実行する 🖲️🔥
Discord ボットに追加したボタンを押してみましょう。
- 登録ボタン
- ステータスを見るボタン
- ログインボーナスをもらうボタン
無事に Firestore とユーザーデータが連携されました🎉
続いて、何度かログインボーナスをもらうと
レベルがあがりました🎉
このように Discordのインタラクションと Firestoreのデータを連携させることができました。