0
Help us understand the problem. What are the problem?

posted at

updated at

Claris Connectで小説家になろう更新通知Discord botを作る

前置き

${\Huge\text iPadください!!!!!!!!!!}$

前置き2

欲望に包まれてこんにちは。この記事は見るからにQiita Engineer Festa 2022のうち1テーマ、Claris Connect を使った SaaS 連携ユースケースを紹介しよう!の賞品であるiPad Pro 256GBに釣られて書かれたものですし、実際そうです。iPadください。
今回何で記事を書くかは色々悩んだんですが、Claris Connectの公式ページによれば

「App が多すぎて、カオス!」な状態はもうおしまい。

だそうなので、逆に言えばなるべくカオスなAppの開発に使うのが効果的かな、という考えに至りました。
そういうわけで今回は、「何もしてないのに機能が増えた」をはじめとする恐怖現象の温床……Discord botの開発に、このツールを使用していこうと思います。

方向性を考えておく

とりあえず、作っていくDiscord botの方向性を決めておきましょう。とはいえClaris Connectはそれ自体がGUIなので、ちょうどフローチャートのように「組みながらどうするか決めていく」ことも可能だと思います。なので今回は、簡単に3つくらいの指針を決めておきます。

  1. 渡されたncode1を持つ作品について定期的に小説家になろう巡回して
  2. 更新があったら関連データを取得して
  3. DiscordにWebhook経由で投稿する

実装

1. APIを叩く部分

1-1. 定期的に実行する

まず、巡回のための定期実行にあたる部分を作ります。
image.png
この画像における左下のSchedulesを追加して進むと、遷移先の画面で発火頻度を聞かれます。今回はとりあえず2分に1度の頻度で実行するので、Custom (advanced)を選択します。
そこから進むとCron Expressionで頻度を入力するよう促されるので、「毎時偶数分目」を意味する

0/2 * * * *

を突っ込みます。
タイムゾーンには普通にAsia/Tokyoを入力して、あとはSave Triggerでトリガーの追加が完了します。

1-2. ストレージを読む

1-2-1. 前置き

とりあえず実行上のデータを保持するためにストレージを作ります2
連携アプリケーションリストで「Database」「Cloud Storage」あたりのカテゴリを見るとAWS S3だのBoxだのFileMaker Cloudだの色々なサービスがヒットします。Claris Connectを使ってることを考えるとFileMaker Cloudが王道かなと思うんですが、今回はそこまで大規模なデータを扱うわけでもないので、Google Sheetsで運用しようと思います。

1-2-2. ファイルを作っておく

一度Google Driveを開き、NekochanRealSaikyouなどの適当な名前でGoogle Sheetsファイルを作成します。コンテキストメニューから共有用URLを取得すると、

`https://drive.google.com/file/d/${ID}/view?usp=sharing`

みたいな文字列がクリップボードに来るので、このIDにあたる文字列を適当な手段で保管しておきます。
NekochanRealSaikyouには、とりあえず

  • configシート:実行に関わる設定を保管するシート
  • novelsシート:監視対象の小説の情報を保管するシート

の二つを作成しておきます。
novelsシートについては、何も書いておかないのもわかりにくいので、小説家になろうのトップページにある月間ランキングの上位5作品(執筆当時)のncodeを入力しておきました。
image.png

1-2-3. 取得の準備

これは罠なんですが、Claris ConnectのGoogle Sheetsモジュールには、ファイルを編集するコマンドはあっても取得するコマンドはありません。Google Driveモジュールも同様です3
ではどうやってSheetsのデータを持ってくるかというと、Google Cloud Platform(GCP)のAPIを叩くことになります。
詳しくはこの記事に譲るとして、今回やるべきは

  1. GCPで適当なプロジェクトを作る
  2. プロジェクト内でGoogle Sheets APIを有効化する
  3. APIキーを作成する
  4. NekochanRealSaikyouリンクを知っている全員を対象として共有する

という工程です。
この状態で、例えば

`https://sheets.googleapis.com/v4/spreadsheets/${ID}/values/novels?key=${KEY}`

にアクセスすれば、novelsシートの内容がjson形式で返ってきます。

1-2-4. 取得

