canvasに表示した画像を丸で囲って表示する

やりたいこと

canvasにdrawImage()で貼り付けた画像を正円で表示させたい(丸型にトリミングされたように表示させたい)

やるまでは「quadraticCurveTo()ベジェ曲線使ってやらなかん感じ〜!?!?!?!数学できないよ〜!!」(数学関係あるかもわからない)って思ってたけど全然使わなかった

実装

一部抜粋

const image = new Image();
image.src = "表示したい画像";
image.onload = () => {
  ctx.beginPath();
  // 半径40pxの正円
  ctx.arc(50, 50, 40, 0, 2 * Math.PI);
  ctx.fill();  // 枠線表示したい場合はctx.stroke();にする
 
  // パスで切り抜く
  ctx.clip();

  // 80x80の画像を表示する
  ctx.drawImage(image, 10, 10, 80, 80);

  // 他の描画に影響が出ないようにrestoreする
  ctx.restore();
};

コード全文

See the Pen Untitled by miyata (@m1y474) on CodePen.

canvasで正円を描画したいのに楕円になってしまう

Reactでcanvas扱ってたときに正円を描画したいのに楕円になったときの備忘録

canvas描画部分

const ctx = this.canvas.current?.getContext('2d');
if (!ctx) {
  return;
}
ctx.fillStyle = "black";
ctx.fillRect(30, 0, 290, 450);
ctx.fillStyle = "orange";
ctx.beginPath();
ctx.arc(100, 50, 15, 0, 2 * Math.PI);
ctx.fill();

修正前

<canvas style={{ width: "320px", height: "450px" }} ref={this.canvas}></canvas>

修正前のcanvasで描画された楕円

修正後

<canvas width="320px" height="450px" ref={this.canvas}></canvas>

修正後のcanvasで描画した正円

原因はstyle属性にwidth,heightを書いていたこと
canvaswidth, height属性として記述すればOK

意図的に楕円を描きたいときはelipseを使う

参考

developer.mozilla.org

developer.mozilla.org

API Blueprintでパラメータにハイフン(-)が含まれる場合にハイフン以降が出力されない

はじめに

API Blueprint でクエリパラメータをつけるエンドポイントを定義したかったけど、出力されたフォーマットが想定していたものと違っていた時の備忘録

修正前

http://example.com/api/example?date=2000-01-10 みたいなURIを想定して、以下のように書いた(一部抜粋)

## サンプル [/api/example{?date}]

### 取得 [GET]

+ Parameters
    + date: 2000-01-10 (string) - 対象年月日

aglio を使って出力されたもの aglioで出力したHTML

2000で切れてる....!!!!

修正後

値をバッククォートで囲ってあげればいい

## サンプル [/api/example{?date}]

### 取得 [GET]

+ Parameters
    + date: `2000-01-10` (string) - 対象年月日

aglioで出力したHTML

ハイフン以降も表示されました


ドキュメントにもちゃんと書いてありました

NOTE: It's important to note, that if a key or value contains reserved characters such as :, (,), <, >, {, }, [, ], _, *, -, +, ` then the value must be wrapped in a code-block using back-ticks.

React Nativeでフォアグラウンドを検知する

はじめに

アプリがフォアグラウンドにあるタイミングで特定の処理を実施したかった

検証環境

$ react-native -v
react-native-cli: 2.0.1
react-native: 0.67.3

コード

import React from 'react';
import { AppState } from 'react-native';

class App extends React.Component {
  componentDidMount() {
    AppState.addEventListener('change', () => {
      if (AppState.currentState === 'active') {
        // ...フォアグラウントで実施したい処理
      }
    });
}

ちゃんとReactNative側に用意されててよかった...

状態

AppStateStatus トリガー
active アプリがフォアグラウンドに入ったとき。
background アプリがバックグラウンドに入ったとき。
inactive iOSのみ。アプリを開いた状態で通知センターを表示したとき。タスクキルする手前のタスク一覧画面のとき。

着信のときもinactiveになるらしいけど検証してないからわからん。。。
unknownextension はどこで使うねん

参考

reactnative.dev

Rect Nativeでダークモードを無効にする

検証環境

$ react-native -v
react-native-cli: 2.0.1
react-native: 0.67.3

実装

iOS

ios/{project}/AppDelegate.mを以下のように修正する

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{

  // 省略

  // ↓追加
  if (@available(iOS 13, *)) {
    self.window.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;
  }
  return YES;
}

※iOS13以下を切るならif文要らないかも

Android

android/app/src/main/java/path/to/project/MainApplication.javaを以下のように修正する

// ↓追加
import androidx.appcompat.app.AppCompatDelegate;

// 省略

public class MainApplication extends Application implements ReactApplication {

