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)向けに書き換えて、以下のようになる(前述サイトの内容に加えて、使用された対象のメッセージリンクを送るようにしている)。これが動作するかどうか、まずはテストしてみよう。


<pre class="dummy_str_visual_basic">const { InteractionResponseType, InteractionType, verifyKey } = require('discord-interactions')
<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) =&gt; {
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[&quot;body&quot;])
     console.log(event["body"])
     const isValidRequest = verifyKey(event[&quot;body&quot;], signature, timestamp, CLIENT_PUBLIC_KEY);
     const isValidRequest = verifyKey(event["body"], signature, timestamp, CLIENT_PUBLIC_KEY);
     if (!isValidRequest) {
     if (!isValidRequest) {
         console.log(&quot;! returning 401&quot;)
         console.log("! returning 401")
       return {
       return {
         statusCode: 401,
         statusCode: 401,
         body: 'Bad request signature'}
         body: 'Bad request signature'}
     }
     }
     const interaction = JSON.parse(event[&quot;body&quot;]);
     const interaction = JSON.parse(event["body"]);


     if(interaction &amp;&amp; interaction.type === InteractionType.APPLICATION_COMMAND) {
     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(&quot;! returning pong&quot;)
     console.log("! returning pong")
     return {statusCode: 200, body: JSON.stringify({type: InteractionResponseType.PONG} )};
     return {statusCode: 200, body: JSON.stringify({type: InteractionResponseType.PONG} )};
     }
     }
}</pre>
}</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>
<pre class="dummy_str_visual_basic">'headers': {
<syntaxhighlight lang="</syntaxhighlight></li></ul>">
            'Content-Type': 'application/json',
visual basic 'headers': { 'Content-Type': 'application/json', }, ```
        },</pre>
 
<p>を付けたら通った…</p></li>
<pre>を付けたら通った…</pre>
<li><p>関連リンク</p>
* 関連リンク
<ul>
** [https://zenn.dev/azu0925/scraps/a6daaf258432dd discord botのinterationsのセキュリティ]
<li>[https://zenn.dev/azu0925/scraps/a6daaf258432dd discord botのinterationsのセキュリティ]</li>
** [https://gammalab.net/blog/pky623yr9s6th/ Discordのslash commandでSQSにメッセージを送る &#45; GammaLab]
<li>[https://gammalab.net/blog/pky623yr9s6th/ Discordのslash commandでSQSにメッセージを送る &#45; GammaLab]</li></ul>
</li></ul>


=== エンドポイントの分割 ===
=== エンドポイントの分割 ===
186行目: 184行目:
==== pininit.js ====
==== pininit.js ====


<pre class="dummy_str_javascript">const { InteractionResponseType, InteractionType, verifyKey } = require('discord-interactions')
<syntaxhighlight lang="javascript">const { InteractionResponseType, InteractionType, verifyKey } = require('discord-interactions')


const fetch = require(&quot;node-fetch-commonjs&quot;);
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) =&gt; {
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[&quot;body&quot;], signature, timestamp, CLIENT_PUBLIC_KEY);
     const isValidRequest = verifyKey(event["body"], signature, timestamp, CLIENT_PUBLIC_KEY);
     if (!isValidRequest) {
     if (!isValidRequest) {
         console.log(&quot;! returning 401&quot;)
         console.log("! returning 401")
         return { statusCode: 401, body: 'Bad request signature' }
         return { statusCode: 401, body: 'Bad request signature' }
     }
     }
     const interaction = JSON.parse(event[&quot;body&quot;]);
     const interaction = JSON.parse(event["body"]);


     if (interaction &amp;&amp; interaction.type === InteractionType.APPLICATION_COMMAND) {
     if (interaction && interaction.type === InteractionType.APPLICATION_COMMAND) {
         console.log(&quot;called as an interaction&quot;)
         console.log("called as an interaction")


         //初期メッセージのための各種情報を取得
         //初期メッセージのための各種情報を取得
221行目: 219行目:
                 'Content-Type': 'application/json' // データ形式をJSONに指定
                 'Content-Type': 'application/json' // データ形式をJSONに指定
             },
             },
             body: event[&quot;body&quot;] // そのまま転送
             body: event["body"] // そのまま転送
         });
         });
         //wait some time for TLS handshake
         //wait some time for TLS handshake
         await new Promise(resolve =&gt; setTimeout(resolve, 500))
         await new Promise(resolve => setTimeout(resolve, 500))
         const ret = JSON.stringify({//1 &lt;&lt; 6 means EPHEMERAL
         const ret = JSON.stringify({//1 << 6 means EPHEMERAL
             type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
             type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
             data: { flags: 1 &lt;&lt; 6, content: `${dispname} ${interaction.data.name}ned: https://discord.com/channels/${interaction.guild_id}/${channel_id}/${message_id}` },
             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(&quot;! returning pong&quot;)
         console.log("! returning pong")
         return { statusCode: 200, body: JSON.stringify({ type: InteractionResponseType.PONG }) };
         return { statusCode: 200, body: JSON.stringify({ type: InteractionResponseType.PONG }) };
     }
     }
}</pre>
}</syntaxhighlight>
<span id="pinjs"></span>
<span id="pinjs"></span>
==== pin.js ====
==== pin.js ====