そういうわけで、実際に取得してみましょう。
これも罠なんですが、Claris ConnectのWebhookモジュールは実はWebhookではありません。なぜそう言えるかというと、クリックした瞬間に
image.png
これが出てくるからです。
いや、HTTP GET requestではないだろ!4という感が相当強いですが……まあいいでしょう。ここからGET requestを選択した状態でContinueして、前述のAPIアクセス用URLを入力する作業をconfignovelsで1回ずつ行います。
返ってきたデータをConvert to JSON formatする必要があるかと思ったんですが、どうもデフォルトでjson形式として認識されているようなので割愛します。

1-3. APIを叩く

1-3-1. 理屈

小説家になろうの更新を感知する方法はいろいろあります。具体的に言うとスクレイピング・Atomフィード・なろう小説APIなどですが、今回はそのうちAPIを使用します。
まず、今回取得すべきデータについて考えると

  • 事前入力されたncodeを持つ作品が対象(=検索は必要ない)
  • 念のため出力上限はできるだけ大きく
  • 作品そのものに付随するデータとして必要なのは、
    • 最終更新日時(更新されたことを確認するため)
    • 話数
    • 文字数(前回更新からの差分を表示するため)
    • ncode
    • タイトル
    • 作者名

といったことが言えます。
公式リファレンスをもとにこれを実装することを考えると、
https://api.syosetu.com/novelapi/api/のクエリを

  • out:json
  • ncode:対象となるncodeを-で連結した文字列
  • of:gl-ga-l-n-t
  • lim:500

に設定してGETすればいいでしょう。

1-3-2. 実装

まず、前述の対象となるncodeを-で連結する工程をやります。
先ほどの仮入力例でいくと、Google Sheets APIのnovelsはこういうjsonを返してきます。

{
  "range": "novels!A1:Z1000",
  "majorDimension": "ROWS",
  "values": [
    [
      "n8703hn"
    ],
    [
      "n5082hp"
    ],
    [
      "n6634hp"
    ],
    [
      "n4481hp"
    ],
    [
      "n8088hp"
    ]
  ]
}

基本はvalues内に入っている二次元リストから、ncode列(0列)に入っている文字列を抽出し、-で連結すればいいでしょう。
Claris Connectにも多分通常のプログラミング言語で言うところのmap文がありそうな気がするんですが、今回は見つけられなかったので気合いでループ実装します。
ここからはダイジェストでお送りします。

結合用変数を作る

image.png
Claris ConnectのVariableを追加、名前はlinked_ncodesにしておきます。
この時どうやら空文字列が使えないっぽいので、先ほどAPIから取得したnovelsシートのうち最初の行のncodeを突っ込んでおきます。

ループを設定する

image.png
Claris Connectでは追加メニューからRepeatを選択することでループを設定することが可能です。
novelsシートのすべての行に対してforEachをかけるイメージですね。

index==0を弾く

image.png
linked_ncodesの宣言時に既に最初の行のncodeは追加済みなので、index==0の場合も処理をすると同じncodeが二重登録されてしまいます。それを防ぐため、Claris Connectのif-Thenで条件を付けています。
やっぱりこういうのはあんまりスマートじゃない感じがあるので、もうちょっといい方法がある気がしますね……

linked_ncodesに追記する

image.png
先ほどのif-Thenの中に追記処理を入れます。
外見的には単純明快、linked_ncodesの中身と-とRepeatから渡された行内容のncode列を結合した結果を、linked_ncodesそのものに代入しています。
Claris Connectのこういうところは良いですね、文字列結合が外見的にわかりやすく行えます。

APIを叩く

image.png
一度ifからもループからも抜けまして、HTTP GET requestを追加します。
基本的には『理屈』のセクションに書いてある内容をそのまま実装している感じですね。
こういうめんどくさくなりがちなクエリ処理をGUIでできるのは結構楽しいと思います。

最終的な全体像はこうなります。
image.png

1-3-3. 確認

ここまでで理論上は小説家になろうAPIを叩く処理が完成したはずなので、本当にできているか確認してみましょう。
Flowsメニューを開いて今まで編集していたフローの横のラジオボタンをオン、フローに飛んで右上のRun Nowを押します。
image.png
するとHistoryに飛ばされますので、実行を待った後履歴の個別リンクを押します。8番のHTTP GET Request命令のレスポンスを確認すると……
image.png
うまいことAPIが叩けているようです!良さそうですね。

1-4. 更新確認

1-4-1. データ構造