   // 省略

  @Override
  public void onCreate() {
    super.onCreate();
    SoLoader.init(this, /* native exopackage */ false);
    initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
    // 追加
    AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
  }

最後に

ダークモード対応してるアプリすごい。

nodeのversionを切り替えるときに `Use of uninitialized value $b1 in numeric comparison (<=>) at ...` と表示される

nodeのversionを切り替えたときに以下のメッセージが表示された

nodebrew use v16.8.0
Use of uninitialized value $b1 in numeric comparison (<=>) at /Users/XXX/.nodebrew/current/bin/nodebrew line 678.
Use of uninitialized value $a1 in numeric comparison (<=>) at /Users/XXX/.nodebrew/current/bin/nodebrew line 678.
use v16.8.0

ググった感じ、~/.nodebrew/node.DS_Storeがあるのが原因らしいので消しておく

$ ls -al ~/.nodebrew/node
# .DS_Store が表示される
$ rm ~/.nodebrew/node/.DS_Store

再度useでエラーが表示されなければOK

$ nodebrew use v16.8.0
use v16.8.0

俺はuseで発生したけど普通にlistとかでもなるっぽい

【GoogleAppsScript】SESAME API を使ってGASから施錠・解錠する

はじめに

この記事は過去に投稿したSlackからNatureRemoのAPIを叩く記事の延長編です。

iliiliiiliili.hatenablog.jp

外出時に電化製品の電源オフったあとに、そのまま鍵の解錠→再施錠を実施するためにSESAME APIを使用します:)

やること

  1. SESAME APIの準備
  2. GASの作成

過去記事の延長線なのでSlackとの連携部分は省略します。

1. SESAME APIの準備

https://partners.candyhouse.co/login から SESAME に登録済みのメールアドレスでログイン

SESAME APIログイン画面

ログインしたらメールが来るので認証などを済ませる

https://partners.candyhouse.co/ が閲覧可能になるので施錠・解錠に使用したい登録デバイスをクリック
API キー・SecretKey・UUID を UUID をコピる

SESAME APIキー画面

2. GASの作成

施錠・解錠には以下の API を使用

doc.candyhouse.co

RequestBody には以下が必要

①操作コマンド

command 操作内容
88 トグル(施錠状態 → 解錠する・解錠状態 → 施錠する)
82 施錠
83 解錠

②履歴に表示される識別子

アプリの操作履歴↓に表示される文字列を base64 形式で指定する

SESAME アプリ画面

// 履歴に表示される名前を"GoogleAppsScript"にしたい場合
Utilities.base64Encode("GoogleAppsScript"); // "R29vZ2xlQXBwc1NjcmlwdA==";

最初、Utilities.base64Encode()があるの知らなくて、ずっと btoa()使ってたよ...

③AES-CMAC 方式で暗号化したタイムスタンプ

暗号化において、以下の記事を参考にさせていただきました。

zmzlz.blogspot.com

AES-CMAC での暗号化には CryptoJS を使用

CryptoJS のうち、以下のファイルを使うので GAS のコード.gsにコピペ

(参考記事だと別ファイルでのスクリプトになってるけど、呼び出せなかったので全部コード.gsに書いた...)

SESAMEAPI 仕様書に書かれてる 1,2,3 の部分は以下だと思う(多分)

// 1. timestamp (SECONDS SINCE JAN 01 1970. (UTC)) // 1621854456905
// 2. timestamp to uint32 (little endian) //f888ab60
// 3. remove most-significant byte //0x88ab60

// 1. timestampのミリ秒を切り捨て
const ts = Math.floor(Date.now() / 1000);
// ビューの作成
const view = new DataView(new ArrayBuffer(4));
// 2. 32bitにしてリトルエンディアンで格納
view.setUint32(0, ts, true);
// 3. hexにして最上位ビットを削除
const src = view.getUint32(0).toString(16).slice(2, 8);
// 暗号化
const sign = CryptoJS.CMAC(
  CryptoJS.enc.Hex.parse(SECRET_KEY),
  CryptoJS.enc.Hex.parse(src)
).toString();

これを関数にして解錠 →40 秒後に再施錠をする

const CMD = { lock: 82, unlock: 83 };
changeSesameCmd(CMD.unlock);
Utilities.sleep(40000);
// 40秒後に再施錠する
changeSesameCmd(CMD.lock);

コード全文

最後に

SESAME APIでRequestが間違ってたときのステータスコードが500エラーなのしんどい画期的ですごい

【GoogleAppsScript】朝に雨が降っていたら傘を持って帰るのを忘れないようにSlackに教えてもらう

(最近SlackとGASの記事ばっか書いてる気がする...)

はじめに

以下を解消するのが目的です