<pre class="dummy_str_javascript">const { InteractionResponseType, InteractionType, verifyKey } = require('discord-interactions')
<syntaxhighlight lang="javascript">const { InteractionResponseType, InteractionType, verifyKey } = require('discord-interactions')
const fetch = require(&quot;node-fetch-commonjs&quot;);
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 =&gt; pin.id === message_id)
     const pinned = pins.find(pin => pin.id === message_id)
     if (commandName == &quot;pin&quot; ^ !pinned) return `the message is already ${commandName}ned`;
     if (commandName == "pin" ^ !pinned) return `the message is already ${commandName}ned`;
     const reqtype = commandName == &quot;pin&quot; ? 'PUT' : 'DELETE'
     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 + &quot;ned!!&quot;)
     console.log(commandName + "ned!!")
     //&quot;pin-agent-log&quot;チャンネルを探す
     //"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 =&gt; channel.name === &quot;pin-agent-log&quot;);
     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: `&lt;@${user_info.id}&gt; ${maintext}` }),
         body: JSON.stringify({ content: `<@${user_info.id}> ${maintext}` }),
     });
     });
     return &quot;pin and log ok&quot;
     return "pin and log ok"
}
}


294行目: 292行目:
     //いつ初期メッセージが送られるかわからないので、4回試行してできるだけ早いうちに削除
     //いつ初期メッセージが送られるかわからないので、4回試行してできるだけ早いうちに削除
     //見た感じ、ほぼ確実に1回目で削除できているようだが…
     //見た感じ、ほぼ確実に1回目で削除できているようだが…
     return new Promise(async (resolve) =&gt; {
     return new Promise(async (resolve) => {
         for (let i = 0; i &lt; 4; 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(&quot;delete ok: &quot; + i)
             if (res.status === 204) resolve("delete ok: " + i)
             console.log(res.statusText)
             console.log(res.statusText)
             await new Promise((r) =&gt; setTimeout(r, 1100)); // 1.1秒待つ
             await new Promise((r) => setTimeout(r, 1100)); // 1.1秒待つ
         }
         }
     });
     });
}
}


exports.handler = async (event, context) =&gt; {
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[&quot;body&quot;], signature, timestamp, CLIENT_PUBLIC_KEY);
     const isValidRequest = verifyKey(event["body"], signature, timestamp, CLIENT_PUBLIC_KEY);
     if (!isValidRequest) {
     if (!isValidRequest) {
         console.log(&quot;! returning 401&quot;)
         console.log("! returning 401")
         return {
         return {
             statusCode: 401,
             statusCode: 401,
316行目: 314行目:
         }
         }
     }
     }
     const interaction = JSON.parse(event[&quot;body&quot;]);
     const interaction = JSON.parse(event["body"]);


     if (interaction &amp;&amp; interaction.type === InteractionType.APPLICATION_COMMAND) {
     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: &quot;ok&quot;
             body: "ok"
         }
         }
     } else {
     } else {
         console.log(&quot;! returning pong&quot;)
         console.log("! returning pong")
         return { statusCode: 200, body: JSON.stringify({ type: InteractionResponseType.PONG }) };
         return { statusCode: 200, body: JSON.stringify({ type: InteractionResponseType.PONG }) };
     }
     }
}</pre>
}</syntaxhighlight>
CLIENT_PUBLIC_KEYとBOT_TOKENはそれぞれDiscordのサイトから取る。PIN_ENDPOINTは各自の設定にあわせて<code>https&#58;//</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&#58;//</code><em><strong><code>foo</code></strong></em><code>.netlify.app/.netlify/functions/</code><em><strong><code>bar</code></strong></em>のようになる。3つともNetlifyの環境変数に入れる。