tmp.php

// TODO: 後で修正する

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


特定のファイルを後からgitignoreに追加する方法

.gitignoreに追加してもリモートリポジトリにまだ残ってるやんけ!って毎回ググってるからいい加減覚えような

.gitignoreに除外するファイル(ディレクトリ)を追加してから以下を実施する

$ git rm --cached ファイル名
$ git add .
$ git commit -m "chore: .gitignoreに追加"

ディレクトリを除外する場合は$ git rm --cached -r ディレクトリ名でOK


webpackのbuildでmain.js.LICENSE.txtを出力させない

webpackでproduction buildしたときdist/にmain.js.LICENSE.txtを生成させたくなかった

const TerserPlugin = require("terser-webpack-plugin");

module.exports = {
  // ...省略
  optimization: {
    minimizer: [
      new TerserPlugin({
        extractComments: false,
      }),
    ],
  },
};

extractCommentsがデフォルトでtrueになっているのでfalseにする。
デフォルトは@preserve @license @cc_onアノテーションがあるコメントを拾ってくるっぽい。
allにすると全てのコメントが出力される


Slackbotからの通知メッセージをattachmentsでリッチにする

通常のテキストはこんな感じ↓

これを↓にする

GASのコード

感想

最初attachmentsに渡す値を配列にしてなかったから送信されなくてハマった。。。
個人的な用途としてはcolor, title, title_linkだけで十分かも。(footer_iconとかいらんくね?)

参考

api.slack.com


【TypeScript】Graph API を使って Instagram の投稿を取得する【2022年6月版(v14.0)】

React x TypeScriptでInstagramの投稿をサイト上に表示したかったときの備忘録
使用したGraph APIはv14.0だけど12.x, 13.xあたりも同じっぽい(多分)(未検証)

全体的な流れのうち、1, 2は書くのめんどいので割愛

  1. Instagramをプロアカウントに変更する
  2. facebookInstagramをリンクさせる
  3. facebook developerアカウントでアプリの作成
  4. Graph APIで使用するアクセストークンの有効期限の延長
  5. instagram business idの取得
  6. instagramの投稿の取得してサイトに表示する

1. facebook アプリの作成

アプリタイプにビジネスを選択してアプリの作成

基本情報は適当に入力してアプリ作成後に表示されるアプリIDapp secretをメモっておく

2. 無期限アクセストークンを取得する

グラフAPIエクスプローラ画面で短期アクセストークンを取得する

以下の項目を指定する

  • Facebookアプリ
    • facebook developerアカウントで作成したアプリ
  • ユーザーまたはページ
  • アクセス許可
    • pages_show_list
    • business_management
    • instagram_basic
    • instagram_manage_comments
    • instagram_manage_insights
    • pages_read_engagement
    • pages_read_user_content
    • public_profile (デフォルトで設定済み)

インスタの投稿だけだし instagram_basic しか使わんだろって思って instagram_basicpublic_profile のみにしてたら無期限アクセストークンが取得できなくてハマった。(公式読め)

Generate Access Token を押すとアクセス許可のためのダイアログが表示されるから許可

③アクセストークン左のinfoマークを押してアクセストークンデバッガーの画面に行く

有効期限が1時間以内になっているので「アクセストークンを延長」を押す

④ 長期アクセストークンが表示されるのでデバッグボタンを押す

発行されたアクセストークンの有効期限が「受け取らない」になってればOK

3. instagram business id を取得する

グラフAPIエクスプローラ画面か、Postmanとかで↓を叩くと、

https://graph.facebook.com/v14.0/me?fields=accounts{instagram_business_account}&access_token=[2.で取得したアクセストークン]1

↓下記の形式でデータが戻ってくるので accounts.data.instagram_business_account.id をメモっておく

{
  "accounts": {
    "data": [
      {
        "instagram_business_account": {
          "id": "123456"
        },
        "id": "123456"
      }
    ],
    "paging": {
      "cursors": {
        "before": "xxxxxx",
        "after": "xxxxxx"
      }
    }
  },
  "id": "123456"
}

4. instagramの投稿の取得

https://graph.facebook.com/v14.0/[3で取得したinstagram business id]?fields=media.limit([取得したい件数]){[取得したいfieldをカンマ区切りで指定]}&access_token=[2で取得したアクセストークン] 2 (URLに()使うのモヤる)

をPostmanとかで叩くと、↓の形式で戻ってくるので media.data を使ってサイトに描画する