  • 朝に雨が降ってると退勤時に雨が降ってようが降ってまいが、会社の傘立てに傘を忘れる確率が100%(降ってるときは傘立てが事務所にあるから取りに戻る必要がある)
  • 傘を一つしか持ってないから2日連続で雨のときに無事死亡する

2つ目はずぶ濡れ出勤すれば済む問題なので1つ目を解消します:)

あと、スマホと傘を常時一緒に持っているわけではないのでスマートタグは使いません

どうしたか

弊社では退勤時、Slackの#timecardに「お先に失礼します。」という投稿をする慣習があります。
その慣習を使って、投稿された「お先に失礼します。」のメッセージのスレッドに傘を持ち帰る旨の投稿をして傘の置き忘れを防ぎます。

チャンネルではなくスレッドに書き込む理由は、投稿された自動メッセージがリモートワークの方など他の社員にとって邪魔な情報になる可能性があるからです。

「お先に失礼します。」に関しての自動化は過去記事に記載していますのでよければ以下もご覧ください :) iliiliiiliili.hatenablog.jp

手順

1. SlackBotの作成

1-1. Event Subscriptionsの設定

https://api.slack.com/apps/からSlackBotを作成します。
この辺りの作成手順はググれば出てくるので割愛します。

メッセージの投稿をトリガーとしてWebhookを使用するのでEvent Subscriptionsをオンにします。

Webhook先のURLをVerifiedにするために一旦GASを以下のコードでデプロイしておきます。

const params = JSON.parse(e.postData.getDataAsString());
if (params.challenge) {
  return ContentService.createTextOutput(params.challenge);
}

SlackのEvent Subscriptionsの設定画面

Request URLVerifiedになればOK

eventは以下を設定しました。(チャンネルのみで使用するのであればmessage.channelsだけでOK。多分。)

1-2. OAuth & Permissions の設定

次に、傘を忘れないようにするメッセージを投稿するためにOAuth & Permissionsを設定します

SlackのOAuth &amp; Permissionsの設定画面

今回はメッセージの書き込みとアプリBotの表示のカスタマイズをしたいのでScopesには以下を設定

アプリBotを使うのでBot User OAuth Tokenに表示されているトークンをメモっておく

2. GASの作成

2-1. パラメータの確認

doPost()でSlackからの投稿を受け取って処理します。

function doPost(e) {
  if (params.event.user !== SLACK_USER_ID) {
    return;
  }
  if (
    params.event.type === "message" &&
    params.event.text === "お先に失礼します。"
  ) {
    checkBringBackUmbrella(params.event.ts);
  }
}

やってること

  • 自分以外のSlackUserIdの場合は何もしない
  • テキストが「お先に失礼します。」の時にのみ天気予報のチェック(+Slackへの投稿)を行う

2-2. 天気予報のAPIを叩く

次に、天気予報のAPIを叩いて当日の雨量を取得します。
天気予報を取得するためのAPIにはOpen-Meteoを使用しました。

open-meteo.com

const date = new Date().toLocaleDateString();
// YYYY-MM-DDのformatに整形
const format = date.split("/").map((row) => (row.length === 1 ? "0" + row : row));
const formattedDate = [format[2], format[0], format[1]].join("-");

const LONGTIDE = 雨量を取得したい地域の経度;
const LATITUDE = 雨量を取得したい地域の緯度;
const res = UrlFetchApp.fetch(
  "https://api.open-meteo.com/v1/forecast?timezone=Asia/Tokyo&latitude=" +
    LATITUDE +
    "&longitude=" +
    LONGTIDE +
    "&hourly=rain&start_date=" +
    formattedDate +
    "&end_date=" +
    formattedDate
);

new Date().toLocaleDateString()の戻りが2022/9/22じゃなくて9/22/2022になるのなんでなん...?
ちなGASのタイムゾーンは東京にしてるし、appsscript.jsontimeZoneAsia/Tokyoになってる)