とりあえずGoogle Sheetsに保管するデータについて、もう少し厳密に構造を考えます。
まあそうはいっても、基本的に更新確認は比較によって行われるので、APIを叩いた結果出てきた諸々をそのまま横に並べればいいでしょう。
この形状を一行として、作品数だけ下に伸ばしていく感じで行きましょう。

["ncode","title","general_lastup","general_all_no","length"]

1-4-2. 実装

問題

基本的に先ほどAPIを叩いて出てきたデータをRepeatで回すだけ……ではありません
あのですね……これは良くないところだと思うんですけど、Claris Connectは持ってきたjsonデータの加工にあんまり向いていないように見えます。FileMakerとの併用が前提だからだとは思うんですが、しかしもう少し何とかしてほしいところがあります。
何が問題かというと、返ってくるjson内の最初の要素です。

[
  {"allcount":887041},
  {
    "title":...,
    "ncode":...
  },
...
]

このallcountを持っているオブジェクトを、Claris Connectは配列における典型と認識します。つまり、この状態でRepeatを設定しようとすると……
image.png
ご覧の通り、itemのキーには titlencodeもありません
典型というのはこういうことです。Repeatは回される要素の持つキーについて、『最初の要素』を参照して推測する……そのため、最初の一件だけallcount以外のキーを持たない今回の場合相性が悪いのです。
これをどうにか解決しましょう。

Remove item from listについて

この課題の解決にあたり最初に思いつくのは、ListsモジュールのRemove item from listコマンドを使うことです。
このコマンドは「任意のリストの任意番目のアイテムを削除してくれる」という非常にゴキゲンな存在です。これを使って取得したデータのうち0番目のみを削除すれば、特殊なキー内容を持つ要素が消えて良さそうです。実際にやってみましょう!
image.png
使えません
はい。
細かい理屈はわからないんですが、たぶんリスト内に存在する連想配列をこのコマンドでは解釈できないため、文字列化して扱ってしまった……みたいな感じだと思います。さすがにこれはもうちょっと何とかならなかったのか、という思いは割とありますね……。

ではどうするのか

こうします。
image.png
ご覧の通り、Documentsモジュールのコマンド二つを使って、jsonを一度csvに変換し、そのcsvをさらにリストに変換する処理をしています。

9 Convert JSON to CSVのオプションは、

  • JSON : step8のdata
  • delimiter : ,
  • Column title : false
  • Flatten : false

10 Convert text to listのオプションは、

  • JSON : step9のcsv
  • Separator : \n

をそれぞれ指定しています。
この処理を挟むことで、最終的に

これが
[
    {
        "allcount": 5
    },{
        "title": "真の聖女らしい義妹をいじめたという罪で婚約破棄されて辺境の地に追放された騎士好き聖女は、憧れだった騎士団の寮で働けて今日も幸せ。【書籍化・コミカライズ決定】",
        "ncode": "N6634HP",
        "general_lastup": "2022-06-08 17:04:30",
        "general_all_no": 48,
        "length": 100518
    },{
        "title": "婚約者が浮気相手と駆け落ちしました。色々とありましたが幸せなので、今さら戻りたいと言われても困ります。【外伝更新中】",
        "ncode": "N5082HP",
        "general_lastup": "2022-06-07 20:00:00",
        "general_all_no": 45,
        "length": 125187
    },{
        "title": "天才魔術師を弟に持つと人生はこうなる",
        "ncode": "N8703HN",
        "general_lastup": "2022-06-06 11:00:00",
        "general_all_no": 57,
        "length": 136290
    },{
        "title": "「お前が代わりに死ね」と言われた私。妹の身代わりに冷酷な辺境伯のもとへ嫁ぎ、幸せを手に入れる",
        "ncode": "N4481HP",
        "general_lastup": "2022-05-08 13:51:59",
        "general_all_no": 16,
        "length": 61510
    },{
        "title": "前世魔術師団長だった私、「貴女を愛することはない」と言った夫が、かつての部下",
        "ncode": "N8088HP",
        "general_lastup": "2022-05-06 21:25:01",
        "general_all_no": 1,
        "length": 6319
    }
]
こうなります。
[
    "5,,,,,",
    ",'真の聖女らしい義妹をいじめたという罪で婚約破棄されて辺境の地に追放された騎士好き聖女は、憧れだった騎士団の寮で働けて今日も幸せ。【書籍化・コミカライズ決定】','N6634HP','2022-06-08 17:04:30',48,100518",
    ",'婚約者が浮気相手と駆け落ちしました。色々とありましたが幸せなので、今さら戻りたいと言われても困ります。【外伝更新中】','N5082HP','2022-06-07 20:00:00',45,125187",
    ",'天才魔術師を弟に持つと人生はこうなる','N8703HN','2022-06-06 11:00:00',57,136290",
    ",'「お前が代わりに死ね」と言われた私。妹の身代わりに冷酷な辺境伯のもとへ嫁ぎ、幸せを手に入れる','N4481HP','2022-05-08 13:51:59',16,61510",
    ",'前世魔術師団長だった私、「貴女を愛することはない」と言った夫が、かつての部下','N8088HP','2022-05-06 21:25:01',1,6319"
]