{
  "media": {
    "data": [
      {
        "media_url": "https://scontent.cdninstagram.com/v/xxxxxxxxxxxx/",
        "media_type": "CAROUSEL_ALBUM",
        "id": "123456"
      }
    ],
    "paging": {
      "cursors": {
        "before": "xxxxxxxxxxxx",
        "after": "xxxxxxxxxxxx"
      },
      "next": "https://graph.facebook.com/v14.0/xxxxxxxxxxxx"
    }
  },
  "id": "123456"
}

5. 記事の取得

export default class Instagram {
  public async fetchPosts(limit: number): Promise<Post[]> {
    return await axios
      .get(
        `https://graph.facebook.com/${process.env.GRAPH_VERSION}/${process.env.GRAPH_BUSSINESS_ID}`,
        {
          params: {
            fields: `media.limit(${limit}){media_url,thumbnail_url,permalink,media_type}`,
            access_token: process.env.GRAPH_ACCSESS_TOKEN,
          },
        }
      )
      .then((res) => res.data.media.data)
      .catch((error) => {
        console.error(error);
      });
  }
}

(※ import などは省略)

今回はReactで実装

<section>
  {this.props.posts.map((row: Post, index: number) => (
    <Box {...row} key={index}></Box>
  ))}
</section>

最終的に↓みたいなマークアップで描画されればOK

<a target="_blank" rel="noopener noreferrer" href="https://www.instagram.com/p/xxxxxx/">
  <img src="https://scontent.cdninstagram.com/v/xxxxxx">
</a>

参考

developers.facebook.com developers.facebook.com developers.facebook.com developers.facebook.com

※本記事は2022/06/26時点の情報です。



  1. URLの中で動的な部分は[]で囲っています。

  2. URLの中で動的な部分は[]で囲っています。

hoverしたときにテキストの枠線を左から右に引く

要点

  • 通常時は疑似要素のborder-bottom: 1px solid #000; を指定したまま width: 0%; にしておく
  • hoverしたときにwidth: 100%; にする

ボタンをhoverしたときに背景色をスライドさせる

これのこと

HTML

<button type="button">Button</button>

ボタン部分

button {
  background-color: #fff;
  width: 200px;
  display: block;
  padding: 10px 0;
  border: 1px solid #000;
  position: relative;
  z-index: 1;
}
button:hover {
  color: #fff;
}

ボタン背景部分

button::before {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: -1;
  content: '';
  background-color: #000;
  transform-origin: right top;
  transform: scale(0, 1);
  transition: transform .3s;
}
button:hover::before {
  transform-origin: left top;
  transform: scale(1, 1);
}

要点

  • transform: scale(0, 1); で疑似要素の背景を非表示にしておく
  • hoverしたときにtransform: scale(1, 1);にして表示する
  • あとはtransform-originでスライドさせたい方向の値を指定する

コード全文

button {
  background-color: #fff;
  width: 200px;
  display: block;
  padding: 10px 0;
  border: 1px solid #000;
  position: relative;
  z-index: 1;
}
button:hover {
  color: #fff;
}
button::before {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: -1;
  content: '';
  background-color: #000;
  transform-origin: right top;
  transform: scale(0, 1);
  transition: transform 0.3s;
}
button:hover::before {
  transform-origin: left top;
  transform: scale(1, 1);
}

その他

感想

右から左、みたいに特定の方向に向かってスライドさせたい場合っていちいち疑似要素使わなかん感じ?クソめんどくね?


スプレッドシートで列名のアルファベットを取得する

B2セルで=COLUMN()したときにBじゃなくて2が表示される

俺がほしいのはBなんだよ!!

ということで完成形

=SUBSTITUTE(REGEXEXTRACT(ADDRESS(ROW(), COLUMN()), "\$.+?\$"), "$", "")

長すぎる......
セル確認したときにぱっと見でどこ参照しているのか分かりづらすぎるんだけどもっと簡単に取得できる方法ない??

やってること

  1. ADDRESS()でセル参照を文字列で取得
  2. $B$2が取得できるのでREGEXEXTRACT()$で囲まれている文字列を検索
  3. SUBSTITUTE()$を空文字に変換

実際はBだけを使うことはないのでINDIRECT()と一緒に使ってセル参照する

Cloud Firestore で collection の基本操作

Cloud Firestore使ったときのメモ
検証用のメモだからダブルクウォートとシングルクウォートが混ざってるのは気にするな

Create

const docRef = await addDoc(collection(db, "collection"), {
  title: "title",
  description: "description",
  created_at: new Date(),
});
console.log("Document written with ID: ", docRef.id);

Read

// 単一の取得
const ref = doc(db, "collection", "document ID");
const snap = await getDoc(ref);
console.log(snap.data());

// 複数取得
const q = query(collection(db, "collection", where('title', '==', 'title')));
const snap = await getDocs(q);
snap.forEach((doc) => console.log(doc.data()));