2-3. Slackにメッセージを投稿する

午前7~9時のいずれかの雨量が0.1以上だったら会社に傘を持ってきているとみなして「お先に失礼します。」のメッセージのスレッドに対して投稿する。
(なお、会社用の鞄に常時折りたたみ傘が入ってるので、10時以降に雨が降った場合は傘立てには傘は入っていないものとしています。)

const src = JSON.parse(res);
// 7,8,9時の雨量
const morningRains = src.hourly.rain.slice(7, 10);
// すべて0.0の場合は雨が降っていなかった
if (morningRains.every((rain) => rain === 0.0)) {
  return;
}
// 雨が降っている場合はSlackのスレッドに書き込む
const SLACK_TOKEN = "SlackのAppBotのトークン";
const SLACK_CHANNEL_NAME = "チャンネル名";
UrlFetchApp.fetch("https://api.slack.com/api/chat.postMessage", {
  method: "post",
  payload: {
    token: SLACK_TOKEN,
    channel: SLACK_CHANNEL_NAME,
    text: "<@"+SLACK_USER_ID+">今日は午前中の雨量が0.1以上でした。傘があれば持って帰りましょう。",
    icon_emoji: ":umbrella:",
    username: "放置傘警察",
    thread_ts: ts,
    reply_broadcast: false,
  },
});

これで雨のときにはSlackが教えてくれるようになりました:)

Slackのスレッド画面

コード全文

最後に

会社以外の傘立てに忘れたらおしまいだね!!

参考

open-meteo.com

【GoogleAppsScript】Slackに投稿したらNature RemoのAPIを使って照明とエアコンを消す

はじめに

外出時にいつも同居人に外出する旨のメッセージを送ってるんだけど↓の3つをやらなくちゃいけないのがめんどい

  1. Nature Remoで照明を消す
  2. Nature Remoでエアコンを消す
  3. Slackでメッセージを送る

「Slackにメッセージを送るのが前提としてあるんだったら他のフローは省略できるのでは...?」って思ったのがきっかけです

Nature Remoとは?

nature.global

やること

  1. Slackにメッセージを投稿する
  2. SlackからGASにイベントを投げてもらう
  3. GASからNature RemoのAPIを叩く

1は人間が実行して2,3を自動化します:)

使うもの

  • Nature Remo API
  • GoogleAppsScript
  • Slack Bot

(いつも GoogleAppsScript なのか GoogleAppScripts なのかが覚えられずググってまう)

手順

1. Slackでアプリbotの作成

https://api.slack.com/apps からアプリBotを作成して、Event Subscriptionsからトリガーとなるイベントを設定する
今回はチャンネルでもDMでも使いたかったから以下を設定

Request URL で検証するために以下の内容でGASを作成して一旦Webアプリケーションとしてデプロイする

function doPost(e) {
  const params = JSON.parse(e.postData.getDataAsString());
  return ContentService.createTextOutput(params.challenge);
}

実行URLをSlack側のRequest URLに入力
↓みたいにVerified になったらSave Changesボタンを押せるようになるので保存

Slack Apiの画面

2. GASからNature RemoのAPIを叩く

https://home.nature.global/ からNature RemoのBearerトークンの発行

電化製品を操作するためのIDの取得には https://api.nature.global/1/appliances/ へGETリクエストを送って確認する

①電気を消す

UrlFetchApp.fetch(
  "https://api.nature.global/1/appliances/" + LIGHT_ID + "/light",
  {
    method: "post",
    headers: { Authorization: "Bearer " + NATURE_REMO_TOKEN },
    payload: {
      appliance: LIGHT_ID,
      button: "off",
    },
  }
);

②エアコンを消す

UrlFetchApp.fetch(
  "https://api.nature.global/1/appliances/" + AIRCON_ID + "/aircon_settings",
  {
    method: "post",
    headers: { Authorization: "Bearer " + NATURE_REMO_TOKEN },
    payload: {
      appliance: AIRCON_ID,
      button: "power-off",
    },
  }
);

クエリパラメータでもbodyでも動作するのなんでなん...?

Slackからのイベントでメッセージが特定の言葉だったときにこれらを実行する

function doPost(e) {
  const SLACK_USER_ID = "自分のSlackUserId";
  const params = JSON.parse(e.postData.getDataAsString());
  if (
    params.event.type === "message" &&
    ["トリガーにしたいメッセージの配列"].includes(params.event.text) &&
    params.event.user === SLACK_USER_ID
  ) {
    turnOffAppliances();
    return;
  }
  return;
}

