テキストブロックの中身を定期的に書き換える ~ イチ(以下)から学ぶNotionAPI×Google Apps Script【Day.7】
こんばんは、ねここです。
昨日、Notion大学のオフラインイベントに参戦しまして、めちゃ刺激を喰らってモチベーションMaximumです。
ということで、もらった刺激をここで還元していきたいと思います。
前回は実践編1として「繰り返しアイテムの日付プロパティに未来の日時を設定する」というのをやりました。
今回は「テキストブロックの中身を定期的に更新する」というのをやっていきたいと思います。
要件、仕様を考える
昨日のイベントでいろんな方の普段使っているページを見せていただいたんですが、自分の好きな言葉の引用をページに貼って、常に刺激を受けられるようにしている人が何名かいたんですよね。
であれば、表示したい引用が複数あるなら手動で書き換えるより、自動で書き変わる仕組みがあったらうれしい、って人が潜在的にめちゃいるんじゃないかなぁ、なんて考えました。
他にも「ToDoリストから今日のタスクを抜き出してテキストブロックに"今日の予定"を表示する」みたいな使い方とかもできるのかなと思ったりします。
定期的に更新する部分は前回やったトリガーを使う方法で実現できますので、そのまま流用しようと思います。使えるものは使っていけ。
書き換える候補を用意する
候補の中からランダムでひとつ選択する
選ばれた引用をテキストブロックに出力する
流れとしたらこのような感じ。前回より簡単な予感もします(フラグ)。
テキストブロックにGASから書き込む
テキストブロックはNotion本体の機能だけでは、間接的に書き換えることはできません。できるんだったら最初から困っていないw
ですがNotionAPIを介せば、テキストブロックの中身を参照したり、書き換えたりすることができ……るはずです。
文字を表示する部分を構成するRichTextのリファレンスを見ると、大きく3タイプあることがわかります。
Text (いわゆる普通のテキスト)
Equation (数式。式ブロックやインライン式を使って記述するやつ)
Mention (ページリンクだったり他のオブジェクトと紐づけされるもの)
このうちMentionは今回は考えないことにします。今回やりたいことには関係が薄そうですし、これ理解するにはちょっと奥が深そうなので学びは別の機会に。
まずは改めてテキスト周りのデータの持ち方を眺めてみます。
最初の部分はなんの装飾もないプレーンなテキスト。
その次はインライン式。
その後ろは、いろんな装飾を積んだテキストです。
このテキストブロックのRich Text部分を拾ってみます。
const ENDPOINT = "https://api.notion.com/v1/";
const NOTION_TOKEN = PropertiesService.getScriptProperties().getProperty("NotionToken");
const TextblockID = PropertiesService.getScriptProperties().getProperty("TextblockID");
const headers = {
"Content-type": "application/json",
"Authorization": "Bearer " + NOTION_TOKEN,
"Notion-Version": "2022-06-28",
};
function myFunction() {
res = getBlockquery(TextblockID);
res_parse = JSON.parse(res);
}
function getBlockquery(blockid) {
url = ENDPOINT + "blocks/" + blockid;
// インテグレーションさんにわたす申請書を作る
const OPTIONS = {
"method" : "GET",
"headers": headers
};
// インテグレーションさん、申請書を提出いたします。よろしくお願いします。
return UrlFetchApp.fetch(url, OPTIONS);
}
これでres_parseの中身を確認します。
デバッガを使って中身を見る
console.logを使って出力してもいいんですがめんどくさいので、ブレイクを付けてparseした直後でプログラムの実行を一時停止します。
前回まではしれっと使ってましたがw
一時停止するとウィンドウ右側にデバッガ画面が表示されます。
ブロックIDの確認方法はいくつかあると思いますが、クリリンのおでこにありそうな6点ボタンから「ブロックへのリンク」を取得して、#以降の部分を参照するのが一番早いと思います。ちなみに6点ブロックはハンドルブロックと呼ぶらしい。
Global の中にスクリプトで動いてる情報が書く情報がほとんど格納されていくので、中身を確認していきます。
インテグレーションさんから受け取ったブロックの情報はparseして res_parse 変数に格納したので、デバッガでその中身を覗いていきます。
テキストブロックのテキスト情報は paragraph の中に詰まっています。
以前にもちらっと触れましたが、テキスト部分は書式が変わるたびに分割されて、それぞれ配列に入ります。
配列の先頭(0番)にはプレーンなテキスト、1番にはインライン式(typeがequationになってますね)、2番にはいろんな装飾をしたテキストが格納されていることがわかります。
という感じで、ぼくはわりと細かくデバッガを使って中身を覗きながらコードを組んでいきます。ほかの人はどうなんですかね。
実際にテキストブロックに書き込んでみる
では、このparagraphに実際にデータを書き込んでいきたいと思います。
function myFunction() {
update = {
"paragraph": {
"rich_text": [
{
"type": "text",
"text": {
"content": "これはGASから書き換えられたテキスト......!"
}
}
]
}
};
patchBlock(TextblockID, update);
}
function patchBlock(blockid, content_data) {
url = ENDPOINT + "blocks/" + blockid;
// インテグレーションさんにわたす申請書を作る
const OPTIONS = {
"method" : "PATCH",
"headers": headers,
"payload": JSON.stringify(content_data)
};
// インテグレーションさん、ブロック更新申請書を提出いたします。よろしくお願いします。
return UrlFetchApp.fetch(url, OPTIONS);
}
無事に書き換えることができましたね。
元の中身を残しておくのを忘れていたのでw、せっかくだしGAS側からざっくり復元したいと思います。
function myFunction() {
update = {
"paragraph": {
"rich_text": [
{
"type": "text",
"text": {
"content": "こ こ は T e x t "
}
},
{
"type": "equation",
"equation": {
"expression": "\\Large\\color{#ffffff}\\colorbox{#000000}{~G~A~Sからインライン式~}"
}
},
{
"type": "text",
"text": {
"content": "ここはいろんな装飾を付けたテキスト"
},
"annotations": {
"bold": true,
"italic": true,
"strikethrough": true,
"underline": false,
"color": "red"
}
}
]
}
};
patchBlock(TextblockID, update);
}
インライン式の中で使われるバックスラッシュはひとつだけ送るとよく制御コードとして扱われて表示に反映されないことが多いので「これは制御コードじゃなくてバックスラッシュを表示してください」という意図を伝えるためにバックスラッシュをふたつ並べて送信します(プログラミングあるある)。
実行すると、こんな感じでぬるっと書き変わります。
これを1時間毎とか定時トリガーで走らせれば、定期的にテキストブロックの文言を変えることができますね。
複数の候補から表示させるものを選ぶ
では、表示部分はなんとかなったので「次は何を表示させるか」という部分を作っていきます。
まずは一番簡単な実装のものからやっていきます。
コードに直接候補を書いて乱数で選ぶ
function myFunction() {
words = [
"「それによ…… 落ちこぼれだって必死で努力すりゃエリートを超えることがあるかもよ」",
"「前向きのバカならまだ可能性はあるが 後ろ向きのカはバカは可能性すらゼロ……」",
"「強い言葉を遣うなよ 弱く見えるぞ」",
"「他人にやらされてた練習を努力とは言わねえだろ」",
"「いちばんいけないのは じぶんなんかだめだと思いこむことだよ」"
];
rnd = Math.floor(Math.random() * words.length);
update = {
"paragraph": {
"rich_text": [
{
"type": "text",
"text": {
"content": words[rnd]
}
}
]
}
};
patchBlock(TextblockID, update);
}
words に表示する候補になるものを格納し、乱数を使ってどれを使うかを選択します。
ちなみに、名言をひねり出す頭はないのでw、こちらのサイトから拾わせていただきました。
Math.random() は0以上1未満の乱数を生成します。
つまり 0~0.99999… の中から選ばれます(1は含まれません)。
その生成した乱数に、候補の数(words.length)を掛けて小数点以下を切り捨てると、なんとどの候補を選ぶのかを決めることができます。
今回の例だと候補は5つなので、乱数 x words.length の結果は0~4.99999… となります。そして端数を切り捨てると0~4を取ることができます。5つの配列はwords[0]~words[4]の範囲なので、これで候補からひとつ選ぶことができるわけです。
この方法、ほんとにイニシエからありますね。最初にこれ覚えたのン十年前。
データベースに格納した候補から選ぶ
では、候補の名言をデータベース上で増やしたりできるようにしていきます。
名言を格納するデータベースを作成して、それを前回までやってたのと同じ手法でGASで読み込みます。
読み込んだものをwords配列に格納すれば、あとは先ほどと同じ手順でひとつ選び、テキストブロックに表示できます。
const ENDPOINT = "https://api.notion.com/v1/";
const NOTION_TOKEN = PropertiesService.getScriptProperties().getProperty("NotionToken");
const DatabaseID = PropertiesService.getScriptProperties().getProperty("DatabaseID");
const TextblockID = PropertiesService.getScriptProperties().getProperty("TextblockID");
function myFunction() {
filter = {};
res = getDBquery(DatabaseID, filter);
res_parse = JSON.parse(res);
words = new Array();
for(i=0; i<res_parse["results"].length; i++) {
words[i] = res_parse["results"][i]["properties"]["名前"]['title'];
}
rnd = Math.floor(Math.random() * words.length);
update = {
"paragraph": {
"rich_text": [
{
"type": "text",
"text": {
"content": words[rnd][0]["plain_text"]
}
}
]
}
};
patchBlock(TextblockID, update);
}
無事にデータベースに登録したものをテキストブロックに渡すことができました。
あとはひたすら名言データを増やすのもよし、タグを使ってページごとに内容を変えるようにしたり、表示する時間帯をプロパティで設定して制御したり、いろんな応用が考えられますね。
ちなみに余談ですが、Notionの機能だけでなんとか乱数を作ろうとしている記事も最近書いてます。
イチミリも役には立たないと思いますが、ぼくの根っこの部分は垣間見えると思うので、よければどうぞw
式ブロックに表示させてみる
ここからはおまけですが、せっかくテキスト表示周りの仕様を掘ったので、"equation"タイプにも突っ込んでみることにしましょう。
表示用の式ブロックを用意します。
const EquationID = PropertiesService.getScriptProperties().getProperty("EquationID");
function myFunction() {
filter = {};
res = getDBquery(DatabaseID, filter);
res_parse = JSON.parse(res);
words = new Array();
for(i=0; i<res_parse["results"].length; i++) {
words[i] = res_parse["results"][i]["properties"]["名前"]['title'];
}
rnd = Math.floor(Math.random() * words.length);
word = "\\Huge " + words[rnd][0]["plain_text"];
update = {
"type": "equation",
"equation": {
"expression": word
}
};
patchBlock(EquationID, update);
}
式ブロックの仕様に合わせて更新部分を調整して、インテグレーションさんに依頼します。
無事表示……なのですが、実はもう一息です。
実は式ブロック(インライン式)の中ではコマンドを入れられる都合上、半角スペースは表示時には無視される仕様になっていて、代わりに ~ を入れると半角スペースとして扱われます。
ですので「弱く」の前にスペースを入れられるように、正規表現を使って一工夫します。
word = "\\Huge " + words[rnd][0]["plain_text"].replace(/\s/,"~");
replace(/\s/,"~") を追加しました。
このreplaceは、(/この条件に当てはまる部分を/, "この文字列に置き換える") というものです。詳しいことは端折りますが正規表現を使えるようになるとテキスト捌きが格段に便利になります。
\s は半角スペースを指します。ですので、(/半角スペースを/, "~に置き換える")ということになります。
無事に式ブロックにも名言を表示させることができましたね。やったぜ。
これも今の実装だと長い名言を入れると表示幅を超えてしまったりしますので、例えば文字数を数えて何文字以上だったら文字サイズを小さくするとかそういう工夫はできるかなと思います。
というところで、今日はここまで。
Day.3で書いた「NotionAPIを使ってやりたいこと」はとりあえず作ることができました。
次になに作るかはまだ決めてないですが、いいアイデアが出たら作ってみようと思います。
また、せっかくなのでこのDay.7まででいろいろ培ったナレッジを項目ごとに切り出した記事を作ったりするのもありかなと思ってます。
せっかく集中してアウトプットすることができたので、もう少しこの勢いでいろいろ書きたいと思ってます。
いや、思ってるだけじゃダメだな。
書きます(なぜ言ってしまった)。
今日のカバー画像
動画編集してる子って指定してみたら、本当に動画編集のようなことをしてて「やるやん」ってなりました。
NEXT
しばし待て。