YouTubeとニコニコ動画のAPIを用いたMisskeyBotを開発した話

2023.12.24 公開ぼすきーアドカレ3rd 12/24記事※この記事は執筆時点での情報についてまとめています。ソースコードやBotの仕様は変更されている可能性があります。

初めましての方ははじめまして。双葉湊音オタクの「はやピー」という者です。

この記事は、Misskeyのサーバーの一つ、ぼすきーにて開催(?)されている「ぼすきーアドベントカレンダー(3枠目)」の12/24の記事です。が、今現在執筆している時間は12/24の正午。そしてまだアドカレに登録していません(!?)。この文章を読んでいるということは、無事に記事を書き上げたということです。褒めてください()

さて、題名にもある通り、今回はYouTubeとニコニコ動画のAPIを用いたMisskeyBotの開発について話したいと思います。一応解説を含みますが、他のサイトではあまり解説されていないようなポイントに絞って解説をするので、他のサイトの方を参考にしたほうが絶対にいいです。

なお、この記事では「プログラミングなんもわからない人がちょっとやってみようかな〜と思える」ことを大切にしているため、表現がざっくりしていたり、厳密に言うと違うみたいなところを無視しています。またソースコードもあまりキレイではありません。エンジニアの皆様におかれましては、者申したいところが多いかと思いますが、どうか許してください。単純に私技術不足なんです許して。

それと、こういった記事を書くのは慣れていないので、お見苦しい点あると思いますがご容赦ください。

自己紹介

さて私のことをだれやねんと思っている人も多いと思うので、ちょっと自己紹介させてください。

ぼすきーを始めとする様々なSNSに生息する学生です。

今年になってからやっと本格的にプログラミングを始めました。

今年になってから双葉湊音オタクになりました。

最近ボカロ曲を作り始めました。

最近絵の練習を始めました。


こんな感じです(雑)

一応リンク張っておきます。

ぼすきー:https://voskey.icalo.net/@hayapii_k

今回作ったもの

それでは今回作ったものを早速見ていきましょう。

今回作ったのは、「双葉湊音が登場するYouTube動画、ニコニコ動画をぼすきーに宣伝するBot」です。

動作例(開発中のため現在稼働中の画面とは異なる場合があります)

上の画像のように、双葉湊音関連の動画を宣伝します。

以下がボットのアカウントです。

https://voskey.icalo.net/@minato_bot 

使用言語・サービス

今回利用したサービスは以下の通りです。

特に有料サービスは使っていないので、技術があれば誰でも気軽にできるでしょう。

ちなみにGASはGoogleアカウント、Misskey APIは任意のサーバーのアカウントが必要となります。

Step0,事前準備

それでは、これからどのようにして作り上げたのかの解説をします。

まずはGoogleAppsScriptのページにアクセスして、新しいプロジェクトを作成します。

Step1,MisskeyAPIを完全に理解しよう

それでは、GASからMisskeyに投稿する方法を解説します。

が、これについてはすでに有識者が解説をさているので、以下のサイトを参考に作業をやってみてください。

【コピペでOK】GoogleAppsScriptを使ってMisskeyに投稿する方法 by なおしむ


できましたか?私はできませんでした。(は?)

ここで私はある問題を起こしてしまいます。

何故か実行できない.........

GAS「TypeError: Cannot read properties of undefined (reading 'server')」

こういうエラーが出たら、2310%実行する関数を間違えています。

上記のサイトでは、postToMisskey()という関数を作成しています。しかし実際にこれを実行するには、引数を与えなくてはなりません。私はこの関数を直接実行してしまったせいで、このようなエラーが発生しました。

このサイトの一番下の画像のソースコードでいうと、misskeytest()という関数を実行する必要があったのです。上の方にある「実行する関数を選択」から、misskeytest()という関数を実行しましょう。うまく投稿できているはずです。


皆さんはこれで、GASからMisskeyに投稿する方法を学ぶことができました。



しかし、Misskeyには他の機能もあります。ノート作成においても、例えば公開範囲を設定したり、アンケートを設定したり、様々な設定があります。

改めて見てみると様々な機能がある。

そんなわけで、たしかに投稿はできるようにはなりましたが、完全には理解してないわけです。

ということでMisskey APIの公式ドキュメントにアクセスしましょう!

https://misskey-hub.net/ja/docs/for-developers/api/