コード全文

最後に

スマートスピーカー持ってません

参考

developer.nature.global swagger.nature.global

api.slack.com

はてブロの記事が投稿から1年以上経過している場合にアラートを表示する

経緯

ありがたいことに投稿日から数年経過しても、検索からアクセスされてる記事がいくつかあるんだけど、情報が古いから○iitaみたいに この記事は公開日からn年以上が経過しています。情報が古い可能性がありますので十分ご注意ください。 っていう注意書きを画面上に表示させたかった

注意

カスタマイズ

1. CSSを追加

管理画面からデザインカスタマイズ画面を開く
はてブロ管理画面 デザインCSS

デザインCSSにalert用のスタイルを追加 (Bootstrapのwarning alertと同じ)

.warning-alert {
    font-size: 12px;
    background-color: #ebd1b6;
    padding: 0.75rem 1.25rem;
    color: #856404;
    background-color: #fff3cd;
    border: 1px solid #fae39f;
    border-radius: 0.25rem;
}

2. JavaScriptの追加

設定詳細設定画面を開く

設定>詳細設定>&lt;head&gt;要素にメタデータを追加w:100

<head>要素にメタデータを追加に以下のJSを追加する

document.addEventListener("DOMContentLoaded", function () {
  const dtElements = document.querySelectorAll(".date.entry-date.first time");
  if (dtElements.length === 0) {
    return;
  }
  const nowTs = new Date().getTime();
  dtElements.forEach((element) => {
    if (!element) {
      return;
    }
    const dt = element.getAttribute("datetime");
    const postedTs = new Date(dt).getTime();
    const diffYears = ((nowTs - postedTs) / (1000 * 60 * 60 * 24) / 365).toFixed();

    // 記事公開日から1年以上経過している場合にalertを表示する
    if (diffYears >= 1) {
      const div = document.createElement("div");
      div.innerText = `この記事は公開日から${diffYears}年以上が経過しています。情報が古い可能性がありますので十分ご注意ください。`;
      div.classList.add("warning-alert");
      element.closest(".entry-header").prepend(div);
    }
  });
});

上記の設定が完了後に記事一覧・詳細画面で以下のように表示されます:)

記事に表示されるalert


MySQLのDELETE文をEXPLAINで確認する

MySQL5.5でDELETE文をEXPLAINで見ようとしたらエラーになった

mysql> EXPLAIN DELETE FROM samples WHERE title = "title";
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'DELETE FROM samples WHERE title = "title"' at line 1

EXPLAINでDELETE文が使えるのは5.6.3からで5.5系では実行できないみたい(泣)

dev.mysql.com

MySQL 5.6.3 現在、EXPLAIN に使用できる説明可能なステートメントは、SELECT、DELETE、INSERT、REPLACE、および UPDATE です。MySQL 5.6.3 より前では、SELECT が唯一の説明可能なステートメントです。

5.5で確認したいときは代替として以下で対応する

EXPLAIN SELECT 1 FROM samples WHERE title = "title";

MySQLで連番のカラムを作成する

いつ使うの?

  • AUTO_INCREMENTのidとは別に仮の連番をMySQLで生成したいとき

前提

↓みたいなテーブルにMySQLで生成した連番のidを追加する

id category
4291 アニメ
11 映画
9302 漫画
322 ドラマ

SQL

SELECT
    category,
    @tmp_id := @tmp_id + 1 tmp_id
FROM
    (
SELECT
    @tmp_id := 0
FROM DUAL
) tmp, samples

結果

id category tmp_id
4291 アニメ 1
11 映画 2
9302 漫画 3
322 ドラマ 4

こんな処理を使わなきゃいけない場合はだいたい設計がおかしいから見直そうな!

コンストラクタで非同期処理を実施しているクラスのテストをJestで書く

constructor()で非同期処理を実施しているクラスのテストを書きたかったときの備忘録

↓みたいにconstructor()で非同期処理を実施していて、Promiseをreturnしていない

export default class A {
  private readonly list: any;

  constructor() {
    new Repository().fetch().then((res) => (this.list = res));
  }

  public b() {
    return this.list;
  }
}

テストしたい部分をasync()で囲ってあげればOK

