Slack API を使って、特定のチャンネルのメッセージと発言ユーザーを取得する #slack

概要

Slack の発言を分析したいみたいな話が職場であって、サクッと取れないかな〜と雑にシェルスクリプトで試してみた記録。

便利なツールとか既にありそうだけどね。。

  • Slack API を使って
  • 特定のチャンネルから一定期間の発言を取得し
  • 「投稿日時」
  • 「その発言をしたユーザー」
  • 「メッセージへのリンクURL」
  • を出力するまでの流れ

APIを叩く準備

Slack API を叩くには Token を発行する必要がある。

Token 単体では発行できなくて、アプリケーションを作成してインストールする流れになるようだ。

主な流れは下記の通り。

  1. Slack Application を作成
  2. Application に対して API を叩くのに必要な Scope を設定
  3. Token を発行
  4. Application を使いたい Slack Team にインストール

下記の記事がとても参考になった。

tearoom6.hateblo.jp

メッセージの取得

conversations.history を使う。

conversations.history method | Slack

Tester タブから実際にリクエストを試せるので便利。

slack_message_list.bash みたいなのを作ってみる。

  • jq を使って必要な情報だけ取り出す
    • ユーザーIDと投稿日時
  • 取得上限はデフォルト100件だが最大の1000にしている
    • LIMIT パラメータ
    • 1000件以上は考慮していない
  • 取得範囲を 2021-01-01 以降にしてる
    • OLDEST パラメータ
#!/usr/bin/env bash

set -eu

readonly API_TOKEN="xxxx-xxxxxxxxx-xxxx"
readonly CHANNEL="C1234567890"
readonly LIMIT=1000

# Unixtime に変換し、小数点配下を埋め。 ※Mac でやったので BSD の date コマンド
readonly OLDEST="$(date -j -f "%Y-%m-%d %H:%M:%S" "2021-01-01 00:00:00" +%s).000000"

## API を叩き、 jq で user と ts だけ抽出してカンマ区切りにする
curl -s "https://slack.com/api/conversations.history?channel=${CHANNEL}&oldest=${OLDEST}&limit=${LIMIT}" \
     -H "Authorization: Bearer ${API_TOKEN}" \
     -H 'Content-Type: application/json; charset=utf-8' \
  | jq -r '.messages[] | [.user, .ts] | @csv'

exit 0

ユーザー情報とメッセージへのリンクURLを取得

ユーザーIDを元にユーザー名、投稿日時を元にメッセージへのリンクURLを取得する。

ユーザーの取得には users.info を使う。

users.info method | Slack

メッセージへのリンクURLの取得には chat.getPermalink を使う。

chat.getPermalink method | Slack

conversations.history で取得した値を連携するような slack_user_link.bash を作成する。

#!/usr/bin/env bash

set -eu

readonly API_TOKEN="xxxx-xxxxxxxxx-xxxx"
readonly CHANNEL="C1234567890"

arg=${1}

# 受け取った値を分解して、不要なクォーテーションを除去
user_id=$(echo ${arg} | cut -d',' -f 1 | sed 's/"//g')
ts=$(echo ${arg} | cut -d',' -f 2 | sed 's/"//g')

## users.info
user_name=$(curl -s "https://slack.com/api/users.info?user=${user_id}" \
     -H "Authorization: Bearer ${API_TOKEN}" \
     -H 'Content-Type: application/json; charset=utf-8' \
     | jq -r .user.name)

## chat.getPermalink
url=$(curl -s "https://slack.com/api/chat.getPermalink?channel=${CHANNEL}&message_ts=${ts}" \
     -H "Authorization: Bearer ${API_TOKEN}" \
     -H 'Content-Type: application/json; charset=utf-8' \
      | jq -r .permalink)

# 投稿日時を表示用に変換
date=$(echo ${ts} | cut -d'.' -f 1 | xargs -I@ date -r @ +"%Y-%m-%d %H:%M:%S")

# カンマ区切りで表示
echo "${date},${user_name},${url}"

exit 0

組み合わせて実行してみる

↓な感じで組み合わせて実行できる。

./slack_message_list.bash | xargs -I@ ./slack_user_link.bash @

最初のスクリプト"U012AB3CDE","1512085950.000216" みたいなのを出力するので、それを元に次のスクリプトAPI を順繰り叩く感じ。

最終的な出力は↓みたいになる。

2021-01-19 10:47:21,user_a,https://ghostbusters.slack.com/archives/C1H9RESGA/p135854651500008
2021-02-18 10:43:24,user_b,https://ghostbusters.slack.com/archives/C1H9RESGA/p135854651500009
2021-03-17 12:55:30,user_c,https://ghostbusters.slack.com/archives/C1H9RESGA/p135854651500000

