はじめに
2025年2月22日に開催する予定の発表ネタの備忘メモ#2です。(2025年2月15日に執筆しています)
備忘メモ#1はこちら。
作るもの
アドベントカレンダー2024の完走賞でいただいた"きーたん"(Qiitan)をエゴサ(QiitaのLGTM、ストック、コメント)で動かします。
魔改造するもの(FukuFukuNyankoまねっこアニマル)
ねこIoTLT vol.5で魔改造したものをさらに改造して、Obnizを接続します。
裁縫 (きーたんの中に仕込む)
裁縫の技術が必要で、背中からアプローチします。
中の詰め物(綿)を取り出します。
きーたんの中に駆動装置を仕込みます。
構成
GASからQiita API v2経由でQiitaの記事をエゴサ(LGTM、ストック、コメントを確認)して、数が増えていたら、Obniz経由で"きーたん"(Qiitan)を動かします。
Qiita API v2
アクセストークンを作成します。
アカウントの設定 → アプリケーション → 個人用アクセストークン → 新しくトークンを発行する
APIには利用制限があります。
- リクエスト上限は1,000回/時間
- 1回の取得数上限は100記事
Obniz ハードウェア REST API
IO Animationの機能を使用します。
(1回のリクエストでON→OFFを実現するため。IOの機能だとONとOFFで2回リクエストを送信する必要があります。1回目の送信が成功したが、2回目の送信が失敗した場合、ONの状態が続いてしまうため。)
IO0の出力を2秒間ONにしてOFFにするJSONフォーマットです。
repeatで回数を指定しないとONとOFFをずっと繰り返します。
[
{
"io": {
"animation": {
"name": "LED",
"repeat": 2,
"status": "loop",
"states": [
{
"duration": 2000,
"state": {
"io0": true
}
},
{
"duration": 500,
"state": {
"io0": false
}
}
]
}
}
}
]
curlコマンドで試す場合 (JSONファイルをcommand.jsonとして保存し、[OBNIZ-ID]に自分のIDを設定します)
curl -d @command.json https://obniz.com/obniz/[OBNIZ-ID]/api/1 -H "Content-Type: application/json" -X POST
GAS (Javascript)
スプレッドシートを用意して
- log (…自分の記事のLGTM、ストック、コメントの最新情報を取得)
- record (…直前のLGTM、ストック、コメントの数値を保存し、数値に変化があれば更新)
- history (…LGTM、ストック、コメント更新履歴を記録)
のシートを用意します。
GASのソースコードです。
ユーザーID、アクセストークン、スプレッドシートID、OBNIZ-IDを設定します。
// ユーザーID
const USER_ID = "ユーザーID";
// アクセストークン
const TOKEN = "アクセストークン";
// 見出し行
const TITLES = [
"No","作成日","タイトル","LGTM","ストック","コメント"
];
// データ整形
const COLUMNS = [
"created_at", "title", "likes_count", "stocks_count", "comments_count"
];
// スプレッドシート
const spreadsheet = SpreadsheetApp.openById('スプレッドシートID')
const sheet = spreadsheet.getSheetByName('log')
const sheet1 = spreadsheet.getSheetByName('record')
const sheet2 = spreadsheet.getSheetByName('history')
// メイン処理
function qiitaMain() {
// シート全体をクリアする。
sheet.clear();
// 見出し行を出力する。
for (let indexY = 0; indexY < TITLES.length; indexY++) {
sheet.getRange(1,indexY + 1).setValue(TITLES[indexY]);
}
// 記事一覧取得処理を呼び出す。
let qiita_data = getQiita();
// 記事一覧を出力する。
for (let indexX = 0; indexX < qiita_data.length; indexX++) {
// No列は個別に出力する。
sheet.getRange(indexX + 3, 1).setValue(qiita_data.length - indexX);
for (let indexY = 0; indexY < TITLES.length; indexY++) {
// 各記事を出力する。
sheet.getRange(indexX + 3, indexY + 2).setValue(qiita_data[indexX][indexY]);
}
}
// 合計行を出力する。
sheet.getRange(2, 1).setValue("ー");
sheet.getRange(2, 2).setValue("ー");
sheet.getRange(2, 3).setValue("ー");
sheet.getRange(2, 4).setFormula("SUM(D3:D" + (qiita_data.length + 1) + ")");
sheet.getRange(2, 5).setFormula("SUM(E3:E" + (qiita_data.length + 1) + ")");
sheet.getRange(2, 6).setFormula("SUM(F3:F" + (qiita_data.length + 1) + ")");
// 実行日時を出力する。
sheet.getRange(qiita_data.length + 5, 1).setValue("実行日時:" + Utilities.formatDate(new Date(),"JST", "yyyy/MM/dd HH:mm"));
// データを更新する。
compare();
}
// 前回の数値と比較してデータを更新
function compare() {
const current_likes_count = sheet.getRange(2, 4).getValue();
const current_stocks_count = sheet.getRange(2, 5).getValue();
const current_comments_count = sheet.getRange(2, 6).getValue();
Logger.log("current:" + current_likes_count + " " + current_stocks_count + " " + current_comments_count);
const before_likes_count = sheet1.getRange(2, 1).getValue();
const before_stocks_count = sheet1.getRange(2, 2).getValue();
const before_comments_count = sheet1.getRange(2, 3).getValue();
Logger.log("before:" + before_likes_count + " " + before_stocks_count + " " + before_comments_count);
// 通知を飛ばしてデータを更新する。
if (current_likes_count > before_likes_count) {
notify();
sheet1.getRange(2, 1).setValue(current_likes_count);
record("likes", current_likes_count);
Utilities.sleep(2000);
}
if (current_stocks_count > before_stocks_count) {
notify();
sheet1.getRange(2, 2).setValue(current_stocks_count);
record("stocks", current_stocks_count);
Utilities.sleep(2000);
}
if (current_comments_count > before_comments_count) {
notify();
sheet1.getRange(2, 3).setValue(current_comments_count);
record("comments", current_comments_count);
Utilities.sleep(1000);
}
}
// 通知結果を記録する
function record(type ,number) {
sheet2.appendRow([Utilities.formatDate(new Date(),"JST", "yyyy/MM/dd HH:mm"), type, number]);
}
// 通知を送信する
function notify() {
const url = "https://obniz.com/obniz/OBNIZ-ID/api/1";
const headers = { "Content-Type" : "application/json" };
const payload = '[{"io":{"animation":{"name":"LED","repeat":2,"status":"loop","states":[{"duration":2000,"state":{"io0":true}},{"duration":500,"state":{"io0":null}}]}}}]';
const options = {
"method" : "POST",
"headers" : headers,
"payload" : payload
}
const response = UrlFetchApp.fetch(url, options);
Logger.log(response);
}
// 記事一覧取得処理
function getQiita() {
let result = [];
let apiUrl = "https://qiita.com/api/v2/users/" + USER_ID + "/items";
let response = UrlFetchApp.fetch(apiUrl);
let total = response.getHeaders()['total-count'];
let page = 1;
let per_page = 100;
let index_result = 0;
while (index_result < total) {
// ページャーごとに分けて記事を取得する。
// 記事を全件取得するまで処理を続ける。
let response = UrlFetchApp.fetch(apiUrl + "?page=" + page + "&per_page=" + per_page);
let json = response.getContentText();
let jsonData = JSON.parse(json);
for (let index_json in jsonData) {
// 取得した記事ごとに処理を行う。
let work = [];
// 記事詳細取得処理を呼び出す。
//let detail = getQiitaDetail(jsonData[index_json]['id']);
// ストック数取得処理を呼び出す。
//let stock_count = getQiitaStock(jsonData[index_json]['id']);
for(let column of COLUMNS) {
switch (column){
case "created_at":
// 日付のフォーマットを変更する。
work.push(Utilities.formatDate(new Date(jsonData[index_json][column]),"JST", "yyyy/MM/dd"));
break;
case "title":
work.push('=HYPERLINK("' + jsonData[index_json]['url'] + '","' + jsonData[index_json][column] + '")');
break;
default:
work.push(jsonData[index_json][column]);
break;
}
}
result[index_result] = work;
index_result++;
}
Logger.log("page=" + page + " done.");
page++;
}
return result;
}
成果物