test("sample", () => {
  async () => {
    expect(new A().b()).toBe([]);
  };
});
$ npm run test
> xxxx@1.0.0 test
> jest

 PASS  __tests__/test.test.js
  ✓ sample (1 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.056 s
Ran all test suites.

自信ないんだけど合ってる...? 間違ってたり、他に方法があったらご指摘ください!

TypeScriptでJestを使う

必要なパッケージのinstall

*1

$ npm install --save-dev @babel/preset-typescript

babel.config.js に追記

module.exports = {
  presets: [
    ["@babel/preset-env", { targets: { node: "current" } }],
    "@babel/preset-typescript" // 追加
  ],
};

テストの実施

$ npm run test

> xxxxxx@1.0.0 test
> jest

 PASS  __tests__/sample.test.js
  ✓ jest sample (18 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.665 s, estimated 1 s

テストが通ることを確認できればOK


全部公式に書いてあるからちゃんと読もうな

jestjs.io


*1:必要なパッケージにts-jestを含めていましたがbabelを使用している場合は不要のため修正しました。
babel+jestで型チェックも行いたい場合は必要になります。id:munieru_jp様コメントありがとうございました!

Slackの投稿を1ヶ月単位でスプレッドシートに書き込む【GoogleAppsScript】


現状・やりたいこと

  • プライベートで使ってるSlackがフリープラン
  • 過去の投稿を残しておきたい
  • 保存する投稿はDMのやりとり

やること

1. Slackでユーザーbotの作成

試してないけどアプリbotだとできなさそうだからユーザーbotにした
(てかDMにアプリbotって追加できないよね。。。?)

メッセージ取得に必要な権限を設定してユーザーbotの作成

2. SlackAPIでSlackに投稿した1ヶ月分の投稿の取得

エンドポイントは https://api.slack.com/api/conversations.history

最初channelにDM相手のユーザーIDを指定しててずっとchannel_not_foundが返ってきてハマった。。。
↓DMにもチャンネルIDあるの知らんかった

SlackチャンネルID

メッセージ取得件数がデフォルトで100件ほどだからresponse_metadata.next_cursorが空になるまでfetchMessages()を実行する

function fetchMessages(results = [], cursor = "") {
  const res = UrlFetchApp.fetch(
    "https://api.slack.com/api/conversations.history?channel=" +
      CHANNEL_NAME +
      "&oldest=" +
      oldest +
      "&latest=" +
      latest +
      "&cursor=" +
      cursor,
    {
      method: "get",
      payload: {
        token: USER_TOKEN,
      },
    }
  );

  const src = JSON.parse(res);
  if (!src.ok) {
    Logger.log(src);
    throw new Error("レスポンス不正");
  }

  results.push(...src.messages);
  // response_metadata.next_cursorがある場合は再帰処理
  if (src?.response_metadata?.next_cursor) {
    return fetchMessages(results, src.response_metadata.next_cursor);
  }
  return results;
}

3. スプレッドシートにシートの作成と書き込み

function myFunction() {
  const ss = SpreadsheetApp.openById(SHEET_ID);
  const sheet = ss.insertSheet();
  const dt = new Date();
  // 前月分の投稿を取得するため-1ヶ月にする
  dt.setMonth(dt.getMonth() + 1 - 2)
  const dateArray = dt.toLocaleDateString().split("/"); // toLocaleDateString()のformatがMM/DD/YYYYになるのはなんで...??
  // 年月毎にシートの作成
  sheet.setName(dateArray[2] + "-" + (dateArray[0].length === 1 ? "0" + dateArray[0] : dateArray[0]));

  const src = fetchMessages();
  // 日付の昇順で登録したいからsort
  src.sort((a, b) => a.ts - b.ts);
  const results = [];
  src.map((row) => {
    const dt = new Date(row.ts * 1000).toLocaleString("JP");
    results.push([dt, USERS[row.user], row.text]); // 時間・ユーザー名・メッセージ
  });

  sheet.getRange(1, 1, results.length, 3).setValues(results); // 一括書き込み
}

4. GASのトリガー設定

トリガーの設定を毎月1日に設定する

GoogleAppsScriptのトリガー設定

あとはスプレッドシートに書き込まれればOK

Googleスプレッドシート

コード全文

感想

UrlFetchApp.fetchparams指定できんの知らんかった
文字列連結むりすぎ

developers.google.com

参考

api.slack.com developers.google.com


株式会社エイルシステムではWebエンジニア・モバイルアプリエンジニアを募集しています。
実務経験がなくてもOKです。ご興味のある方は弊社HPよりご連絡ください。