その他参考

unixtimeとdatetimeを変換する(Mac/BSD編) - Qiita

Next.js で Material-UI と styled-components を使う #nextjs

概要

Next.js で Material-UI を利用しつつ、 styled-components でカスタマイズできる環境を構築した記録。

nextjs.org

material-ui.com

styled-components.com

Next.js でプロジェクトを作る

$ npx create-next-app

Material-UI を install する

$ npm install @material-ui/core

styled-components を install する

$ npm install styled-components

babel-plugin-styled-components を install する

これを入れないと Warning: Prop className did not match. とか言われる。

$ npm install --save-dev babel-plugin-styled-components

.babelrc を(なければ)作成して、設定する。

{
  "presets": ["next/babel"],
  "plugins": [["styled-components", { "ssr": true }]]
}

github.com

_app.js で優先順位を調整する

styled-components が最後に当たるようにするには、CSS injection order を設定しておくらしい。

Style Library Interoperability - Material-UI

<StylesProvider injectFirst> で Component ツリーを囲んでおく。

CssBaseline は reset.css 的なやつらしい。

import '../styles/globals.css'
import {StylesProvider} from "@material-ui/core";

function MyApp({Component, pageProps}) {
    return (
        <StylesProvider injectFirst>
            <CssBaseline/>
            <Component {...pageProps} />
        </StylesProvider>
    )
}

export default MyApp

_document.js を作成して修正する

material-ui や styled-components を Next.js で使うには、 pages/_document.js をカスタマイズする必要がある。

(なければ)ファイルを新規に作成し、どちらも公式の example があるので、参考にしながらいい感じにマージする。

material-ui/_document.js at master · mui-org/material-ui · GitHub

next.js/_document.js at master · vercel/next.js · GitHub

import React from 'react';
import Document, {Html, Head, Main, NextScript} from 'next/document';
import {ServerStyleSheets as MaterialUIStyleSheets} from '@material-ui/core/styles';
import {ServerStyleSheet as StyledComponentsStyleSheets} from "styled-components";

export default class MyDocument extends Document {
    render() {
        return (
            <Html lang="ja">
                <Head/>
                <body>
                <Main/>
                <NextScript/>
                </body>
            </Html>
        );
    }
}

MyDocument.getInitialProps = async (ctx) => {
    const materialUISheets = new MaterialUIStyleSheets()
    const styledComponentsSheets = new StyledComponentsStyleSheets()
    const originalRenderPage = ctx.renderPage

    try {
        ctx.renderPage = () =>
            originalRenderPage({
                enhanceApp: (App) => (props) => styledComponentsSheets.collectStyles(
                    materialUISheets.collect(<App {...props} />)
                ),
            })

        const initialProps = await Document.getInitialProps(ctx);

        return {
            ...initialProps,
            styles: (
                <>
                    {initialProps.styles}
                    {styledComponentsSheets.getStyleElement()}
                </>
            ),
        }
    } finally {
        styledComponentsSheets.seal()
    }
};

material-ui の Button を styled-component でカスタマイズしてみる

pages/sample.js みたいなのを作って、カスタマイズしてみる。

styled() に material-ui の Component を渡してあげれば style を上書きできる。

import React from 'react';
import styled from 'styled-components';
import Button from '@material-ui/core/Button';

const StyledButton = styled(Button)`
  background-color: red;
`;

const Sample = () => {
    return (
        <div>
            <StyledButton>Customized</StyledButton>
        </div>
    )
}

export default Sample

参考

CakePHP でカスタム Validation を追加しやすくする #cakephp

概要

CakePHP には独自に定義したカスタム Validation を使う方法がいくつかある。

Default のバリデーションプロバイダーを差し替えることで、アプリケーション全体で使うようなルールを追加しやすくする。

例えば「会員IDは 0 埋めを含む8桁の数字」のようなカスタムバリデーションを使う場合、下記のような形で呼び出せるようにする。

$validator = new AppValidator();
$validator->memberId('member_id', 'member_id は8桁の数字で指定してください');

Validation を拡張したクラスを作る

Validation を拡張して、カスタム Validation を追加するための独自クラスを作る。

Cake\Validation\Validation を拡張することで、既存のチェック処理を直接的にも間接的にも使える。

class AppValidation extends Validation
{
    /**
     * 会員IDをチェックする
     *
     * @param string $check Value to check
     * @return bool Success
     */
    public static function isMemberId($check)
    {
        // 数字8桁のチェック
        return self::custom($check, '/^\d{8}$/');
    }
}