これをRepeatでループして、適宜リスト化していく感じで行きましょう。

ループ基盤

さて、回していきましょう。
まず先ほどncodeを結合した時と同じようにRepeatを追加、index==0Ifで弾きます。
image.png

表の整形

先ほど、Google Sheetsのnovelsが持つデータはこういう内容だと書きました。

["ncode","title","general_lastup","general_all_no","length"]

ですが、現時点で存在するデータはこういう並びです。

["空要素","title","ncode","general_lastup","general_all_no","length"]

下を上に整形するために手っ取り早い工程は、

  1. 0番目の要素(空要素)を2番目の要素(ncode)で置換する
  2. 2番目の要素を削除する

という流れでしょう。
それをやっているのがこの部分です。
image.png
13番でcsv状態のテキストをリストに変換し、14番と15番で前述の工程をやっています。
ここまでの処理を走らせてみたところ、Historyのログでは
image.png
こういうオブジェクトが帰ってきているようなので、たぶん大丈夫だと思います。

対応するncodeのローをテーブルから探す

忘れがちなんですが、Google Sheets APIから返ってきたのはあくまでも二次元配列にすぎません。つまり、連想配列のようにキーを渡せばバリューが帰ってくるわけではなく……特定のncodeを持つ行を取得するためにはループを回すしかない、ということになります5
ということでstep3で取得したデータのうちvaluesRepeatし、IfEqual to Case-Insensitive(大文字と小文字を区別しないで比較)を使います。
これにより、step11のループとstep16のループで回されているncode一致した場合のみの処理を行うようにできます。
image.png

更新されているかどうかを確認する

ついにこのフェーズですよ!
image.png
まず、flagという名前でvariableを作っておきます。判定処理を経て、これが-1なら更新されていない、1なら更新されている……という寸法でいきます。

更新確認は、

  • 最終更新時刻general_lastupが前回の取得から変化している
  • そもそもgeneral_lastupが設定されていない

のうちどちらかの条件を満たすことで行われます。
まず、後者から実装していきましょう。

image.png
いちいちスクショを貼るのも面倒なので、3つの処理をまとめて解説します。ここでは、

  • 19: novelsシートのうち処理中の行の長さを取得
  • 20: 取得した長さが3未満、つまり general_lastupの列を含まないなら、条件分岐
  • 21: フラグを1に設定

という処理をしています。

次に前者です。
image.png
ここでは、

  • 22: novelsシートのうち処理中の行のgeneral_lastup列に位置する要素を取得
  • 23: 取得した要素がAPIから得られる内容と違うなら、条件分岐
  • 21: フラグを1に設定

という処理です。

1-5. 更新時処理

1-5-1. 実装

シートを書き換える処理

いよいよ、更新された場合の処理です!
まずflagが立てられているかで条件分岐します。
image.png
分岐内でA,B,C,D,Eを内容としたリストを作ります。
image.png
これはClaris ConnectのGoogle Sheetsモジュール用のリストです。作っている途中に気づいたんですが、どうもUpdate spreadsheet row行番号を変数で指定できないという謎仕様があるみたいなんです!
Update cellでは普通に変数指定が可能なのでこちらを使うことになりますが、それにあたりA1とかC5みたいな感じの指定方法をとる必要があって、それに使うためのアルファベットリストなわけですね。
ここからはダイジェストです。
image.png

  • 27: API側の処理中の行が持つncode,title,general_lastup,general_all_no,lengthの5項目をRepeat
  • 28: 先ほどのA,B,C,D,Eのリストから、step27で処理中の項目に対応するものを抜き出す
  • 29: Google Sheetsの行数は1から始まるので、行数カウンタに1を足しておく
  • 30: step28とstep29を組み合わせたセルに書き込む

これで、いよいよDiscordに投稿する部分だけです!