// 順番と件数を指定して複数取得
const q = query(collection(db, "collection"), orderBy('created_at', 'desc'), limit(2));
const snap = await getDocs(q);
snap.forEach((doc) => console.log(doc.data()));

Update

const ref = doc(db, "collection", "document ID");
await updateDoc(ref, {
    title: "update title",
});

Delete

await deleteDoc(doc(db, 'collection', 'document ID'))

参考

firebase.google.com


Atomフィードで取得したRedmineのチケット情報をSlackに通知する

いつ使うの?

以下の状態のRedmineを使用しているとき

  • チケット操作時にメール通知が来ない
  • APIアクセスキーが発行されていない
  • 管理権限を持っていない

Slack App のRSSインテグレーションも使ったんだけど新規チケットの通知しか来ないし思いの外役立たずでした

前提

  • Googleアカウント作成済み
  • Slackbot作成済み

コード

Slackへのリクエストは以下のライブラリを使用しています

github.com

function myFunction() {
  // Redmine のチケット情報を取得する
  const feedURL = 'https://{Redmineのドメイン}/issues.atom?query_id={適当なquery_id}&key={Atomアクセスキー}';
  const response = UrlFetchApp.fetch(feedURL);
  const xml = XmlService.parse(response.getContentText());
  const rootDoc = xml.getRootElement();
  const atom = XmlService.getNamespace('http://www.w3.org/2005/Atom')

  const items = rootDoc.getChildren('entry', atom)
  if (items.length === 0) {
      return;
  }
  const slackUserId = '{SlackのUserId}';
  let message = '<@' + slackUserId + '> さん宛の Redmine のチケットが' + items.length + '件あります。\n';

  items.forEach((item) => {
    const title = item.getChild('title', atom).getText();
    // `/1111` のようにスラッシュ+数字の羅列をチケットIDとみなして検索
    const issueId = item.getChild('id', atom).getText().match(/\/([0-9]+)/)[1];
    // タイトルの文字列にチケットのリンクを貼る
    message += '<https://{Redmineのドメイン}/issues/' + issueId + '|' + title + '>\n';
  })

  const options = {
    username: 'Redmine',
    link_name: true,
    icon_emoji: ":redmine:"
  };
  // slackに投稿
  const slackApp = SlackApp.create('xoxb-XXXXXXXXXXXX'); // Slackbot のtoken
  slackApp.postMessage("#Slackのチャンネル名", message, options);
}

((GASのシンタックスハイライトってjavascriptでいいの…?))

後は定期実行のトリガーを任意で作成してSlackに通知が来ればOK

感想

早くGASでもテンプレートリテラル使えるようにしてくれ


Toggl の作業内容を Redmine に登録する

弊社では作業管理ツールに Redmine を使用しています。
チケットと呼ばれる各タスクに作業時間を入力して、↓みたいに退社時に Slackbot で1日の作業内容がわかるようになっています。

(ちなみに上記のSlackbotは弊社代表の自作です。)

ただ、これが死ぬほどめんどくさいんです。
作業開始時の時刻を覚えておいて作業が完了したら終了時の時刻から逆引きして時間を出すっていう、算数の引き算でつまずいた人間からすると地獄です。冗談抜きに引き算だけで30分くらい取られます。
ということでめんどくさいことはプログラムにやらせよう!

事前準備

Toggl アカウントの作成

toggl.com

これがなきゃ始まりません。
アカウント作成後の profile 画面に API Token が発行されているのでコピります。

Toggl 拡張機能のインストールと設定

chrome.google.com

なくても成立はするんですが、Redmine 上でタイマーを打ちたいのでインストールします。
インストール後に、拡張機能の設定で Integrations メニューの中で Redmine のURLを有効にします。
(自社サーバーに Redmine を立ててる場合は Custom URLs for integrationsドメインを登録すればOK)

最終的にRedmine のチケットを見た時に Toggl のボタンが表示されてればOK

プログラムの作成

最低限必要な処理以外は省略してるので「リクエスト周りのエラーハンドリングどうすんの?」とか言う輩は帰ってください。
アーキテクチャ警察も帰ってください。

API Document は以下を参照しました

① 当日の作業記録を取得する

<?php
$togglUri = 'https://api.track.toggl.com/api/v8/time_entries';
$options = ['auth' => [$_ENV['TOGGL_API_TOKEN'], 'api_token']];
$dateTime = new DateTimeImmutable();
$query = http_build_query([
    'start_date' => $dateTime->setTime(00, 00, 00)->format('c'),
    'end_date' => $dateTime->setTime(23, 59, 59)->format('c'),
]);
$entryRequest = (new \GuzzleHttp\Client())->request('GET', "{$togglUri}?{$query}", $options);

