Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

This article is a Private article. Only a writer and users who know the URL can access it.
Please change open range to public in publish setting if you want to share this article with other users.

ねこIoTLT vol.10の発表ネタを考える#2 (エゴサーチでQiitan(きーたん)を動かす)

Last updated at Posted at 2025-02-15

はじめに

2025年2月22日に開催する予定の発表ネタの備忘メモ#2です。(2025年2月15日に執筆しています)

備忘メモ#1はこちら。

作るもの

アドベントカレンダー2024の完走賞でいただいた"きーたん"(Qiitan)をエゴサ(QiitaのLGTM、ストック、コメント)で動かします。

魔改造するもの(FukuFukuNyankoまねっこアニマル)

ねこIoTLT vol.5で魔改造したものをさらに改造して、Obnizを接続します。

スクリーンショット 2025-02-15 17.57.18.png

スクリーンショット 2025-02-15 17.59.08.png

スクリーンショット 2025-02-15 17.59.22.png

裁縫 (きーたんの中に仕込む)

裁縫の技術が必要で、背中からアプローチします。

IMG_9309.jpg

中の詰め物(綿)を取り出します。

IMG_9311.jpg

きーたんの中に駆動装置を仕込みます。

IMG_9314.jpg

構成

GASからQiita API v2経由でQiitaの記事をエゴサ(LGTM、ストック、コメントを確認)して、数が増えていたら、Obniz経由で"きーたん"(Qiitan)を動かします。

スクリーンショット 2025-02-15 18.08.53.png

Qiita API v2

アクセストークンを作成します。
アカウントの設定 → アプリケーション → 個人用アクセストークン → 新しくトークンを発行する

スクリーンショット 2025-02-16 17.28.06.png

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をずっと繰り返します。

command.json
[
    {
        "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、ストック、コメント更新履歴を記録)

のシートを用意します。

スクリーンショット 2025-02-16 17.22.12.png

スクリーンショット 2025-02-16 17.22.51.png

スクリーンショット 2025-02-16 17.23.08.png

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;
}

成果物

スクリーンショット 2025-02-16 16.15.12.png

0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?