1-5-2. 残念なお知らせ

image.png
APIのリクエスト制限に達しました
はい。
………………まあ、テストランができなくなっただけだと考えましょう。

1-5-3. Discord側のWebhook設定

まあ、気を取り直して……取り直せるかこれ?……取り直して、やっていきましょう。
Discordの適当な自分に管理権限があるサーバーで、適当にチャンネルを作ります。
image.png
いかにも精神状態が反映されたチャンネル名ですね。

ここからチャンネル設定を開いて、『連携サービス』を押し……
image.png
『ウェブフックを作成』を押すとWebhookが作れます。
image.png
『ウェブフックURLをコピー』を押し、出てきたやつをNekochanRealSaikyouconfigシートの1行目にでも置いておきます6

1-5-4. 投稿用の準備

いろいろ準備をします。

文字数差分

「何文字増えました」みたいなのを知らせる機能が欲しいので、増えた文字数を取得します。
image.png

  • 31: length列を取得
    • どうも前述の「典型」現象が発生した状態でAPIが使えなくなったせいで、アイテムからインデックス指定で取り出すにはいちいちこれをやる必要があるっぽいです。なんてこった!
  • 32: APIで取得したlengthと比較する
日付のフォーマット

最終更新日時をフォーマットします。
image.png
Add dateとはいうものの、内部では1秒も足していません。Format dateがなぜか失敗することに対する苦肉の策です。

添えるjson

image.png

{"content":"小説が更新されました!","embeds":[{"color":2340239,"timestamp":"{{kmmX.add_date.date}}","title":"{{OuoH.list.list.[1]}}","url":"https://ncode.syosetu.com/{{OuoH.list.list.[0]}}/{{OuoH.list.list.[3]}}/","fields":[{"name":"話数","value":"第{{OuoH.list.list.[3]}}話","inline":true},{"name":"文字数","value":"{{OuoH.list.list.[4]}}文字(+{{VSIh.subtract.subtract}})","inline":true}],"author":{"name":"{{OuoH.list.list.[1]}}","url":"https://ncode.syosetu.com/{{OuoH.list.list.[0]}}/"},"image":{"url":"https://sbo.syosetu.com/{{OuoH.list.list.[0]}}/twitter.png"}}]}

詳細は省く……というか説明しようがありません。この二重波括弧はClaris Connectの変数なんかを使用した場合に出てくるやつのようで、これらを踏まえた解説は極めて難しいです。多分出力された結果を見たほうが分かりやすいと思うし、説明はしません。
Discordのembedsを活用しているため結構長いです。

Webhookにリクエストを投げる

ついにこの時です。
APIが制限されたとはいえ、どうもTest Actionについては普段通り使えるようです。なのでそれを踏まえ、生成したjsonをconfigから読んだURLにPOSTリクエストで投げてみます。
image.png
これをテストとして実行すると……
image.png
(知らない小説ですが)更新通知が届きました!!!!!!!
本当にありがとうございました。

今回のフローの最終的な全体像

image.png

感想

記事自体の感想

  • API利用は計画的に!
  • 本来はもうちょっとカオスにしたかったんですが、API枯渇が先でしたね……

Claris Connectの感想

  • ところどころ罠が目立つが、それを熟知したうえで使えば楽しいツールかもしれない
    • 罠の例:Webhookモジュール内にGETリクエストが置かれている
  • 変数の中身が空だと宣言できないのってどうなんだろう
  • データのフォーマッティングが雑にできるのは非常によかった

感想2

${\Huge\text iPadください!!!!!!!!!!}$

参考

脚注

  1. 小説家になろうの作品におけるユニークIDみたいなもの

  2. 本来であればこの『ストレージを読む』セクションはもっと前に配置したかったんですが、Claris Connectではトリガーが必須となるため定期実行の後に置かざるを得ませんでした

  3. Download Google Doc, Slide, or Sheetという名前のコマンドはあるんですが、URLしか返ってきてないように見える

  4. 一般的にWebhookはPOSTリクエストを利用するものなので

  5. 長期的設計ではマネしちゃだめだぞ!Claris Connectの連想配列周りが面倒というのはわかるけどね!

  6. 今回使っているサーバーはWebhookを盗まれたところで特に実害がないような場所だからやっていますが、セキュリティ的には相当カスです。場合によりもっと堅牢にするのがいいと思います。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
0
Help us understand the problem. What are the problem?