$res = $entryRequest->getBody()->getContents();
$entries = json_decode($res, true);

② Toggl の作業内容から Redmine に登録する作業時間データを用意する

<?php
// 活動の選択肢
$activities = ['作業内容1' => 1, '作業内容2' => 2];
foreach ($entries as $entry) {
    // 作業時間のコメント
    $description = $entry['description'] ?? '';

    // 作業時間の時間
    // 開始・終了の差分
    $start = new DateTimeImmutable($entry['start']);
    $stop = new DateTimeImmutable($entry['stop']);
    $diff = $stop->diff($start);
    $min = round(($diff->i / 60), 3);
    $diffHours = $diff->h + $min;

    // Redmine でタイマーを打ったときのフォーマットに沿って` #1234 `の文字列をチケットIDとして検索
    preg_match('/\ #([0-9]+)\ /', $description, $results);
    // description にチケットIDがあればサブパターンマッチの文字列を使用する
    // プロジェクトに紐付かない場合は任意のデフォルトのチケットIDを指定
    $issueId = $results[1] ?? $_ENV['REDMINE_DEFAULT_ISSUE_ID'];

    // タグの登録があれば1番目のタグを活動とする
    // なければデフォルトの活動IDを指定
    $activityId = $activities[$entry['tags'][0] ?? 1];

    // xmlの生成
    $xmlValues = [
        'issue_id' => $issueId,
        'spent_on' => $dateTime->format('Y-m-d'),
        'hours' => $diffHours,
        'activity_id' => $activityId,
        'user_id' => $_ENV['REDMINE_USER_ID'],
        'comments' => $description,
    ];
    $xml = new SimpleXMLElement('<time_entry></time_entry>');
    foreach ($xmlValues as $key => $value) {
        $xml->addChild($key, $value);
    }
}

Toggl の作業レコードごとに以下のようなXMLが生成されます。

<time_entry>
    <issue_id>1234</issue_id>
    <spent_on>2022-02-19</spent_on>
    <hours>1.0</hours>
    <activity_id>1</activity_id>
    <comments>タスク #1234 テキストテキストテキストテキスト</comments>
    <user_id>1</user_id>
</time_entry>

Redmine に作業時間を登録する

<?php
$redmineUri = "{$_ENV['REDMINE_URL']}time_entries.xml?key={$_ENV['REDMINE_API_KEY']}";
$request = (new \GuzzleHttp\Client())->request('POST', $redmineUri, [
    'headers' => ['Content-Type' => 'text/xml',],
    'body' => $xml->asXML()
]);

APIキーが発行されてない Redmine を使ってる場合は知りません!管理者に頼もう!

作業の登録

ここからがやりたかったことです

  1. 作業の前後で Redmine のチケット画面に表示されてる Toggl のタイマーをクリック

    タグを付けると活動がわかるのでタグだけ任意でつけておく
  2. 退勤前にプログラムを実行する
  3. Redmine に作業内容が登録されるのを確認する

最終的に↓みたいにして使ってます!

コード全文は以下に置いてあります。

https://github.com/m1y474/toggl-redmine

最後に

Toggl 過激派なので Toggl 布教の記事も書きたい…(Toggl Trackという名称を使ってない時点で過激派ではない)

ちなみにこのプログラムを作るまでは、下記サイトをiframeに埋め込んだものを拡張機能にして手動で計算してました!お世話になりました!2桁以上の引き算は苦手です!!

www.jalps.net

GitHubにpushしたらHerokuへデプロイする

f:id:IlIIlIIIlIIlI:20220217233028p:plain

Herokuデビューしたときの備忘録
結構前のだから手順抜けてるかも

前提

  • GitHubとHerokuのアカウントが作成済み
  • GitHubとHerokuのアカウントが連携済み

App作成

App nameは空欄にしとけばHeroku側で勝手に作ってくれるのでそのままCreate appを押す
名前つけたい場合は使われてない名前なら任意でつけてOK

f:id:IlIIlIIIlIIlI:20220217231302p:plain

Deploy設定

App作成後にDeployの画面が表示されるのでDeployment MethodGitHubを選択する

f:id:IlIIlIIIlIIlI:20220217231449p:plain

Connect to GitHubが表示されるので対象のRepositoryのConnectを押す

f:id:IlIIlIIIlIIlI:20220217231728p:plain

Automatic deploysで任意のブランチを選択してEnable Automatic Deploysをオンにする

f:id:IlIIlIIIlIIlI:20220217231932p:plain

手動デプロイがしたいときはManual deployから任意のブランチを選択してDeploy Branch押すだけ f:id:IlIIlIIIlIIlI:20220217232040p:plain