Validator を拡張したクラスを作る

Validator を拡張して、プロキシメソッドを追加するための独自クラスを作る。

このクラスはなくてもよいのだが、カスタムバリデーションを使う際にいちいち配列を作ったりせずに済むようになるため、よく使うものには用意しておくと便利だと思う。

class AppValidator extends Validator
{
    /**
     * 会員IDのルールを追加する
     *
     * @param string $field The field you want to apply the rule to.
     * @param string|null $message The error message when the rule fails.
     * @return $this
     * @see AppValidation::isMemberId()
     */
    public function memberId($field, $message = null)
    {
        // この辺の書き方は、本家 Validator の類似メソッドを参考にするといいと思う
        $extra = array_filter(['message' => $message]);

        return $this->add(
            $field,
            'member_id',
            $extra + [
                'rule' => 'isMemberId',
            ]
        );
    }
}

デフォルトのバリデーションプロバイダーを差し替える

config/bootstrap.php でバリデーションプロバイダーを差し替えることで、アプリケーション全体に拡張した Validation が適用される。

addDefaultProvider で name を default にすることで、カスタムバリデーションでも provider の明示的な指定が不要になる。

ただし default を差し替える場合、 RulesProvider インスタンスで渡してやる必要がある。

ちなみに name を固有のものにすれば、 プロバイダーを使い分けることが可能。

※その場合、プロバイダーの名前を rule で渡してやる必要がある

// bootstrap.php でデフォルトのプロバイダーを差し替える
Validator::addDefaultProvider(
    'default', 
    new RulesProvider(AppValidation::class)
);

まとめ

  • 独自Validationを作る
  • 独自Validatorを作る
  • 独自Validationをデフォルトのバリデーションプロバイダーにする

上記を行うことで、カスタムバリデーションの追加がしやすくなる。

アプリケーション内横断で使いたいようなカスタムルールはそれなりにあると思うので、同じルールが分散するよりかは、定義する場所を確保しておくとメンテしやすいと思う。

参考

CakePHP で uploadedFile の Validation をテストする #cakephp #php

概要

CakePHP にはアップロードファイル用の validator が用意されていて便利。

ただテストをするときにちょっとめんどくさい点が2つある。

1つ目はファイルをアップロードするリクエストを作ること。

リクエストするデータとは別に $_FILES にも情報を突っ込んでおかないと、Controller で \Cake\Http\ServerRequest::getUploadedFile あたりを呼び出した時に値が取れなくなる。

もう1つは is_uploaded_file を stub にする必要があること。

uploadedFile の Validation はアップロードファイル判定に is_uploaded_file を使っており、テスト時にこの判定をくぐり抜けるのが難しい。

uploadedFile の Validation

Controller 内で↓のように書ける。便利。

$validator = new Validator();
$validator->uploadedFile(
  'my_file',
  ['types' => ['text/plain']], 
  'ファイル形式が正しくありません'
);
$errors = $validator->errors($this->request->getData());

オプションは mime type 以外にも、サイズ判定などがある。

テストで uploadedFile のリクエストを作る

uploadedFile を Controller のテスト (IntegrationTestTrait) で使う場合、 \Psr\Http\Message\UploadedFileInterface の実装インスタンスを生成してリクエストに乗せる。

\Zend\Diactoros\UploadedFile を使えばいいと思う。

このとき、リクエストデータとは別に $_FILES にもセットしておく必要がある。

直接代入してもいいのだが、 \Cake\TestSuite\IntegrationTestTrait::configRequestfiles いうキーで渡してあげると、結果的に同じことができるようだ。

$testFile = TESTS . 'Fixture' . DS . 'files' . DS . 'my_file.txt';
$uploadedFile = new UploadedFile(
    $testFile,
    10,
    UPLOAD_ERR_OK,
    'my_file.txt',
    'application/octet-stream'
);

// 別途設定する
$this->configRequest(
    [
        'files' => [
            'my_file'=> [
                'error' => $uploadedFile->getError(),
                'name' => $uploadedFile->getClientFilename(),
                'size' => $uploadedFile->getSize(),
                'tmp_name' => $testFile,
                'type' => $uploadedFile->getClientMediaType(),
            ]
        ]
    ]
);

// リクエスト
$this->post("your/api/", ['my_file' => $uploadedFile]);

ちなみにファイルの中身の確認は ext/finfo で 'tmp_name' に対して行われる。

実ファイルに対してチェックが行われるので、配置しておく必要がある。

is_uploaded_file を stub 化する