※本記事執筆時の2023/12/24現在、Misskey Hubの大幅なアップデートが行われている途中です。APIのページについては、まだページのアップデートが行われておりません。本記事では、従来のMisskey Hubのページを参考に解説を行いますので、現在のページと表現が一部異なる可能性があります。


それでは、ここからコピペしたソースコードを使って、一体何が行われているのかを軽く解説します。以下がソースコードです。

function postToMisskey(text,options) {

 return UrlFetchApp.fetch(

   `https://${options.server}/api/notes/create`,

   {

     'method': 'POST',

     'headers' : {'Content-Type': 'application/json'},

     'payload': JSON.stringify({ i: options.token, text: text})

   }

 );

}


function misskeytest() {

 postToMisskey('双葉湊音は可愛い。異論は認めない。',{server: 'voskey.icalo.net',taken: 'QAWSEDRFTGYHUJIKOLP'});

まず、Misskey Hubには"HTTP APIはすべてPOST"という記載があります。これは5行目の"method"というところに書かれています。こちらのサイトを参考にしてみてください(難しいところはガンガン読み飛ばしてOK)

次に、"要求ヘッダーにContent-Type: application/jsonを指定します。"という記載があります。これは6行目の"headers"というところに、"'Content-Type':'application/json'"というところに書かれています。たしかにヘッダーです。APIを使うには、要求ヘッダーというものが必要そうです。

次、"アクセストークンは、iというパラメータ名でリクエストボディJSONに含めます。"という記載。これは、7行目のpayloadに書かれています。アクセストークンはここに書け!ということです。

最後に、"ベースURLはhttps://{サーバーのドメイン}/apiです。"という記載。これは超ざっくり言えば「ここにデータを送ってね」という意味で、3行目に記載されています。これはまたあとで解説します。

という感じで、なんとなく理解していただけたでしょうか。とりあえず完全に理解できなくてもなんとなくでわかれば大丈夫です。正直私もよくわかりません。


さて次からが面白いところです。エンドポイント一覧(従来のMisskey Hub)にアクセスします。そして、絞り込みなどでnotes/createのページにアクセスしてください。

さて、notes/createのページにやってきました。....notes/createって、どっかで見たことありませんか?そう、さっきのーソースコードの3行目に

`https://${options.server}/api/notes/create`,

という表記があります。そう、ノートを作成するときには、/notes/createにPOSTする必要があるということです。そして、先頭にはoptions.serverなどと書いてありますが、これはベースURLを指します。Misskey Hubには"ベースURLはhttps://{サーバーのドメイン}/api"と書いてあります。よってノートを投稿するには、https://{サーバーのドメイン}/api/notes/createにPOSTすれば良いということです。ノートを削除するときにはnotes/deleteに、グローバルタイムラインを取得するときはnotes/global-timelineにPOSTします。さあ、盛り上がってきましたねえ!!!!!!

さて、では次に「パラメータ」を見ていきます。何やらよくわからない文字列が並んでいますが、大したことはないです。"text"というのをクリックしてみてください。"説明 ノートの本文。"と書かれています。

もうお気づきの方もいるでしょう。これは、ソースコード7行目のtext: textというのを表しています。パラメータに書かれているものを、'payload'の中に書けばいいということです!

例を上げます。visibilityパラメータを使用して、ノートの公開範囲をspecified(設定したユーザーのみ閲覧可)にするには、以下のように書きます。

function postToMisskey(text,options) {

 return UrlFetchApp.fetch(

   `https://${options.server}/api/notes/create`,

   {

     'method': 'POST',

     'headers' : {'Content-Type': 'application/json'},

     'payload': JSON.stringify({ i: options.token, text: text , visibility: "specified"})

   }

 );

}


function misskeytest() {

 postToMisskey('双葉湊音は可愛い。異論は認めない。',{server: 'voskey.icalo.net',taken: 'QAWSEDRFTGYHUJIKOLP'});

これで指定した人のみ閲覧することができるようになりました。Botのテストを行う時は、このように非公開でやりましょう!


という感じで、すごくなんとなくでざっくりでしたが、なんとなくを理解することはできたのではないかと思います。意外とこういう初心者向けの情報を書く記事少ないんだよなぁ。

プログラミングにおいて、なんとなくコピペに頼りがちですが、このように公式ドキュメントを読むとできることが一気に広がるのでぜひ読んでください。というかよく読んでください。

これで、みなさんはMisskey APIないしはAPI全般について、なんとなく理解できたと思いますので、ぜひいろんなことをやってみてください。

Step2,YouTubeAPIを完全に理解しよう

さて、Misskey APIについてはつい熱がこもってしまったので、だらだら話してしましたが、続いてYouTube APIについてです。

今回GASを使っていますが、GASとYouTube APIって非常に相性がよくて、Misskey APIみたいにめんどくさいことしなくても、もうYoutube.Search.listみたいにそれ専用の命令があるくらいです。もはやAPIじゃない

こちらも参考サイトがあるので、ソースコードをぱk参考にしていきましょう。

GASでYouTubeのデータを取得してスプレッドシートに表示する方法を解説! マツキヨのブログ

※スプレッドシートは今回関係ないので、そこの部分は気にしないでください。


さて真ん中ぐらいにある「ラーメン」の検索結果を表示するプログラムはうまく動作したでしょうか。私は特に手こずることはなかったです。

さてこちらも先ほどと同じように、公式ドキュメントにアクセスしていきましょう。

https://developers.google.com/youtube/v3/docs/search/list?hl=ja

qというパラメータの説明を見てください。"q パラメータでは、検索するクエリ語句を指定します。"ということで、たしかにそうだなとおわかりいただけると思います。こちらもいろいろ試してみてください。


で、今回やりたいことは、YouTubeに今日公開された動画を取得するということです。これは、orderパラメータとpublishedAfterパラメータを使用することで実装できます。orderパラメータは、取得した動画を並び替えることができます。今回は新しい順に並び替えるようにします。publishedAfterは、指定した日時以降に公開された動画に絞り込むというものです。今回はこの日時を当日の0時0分0秒にすることで、今日公開された動画を取得するようにします。

publishedAfterの説明を見ると"RFC 3339 形式の date-time 値"とあります。なにこれおいしいの?

ここはggって調べます。ついでに当日の0時0分0秒の時間を取得するサイトも発見しました。

RFC3339形式の日付の取り扱いのコツ

【Javascript】今日の0時0分0秒の日付計算

とりあえず0時0分0秒の時間をとるものは関数にまとめます(だいぶひでぇ関数名ですが許して)。

function today_000() {

 var _d = new Date();

 var d = new Date(_d.getFullYear(), _d.getMonth(), _d.getDate(), 0, 0, 0);

 return d;

}

そして、これをRFC3339形式に変換するということなので

Utilities.formatDate(today_000(), 'JST', `yyyy-MM-dd'T'HH:mm:ssXXX`)

とすればいけますね。

最終的に「双葉湊音」の動画を取得するということで、以下のような関数に仕上げました。

function searchByKeyword() {

 const results = YouTube.Search.list('snippet', {

   q: '双葉 湊音',

   maxResults: 50,

   type: 'video',

   order: "date",

   publishedAfter: Utilities.formatDate(today_000(), 'JST', `yyyy-MM-dd'T'HH:mm:ssXXX`)

 });

 return results.items

}

いや〜非常に簡単ですね。ggればどうにかなりますし、それでも無理ならChatGPTという手もあります。素晴らしい世の中だ!

Step3,ニコニコ動画APIを完全に理解しよう

本当ならYouTubeだけにしようかなと思っていたのですが、ニコニコ動画にも「スナップショット検索API v2」というものがあるので、これも使ってみることにします。

今回は参考サイトが見つからなかったので、自力で頑張ってみます。

まずは公式ドキュメントへ。

https://site.nicovideo.jp/search-api-docs/snapshot


まずはヘッダー、User-Agentにアプリケーション名を入れて、クエリパラメータ、とりあえずqとtargetsと_contextが必須だからここに値を入れて、ひとまず実行!!!!

あれ!!!データがない!!!!!!おかしい!!!!!dataにデータがあるはずなのに!!!!!

帰ってきたdataはなぜか { } 。 一体なぜ?


...と思ったら、fieldsという、ヒットしたコンテンツのフィールドが設定されてなかった。なんでこれが省略可能なんだよ(怒

そして、今日のデータを取得するというのはfiltersパラメータでYouTubeと同じようにいじれる。日時はISO 8601形式。なにそれおいしいの?


とそんなこんなで、完成したソースコードがこちら。

function searchByKeywordNiconico() {

 return UrlFetchApp.fetch(

   `https://api.search.nicovideo.jp/api/v2/snapshot/video/contents/search`,

   {

     'method': 'GET',

     'headers' : {'User-Agent': 'Minato-bot'},

     'payload': {

       'q': '双葉 湊音',

       'targets': 'title,description,tags',

       'fields': 'contentId,title,startTime',

       '_sort': '-startTime',

       '_limit': '50',

       '_context': 'Minato-bot',

       'filters[startTime][gte]':Utilities.formatDate(today_000(), 'JST', "yyyy-MM-dd'T'HH:mm:ssXXX")

     }

   }

 )

}

この記事では、書くのがめんどくさくなってきたのでかなり省略しましたが、実際これがかなり手こずりました。疲れた。

Step4,文にして装飾しよう

さてこれでようやっとデータを入手できたので、文字を装飾してMisskeyに流す関数を作ります。

ところで、GASの文字列において、改行するにはどうすればよいでしょうか?

答えはエスケープ文字\nを使用します。というわけで早速投稿する文章を生成する関数を制作していきます。

https://auto-worker.com/blog/?p=1111

またMisskeyに投稿する上では、文字の装飾を使うと非常に良いです。Misskey.designにMFM&文字装飾マトメというページがありますので、参考にしてみてください。

ということで完成したソースコードがコチラ

function misskeyYoutube() {

 var youtubeItems = searchByKeyword();

 var sentence = "今日投稿された「双葉 湊音」の動画(YouTube)\n";

 if (youtubeItems==""){

   sentence = sentence + "\n今日投稿された動画はありません。"

 } else {

   for(let i = 0; i < youtubeItems.length; i++){

     sentence += "\n[" + youtubeItems[i].snippet.title + "](https://www.youtube.com/watch?v=" + youtubeItems[i].id.videoId + ")";

   }

 }

 sentence += "\n\n<small>投稿された動画はYouTube APIから取得したものであり、公認されたものではありません。また、双葉湊音と直接関係のない動画が表示されている可能性もあります。このBotは双葉湊音好きが運営している趣味の範囲のBotです。双葉湊音公式ではございませんし、公認されているものでもありません。管理者:Hayapii_k</small>";

 return sentence;

}


function misskeyNiconico() {

 var niconicoItems = JSON.parse(searchByKeywordNiconico());

 niconicoItems = niconicoItems.data

 var sentence = "今日投稿された「双葉 湊音」の動画(Niconico動画)\n";

 if (niconicoItems==""){

   sentence = sentence + "\n今日投稿された動画はありません。"

 } else {

   for(let i = 0; i < niconicoItems.length; i++){

     sentence += "\n[" + niconicoItems[i].title + "](https://nico.ms/" + niconicoItems[i].contentId + ")";

   }

 }

 sentence += "\n\n<small>投稿された動画はニコニコ動画 スナップショット検索API v2から取得したものであり、公認されたものではありません。また、双葉湊音と直接関係のない動画が表示されている可能性もあります。このBotは双葉湊音好きが運営している趣味の範囲のBotです。双葉湊音公式ではございませんし、公認されているものでもありません。管理者:Hayapii_k</small>";

 return sentence;

}

YouTubeとニコニコ動画でわけているあたり、なんか改善できそうな気がしますが、とりあえずこれで勘弁してください。

Step5,GASの設定をして完成!

ということで全体コードが完成しました。以下がソースコード全体です。

コード.gs

//YouTubeに公開された動画をMisskeyに投稿

function postMisskeyYouTube() {

 postToMisskey(misskeyYoutube(), { server: 'voskey.icalo.net', token: 'yourTaken' });

 console.log(misskeyYoutube())

}


//投稿文生成

function misskeyYoutube() {

 var youtubeItems = searchByKeyword();

 var sentence = "今日投稿された「双葉 湊音」の動画(YouTube)\n";

 if (youtubeItems==""){

   sentence = sentence + "\n今日投稿された動画はありません。"

 } else {

   for(let i = 0; i < youtubeItems.length; i++){

     sentence += "\n[" + youtubeItems[i].snippet.title + "](https://www.youtube.com/watch?v=" + youtubeItems[i].id.videoId + ")";

   }

 }

 sentence += "\n\n<small>投稿された動画はYouTube APIから取得したものであり、公認されたものではありません。また、双葉湊音と直接関係のない動画が表示されている可能性もあります。このBotは双葉湊音好きが運営している趣味の範囲のBotです。双葉湊音公式ではございませんし、公認されているものでもありません。管理者:Hayapii_k</small>";

 return sentence;

}


//YouTubeからデータを取得

function searchByKeyword() {

 const results = YouTube.Search.list('snippet', {

   q: '双葉 湊音',

   maxResults: 50,

   type: 'video',

   order: "date",

   publishedAfter: Utilities.formatDate(today_000(), 'JST', `yyyy-MM-dd'T'HH:mm:ssXXX`)

 });

 return results.items

}


//Niconicoに公開された動画をMisskeyに投稿

function postMisskeyNiconico() {

 postToMisskey(misskeyNiconico(), { server: 'voskey.icalo.net', token: 'yourTaken' });

 console.log(misskeyNiconico())


}


//投稿文を生成

function misskeyNiconico() {

 var niconicoItems = JSON.parse(searchByKeywordNiconico());

 niconicoItems = niconicoItems.data

 var sentence = "今日投稿された「双葉 湊音」の動画(Niconico動画)\n";

 if (niconicoItems==""){

   sentence = sentence + "\n今日投稿された動画はありません。"

 } else {

   for(let i = 0; i < niconicoItems.length; i++){

     sentence += "\n[" + niconicoItems[i].title + "](https://nico.ms/" + niconicoItems[i].contentId + ")";

   }

 }

 sentence += "\n\n<small>投稿された動画はニコニコ動画 スナップショット検索API v2から取得したものであり、公認されたものではありません。また、双葉湊音と直接関係のない動画が表示されている可能性もあります。このBotは双葉湊音好きが運営している趣味の範囲のBotです。双葉湊音公式ではございませんし、公認されているものでもありません。管理者:Hayapii_k</small>";

 return sentence;

}


//Niconicoからデータを取得

function searchByKeywordNiconico() {

 return UrlFetchApp.fetch(

   `https://api.search.nicovideo.jp/api/v2/snapshot/video/contents/search`,

   {

     'method': 'GET',

     'headers' : {'User-Agent': 'Minato-bot'},

     'payload': {

       'q': '双葉 湊音',

       'targets': 'title,description,tags',

       'fields': 'contentId,title,startTime',

       '_sort': '-startTime',

       '_limit': '50',

       '_context': 'Minato-bot',

       'filters[startTime][gte]':Utilities.formatDate(today_000(), 'JST', "yyyy-MM-dd'T'HH:mm:ssXXX")

     }

   }

 )

}


//文章からMisskeyに投稿する

function postToMisskey(text,options) {

 return UrlFetchApp.fetch(

   `https://${options.server}/api/notes/create`,

   {

     'method': 'POST',

     'headers' : {'Content-Type': 'application/json'},

     'payload': JSON.stringify({ i: options.token, text: text , visibility: "specified"})

   }

 );

}


//今日の0時0分0秒を求める

function today_000() {

 var _d = new Date();

 var d = new Date(_d.getFullYear(), _d.getMonth(), _d.getDate(), 0, 0, 0);

 return d;

}



最後に、関数postMisskeyYoutube()とpostMisskeyNiconico()を定期的に実行させるようにします。

左側のメニューから「トリガー」を選んで「トリガーを追加」

関数を選んで、「日付ベースのタイマー」、任意の時刻を選択して保存。これで、毎日定期的に実行される.....はずです(まだ1回もテストしてない)

「みなとぼっと」の今後の展望

...よし、なんとか完成しました。このBotを「みなとぼっと」と名付けます。

今はYouTubeとニコニコ動画の内容を宣伝するだけですが、今後は双葉湊音に関するイベントまで何日かを知らせたり、双葉湊音に関する情報を定期的に発信したりなど、より機能を充実させていきたいと考えています。なにかいい案があれば私に教えてください。

最後に

ということでいかがだったでしょうか。なんかめちゃくちゃ情報量あったような気がしますが、実際難しいことはしていません。

APIを用いることで、このような新しいサービスを作ることができます。APIを本格的に使ったサービスを作るのはこれが初めてでしたが、非常に楽しかったです。

今後もAPIを用いた様々なサービスを作っていけたらいいなと思います。

本記事によって、Misskeyやぼすきー、APIやプログラミングについて少しでも興味を持ってくれたら非常に嬉しいです。

最後に、ぼすきーAdvent Calender 2023の3枠目を作ってくださったいぬいぬさん(@InuInuGames)に感謝いたします。ありがとうございます!!!!!!