「Discord bot」の版間の差分
Notion-MW |
Notion-MW |
||
98行目: | 98行目: | ||
[https://zenn.dev/azechi/scraps/a98add131e8410 DiscordのSlash Commandをwebhookで受信する方式で作ってみる]にある[https://github.com/discord/discord-interactions-js https://github.com/discord/discord-interactions-js]を使うとよさそう。これをNetlify(AWS Lambda)向けに書き換えて、以下のようになる(前述サイトの内容に加えて、使用された対象のメッセージリンクを送るようにしている)。これが動作するかどうか、まずはテストしてみよう。 | [https://zenn.dev/azechi/scraps/a98add131e8410 DiscordのSlash Commandをwebhookで受信する方式で作ってみる]にある[https://github.com/discord/discord-interactions-js https://github.com/discord/discord-interactions-js]を使うとよさそう。これをNetlify(AWS Lambda)向けに書き換えて、以下のようになる(前述サイトの内容に加えて、使用された対象のメッセージリンクを送るようにしている)。これが動作するかどうか、まずはテストしてみよう。 | ||
< | <syntaxhighlight lang="visual basic">const { InteractionResponseType, InteractionType, verifyKey } = require('discord-interactions') | ||
const CLIENT_PUBLIC_KEY = process.env.CLIENT_PUBLIC_KEY; | const CLIENT_PUBLIC_KEY = process.env.CLIENT_PUBLIC_KEY; | ||
exports.handler = async (event, context) = | exports.handler = async (event, context) => { | ||
console.log(event) | console.log(event) | ||
const signature = event.headers['x-signature-ed25519']; | const signature = event.headers['x-signature-ed25519']; | ||
const timestamp = event.headers['x-signature-timestamp']; | const timestamp = event.headers['x-signature-timestamp']; | ||
console.log(signature, timestamp) | console.log(signature, timestamp) | ||
console.log(event[ | console.log(event["body"]) | ||
const isValidRequest = verifyKey(event[ | const isValidRequest = verifyKey(event["body"], signature, timestamp, CLIENT_PUBLIC_KEY); | ||
if (!isValidRequest) { | if (!isValidRequest) { | ||
console.log( | console.log("! returning 401") | ||
return { | return { | ||
statusCode: 401, | statusCode: 401, | ||
body: 'Bad request signature'} | body: 'Bad request signature'} | ||
} | } | ||
const interaction = JSON.parse(event[ | const interaction = JSON.parse(event["body"]); | ||
if(interaction & | if(interaction && interaction.type === InteractionType.APPLICATION_COMMAND) { | ||
const guild_id = interaction.guild_id | const guild_id = interaction.guild_id | ||
const mess = interaction.data.resolved.messages | const mess = interaction.data.resolved.messages | ||
129行目: | 129行目: | ||
return {statusCode: 200,'headers': {'Content-Type': 'application/json'},body: ret} | return {statusCode: 200,'headers': {'Content-Type': 'application/json'},body: ret} | ||
} else { | } else { | ||
console.log( | console.log("! returning pong") | ||
return {statusCode: 200, body: JSON.stringify({type: InteractionResponseType.PONG} )}; | return {statusCode: 200, body: JSON.stringify({type: InteractionResponseType.PONG} )}; | ||
} | } | ||
}</ | }</syntaxhighlight> | ||
CLIENT_PUBLIC_KEYはnetlifyの画面の環境変数から設定する(よくあるやつ)。 | CLIENT_PUBLIC_KEYはnetlifyの画面の環境変数から設定する(よくあるやつ)。 | ||
<ul> | <ul> | ||
<li><p>最初試していたとき、Netlify上では応答を返すところまで上手くいっているのに「インタラクションに失敗しました」interaction failed(一旦画面移動してから見ると「アプリケーションが応答しませんでした」application did not respondに変わっている)と出続けていたが、応答に</p> | <li><p>最初試していたとき、Netlify上では応答を返すところまで上手くいっているのに「インタラクションに失敗しました」interaction failed(一旦画面移動してから見ると「アプリケーションが応答しませんでした」application did not respondに変わっている)と出続けていたが、応答に</p> | ||
< | <syntaxhighlight lang="</syntaxhighlight></li></ul>"> | ||
visual basic 'headers': { 'Content-Type': 'application/json', }, ``` | |||
<pre>を付けたら通った…</pre> | |||
* 関連リンク | |||
** [https://zenn.dev/azu0925/scraps/a6daaf258432dd discord botのinterationsのセキュリティ] | |||
** [https://gammalab.net/blog/pky623yr9s6th/ Discordのslash commandでSQSにメッセージを送る - GammaLab] | |||
=== エンドポイントの分割 === | === エンドポイントの分割 === | ||
186行目: | 184行目: | ||
==== pininit.js ==== | ==== pininit.js ==== | ||
< | <syntaxhighlight lang="javascript">const { InteractionResponseType, InteractionType, verifyKey } = require('discord-interactions') | ||
const fetch = require( | const fetch = require("node-fetch-commonjs"); | ||
const CLIENT_PUBLIC_KEY = process.env.CLIENT_PUBLIC_KEY; | const CLIENT_PUBLIC_KEY = process.env.CLIENT_PUBLIC_KEY; | ||
const PIN_ENDPOINT = process.env.PIN_ENDPOINT; | const PIN_ENDPOINT = process.env.PIN_ENDPOINT; | ||
exports.handler = async (event, context) = | exports.handler = async (event, context) => { | ||
//署名の検証が必要 | //署名の検証が必要 | ||
const signature = event.headers['x-signature-ed25519']; | const signature = event.headers['x-signature-ed25519']; | ||
const timestamp = event.headers['x-signature-timestamp']; | const timestamp = event.headers['x-signature-timestamp']; | ||
const isValidRequest = verifyKey(event[ | const isValidRequest = verifyKey(event["body"], signature, timestamp, CLIENT_PUBLIC_KEY); | ||
if (!isValidRequest) { | if (!isValidRequest) { | ||
console.log( | console.log("! returning 401") | ||
return { statusCode: 401, body: 'Bad request signature' } | return { statusCode: 401, body: 'Bad request signature' } | ||
} | } | ||
const interaction = JSON.parse(event[ | const interaction = JSON.parse(event["body"]); | ||
if (interaction & | if (interaction && interaction.type === InteractionType.APPLICATION_COMMAND) { | ||
console.log( | console.log("called as an interaction") | ||
//初期メッセージのための各種情報を取得 | //初期メッセージのための各種情報を取得 | ||
221行目: | 219行目: | ||
'Content-Type': 'application/json' // データ形式をJSONに指定 | 'Content-Type': 'application/json' // データ形式をJSONに指定 | ||
}, | }, | ||
body: event[ | body: event["body"] // そのまま転送 | ||
}); | }); | ||
//wait some time for TLS handshake | //wait some time for TLS handshake | ||
await new Promise(resolve = | await new Promise(resolve => setTimeout(resolve, 500)) | ||
const ret = JSON.stringify({//1 | const ret = JSON.stringify({//1 << 6 means EPHEMERAL | ||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, | type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, | ||
data: { flags: 1 | data: { flags: 1 << 6, content: `${dispname} ${interaction.data.name}ned: https://discord.com/channels/${interaction.guild_id}/${channel_id}/${message_id}` }, | ||
}) | }) | ||
return { statusCode: 200, 'headers': { 'Content-Type': 'application/json' }, body: ret } | return { statusCode: 200, 'headers': { 'Content-Type': 'application/json' }, body: ret } | ||
} else { | } else { | ||
console.log( | console.log("! returning pong") | ||
return { statusCode: 200, body: JSON.stringify({ type: InteractionResponseType.PONG }) }; | return { statusCode: 200, body: JSON.stringify({ type: InteractionResponseType.PONG }) }; | ||
} | } | ||
}</ | }</syntaxhighlight> | ||
<span id="pinjs"></span> | <span id="pinjs"></span> | ||
==== pin.js ==== | ==== pin.js ==== | ||
< | <syntaxhighlight lang="javascript">const { InteractionResponseType, InteractionType, verifyKey } = require('discord-interactions') | ||
const fetch = require( | const fetch = require("node-fetch-commonjs"); | ||
const CLIENT_PUBLIC_KEY = process.env.CLIENT_PUBLIC_KEY; | const CLIENT_PUBLIC_KEY = process.env.CLIENT_PUBLIC_KEY; | ||
250行目: | 248行目: | ||
{ headers: { 'Authorization': `Bot ${BOT_TOKEN}` } }); | { headers: { 'Authorization': `Bot ${BOT_TOKEN}` } }); | ||
const pins = await pinsResponse.json(); | const pins = await pinsResponse.json(); | ||
const pinned = pins.find(pin = | const pinned = pins.find(pin => pin.id === message_id) | ||
if (commandName == | if (commandName == "pin" ^ !pinned) return `the message is already ${commandName}ned`; | ||
const reqtype = commandName == | const reqtype = commandName == "pin" ? 'PUT' : 'DELETE' | ||
const pinreq = await fetch(`https://discord.com/api/v10/channels/${channel_id}/pins/${message_id}`, | const pinreq = await fetch(`https://discord.com/api/v10/channels/${channel_id}/pins/${message_id}`, | ||
{ method: reqtype, headers: { 'Authorization': `Bot ${BOT_TOKEN}` } }); | { method: reqtype, headers: { 'Authorization': `Bot ${BOT_TOKEN}` } }); | ||
258行目: | 256行目: | ||
if (pinreq.status != 204) return await pinreq.text() | if (pinreq.status != 204) return await pinreq.text() | ||
//エラーにならなければ成功 | //エラーにならなければ成功 | ||
console.log(commandName + | console.log(commandName + "ned!!") | ||
// | //"pin-agent-log"チャンネルを探す | ||
const channelsResponse = await fetch(`https://discord.com/api/v10/guilds/${guild_id}/channels`, | const channelsResponse = await fetch(`https://discord.com/api/v10/guilds/${guild_id}/channels`, | ||
{ headers: { 'Authorization': `Bot ${BOT_TOKEN}` } }); | { headers: { 'Authorization': `Bot ${BOT_TOKEN}` } }); | ||
const channels = await channelsResponse.json(); | const channels = await channelsResponse.json(); | ||
const targetChannel = channels.find(channel = | const targetChannel = channels.find(channel => channel.name === "pin-agent-log"); | ||
if (!targetChannel) return 'Channel pin-agent-log not found!!!!'; | if (!targetChannel) return 'Channel pin-agent-log not found!!!!'; | ||
//メッセージの述語部分 | //メッセージの述語部分 | ||
286行目: | 284行目: | ||
'Content-Type': 'application/json', | 'Content-Type': 'application/json', | ||
}, | }, | ||
body: JSON.stringify({ content: ` | body: JSON.stringify({ content: `<@${user_info.id}> ${maintext}` }), | ||
}); | }); | ||
return | return "pin and log ok" | ||
} | } | ||
294行目: | 292行目: | ||
//いつ初期メッセージが送られるかわからないので、4回試行してできるだけ早いうちに削除 | //いつ初期メッセージが送られるかわからないので、4回試行してできるだけ早いうちに削除 | ||
//見た感じ、ほぼ確実に1回目で削除できているようだが… | //見た感じ、ほぼ確実に1回目で削除できているようだが… | ||
return new Promise(async (resolve) = | return new Promise(async (resolve) => { | ||
for (let i = 0; i | for (let i = 0; i < 4; i++) { | ||
const res = await fetch(`https://discord.com/api/v10/webhooks/${appId}/${interaction_token}/messages/@original`, { method: 'DELETE' }); | const res = await fetch(`https://discord.com/api/v10/webhooks/${appId}/${interaction_token}/messages/@original`, { method: 'DELETE' }); | ||
//204だったら削除成功 | //204だったら削除成功 | ||
if (res.status === 204) resolve( | if (res.status === 204) resolve("delete ok: " + i) | ||
console.log(res.statusText) | console.log(res.statusText) | ||
await new Promise((r) = | await new Promise((r) => setTimeout(r, 1100)); // 1.1秒待つ | ||
} | } | ||
}); | }); | ||
} | } | ||
exports.handler = async (event, context) = | exports.handler = async (event, context) => { | ||
const signature = event.headers['x-signature-ed25519']; | const signature = event.headers['x-signature-ed25519']; | ||
const timestamp = event.headers['x-signature-timestamp']; | const timestamp = event.headers['x-signature-timestamp']; | ||
const isValidRequest = verifyKey(event[ | const isValidRequest = verifyKey(event["body"], signature, timestamp, CLIENT_PUBLIC_KEY); | ||
if (!isValidRequest) { | if (!isValidRequest) { | ||
console.log( | console.log("! returning 401") | ||
return { | return { | ||
statusCode: 401, | statusCode: 401, | ||
316行目: | 314行目: | ||
} | } | ||
} | } | ||
const interaction = JSON.parse(event[ | const interaction = JSON.parse(event["body"]); | ||
if (interaction & | if (interaction && interaction.type === InteractionType.APPLICATION_COMMAND) { | ||
//各種情報を取得 | //各種情報を取得 | ||
const mess_data = interaction.data.resolved.messages | const mess_data = interaction.data.resolved.messages | ||
330行目: | 328行目: | ||
return { | return { | ||
statusCode: 200, | statusCode: 200, | ||
body: | body: "ok" | ||
} | } | ||
} else { | } else { | ||
console.log( | console.log("! returning pong") | ||
return { statusCode: 200, body: JSON.stringify({ type: InteractionResponseType.PONG }) }; | return { statusCode: 200, body: JSON.stringify({ type: InteractionResponseType.PONG }) }; | ||
} | } | ||
}</ | }</syntaxhighlight> | ||
CLIENT_PUBLIC_KEYとBOT_TOKENはそれぞれDiscordのサイトから取る。PIN_ENDPOINTは各自の設定にあわせて<code>https://</code><em><strong><code>foo</code></strong></em><code>.netlify.app/.netlify/functions/</code><em><strong><code>bar</code></strong></em>のようになる。3つともNetlifyの環境変数に入れる。 | CLIENT_PUBLIC_KEYとBOT_TOKENはそれぞれDiscordのサイトから取る。PIN_ENDPOINTは各自の設定にあわせて<code>https://</code><em><strong><code>foo</code></strong></em><code>.netlify.app/.netlify/functions/</code><em><strong><code>bar</code></strong></em>のようになる。3つともNetlifyの環境変数に入れる。 | ||