\Cake\Validation\Validation::uploadedFile が is_uploaded_file を使用しており、どうにも validation をテストで通過できなかったが、 stub 化することで対応できた。

このやり方は CakePHP 本体が同じことをやってる。

stub 用のファイルを用意して、テスト側で読み込めばOK。

// stub 側はこんな感じ
namespace Cake\Validation;

function is_uploaded_file($filename)
{
    return file_exists($filename);
}
// テスト側で stub を読み込む
require_once __DIR__ . DS . 'stubs.php';

参考

CakePHP4 と php8 の Docker コンテナを作った #cakephp #php

概要

公式 Docker Image の php:8.0-apache をベースにして、CakePHP のコンテナを作った。

DockerHub & GitHub に置いてある。

hub.docker.com

github.com

なんで作ったか

ここ最近は仕事で触る中心が CakePHP になっているので、手元で色々試せる環境をサクッと用意できるようにしておきたかったから。

あと僕は元々 php 畑じゃないこともあり、手前の理解と準備でそれなりに時間がかかったから。

OS に必要なパッケージとか php 拡張とか、設定含めた Web サーバの準備あたり。
(Web サーバについては開発だけならビルトインサーバでもいいんだけど)

突然 CakePHP 触ることになった自分みたいなケースのように、「学習の順番」をある程度柔軟にコントロールしたかったりすると役に立つかもしれない。

雑記

php:<version>-apache をベースにしている

phpApache とセットになっているイメージが公式から提供されているので、それを元にしている。

Web サーバーに強いこだわりがない(nginx 使いたいとか)がなかったのと、せっかく公式が提供しているなら使わない手はないかなと。

設定ファイルの構成とかも乗っかったりできるのが楽だった。

GitHub Actions を使ってみた

GitHub での Tag 作成をトリガーにして、 DockerHub へ反映するのに GitHub Actions を使ってみた。

DockerHub の Automated Build は正規表現でブランチ名やタグ名を引っ掛けることができるはずなんだけど、どうにもできないので、せっかくならと試してみた。

今はタグだけなんだけど、いっそ main ブランチもまとめて寄せてしまってもいいかもしれない。

それっぽい Issue は見つけた。

Automated Build Regex Doesn't Work · Issue #1831 · docker/hub-feedback · GitHub

CI もしてみた

CakePHP の実行環境ということで、新規プロジェクトを作って http アクセスできることをテストにしている

docker-cakephp/ci.yml at main · su-kun1899/docker-cakephp · GitHub

その他

KGDC Tech Conference で LT してきました #KGDC

speakerdeck.com

「息を吸うようにエラーを監視する」というタイトルで発表してきました。

イベントについて

KDDI グループ会社それぞれの技術者が集まって主催するイベントです。

グループ会社とはいえやってることはそれぞれ違うので、アドテクの話だったりインフラの話だったりE2Eの話だったりと、いい意味でバラエティに富んだ楽しいイベントだったと思います。

kgdc.connpass.com

今後継続的に開催していくようです!

LT について

本当は5分にとても収まらない色々があったw

確か入社初日の夜遅くに Slack にアラートが飛んできて、でも誰も反応してなくて、「これ大丈夫なやつですか?」とドキドキしながら聞いて回ったころからするとだいぶいい感じになりました。 (結果大丈夫なやつでスルーされてたんだけど、新参者は分からんので)

最初は一人でひたすら Issue 立てて、聞いて回って記録して、Slackの通知チャンネル整理して。

今はかなりチームっぽくなり、発生したエラーにはすべて何かしらのリアクションが 見える形で されています。

夕会やモブプロはある意味みんなで謎解きしてるみたいな楽しさもあります。

エラー監視と運用のところは自分がコネヒトに入社してから割と力を入れてきたところなので、ひとつ対外的なアウトプットにできてよかったなと思います。

「More Effective Agile」を読んだ

「CODE COMPLETE」のスティーブ・マコネルがアジャイルについて書いたということで読んでみた。

アジャイル関連の書籍はちょこちょこ読んでるんだけれども、久々に「これはいいなぁ!」と強く感じた本です。

個人的に、「アジャイル」ってエモーショナルな部分にフォーカスされがちなときがあると思っている。

一緒に考えるべき技術的な側面がどこか切り離されて表現されているように見えたり、宗教的になって「熱狂的な信者とそれ以外」のように感じてしまったり。

この本はある意味淡々と論理的に、「ソフトウェア開発をいかに効果的に行うか」という話をアジャイルのコンテキストで述べている。(タイトルそのまんまだ!)

「なぜアジャイルでやるのか」という問いに答えるためのヒントがたくさん詰まっていると思う。

非常におすすめです。