Slack キーワード通知に設定しているワード 100 連発

エゴサが趣味なので、社内の Slack のキーワード通知をめちゃくちゃ便利に使っている。

slack.com

何か見ておくと良いことがありそうなものを片っ端から通知するようにしているんだけど、100 個が上限なの知ってた?

f:id:onk:20210331222523p:plain
100 個以上設定しようとするとこうなる

僕はこんなキーワードを入れています。適当に分類しながら見てみよう。

自分

onk, o/nk, on/k, おんく, 大仲, onaka, 眼科

自分の名前やハンドルネームを入れておくと、どこかで自分が呼ばれたときに気づけるようになる。

定時外だと親切でキーワード通知避けのために / を入れて発言する人もいるので、それも通知させるためにこういうキーワードになっています。戸籍ネームはほっっとんど呼ばれることは無いんだけど、念のため。

「眼科」は中心性漿液性網脈絡膜症になってるっぽいけど放置してたら T シャツが作られたので、戒めのためにずっと通知しています。

suzuri.jp

チーム

自分が所属しているチーム名だったり、プロダクトのコードネームだったり、サブ会 名だったり。

他のチームから言及されたときにすぐに気づける用。

サービス

小説, ノベル, カクヨム, kakuyomu, 魔法, maho, らんど, ランド

カクヨム魔法のiらんどを運営しているチームに居るので、サービスに関連しそうな話題が出たときに気づけるように。

障害検知

CRITICAL, 障害, 不具合, 負荷, 脆弱性, 重い, 重く, Incident, 申し送り

障害っぽかったら通知が来るようにしてある。

「申し送り」は障害対応後とかに使われることが多いワードですね。「重い」は月初に勤怠管理システムに対しての発言をよく見る。

インフラ系

infra, インフラ, SRE, SLA, SLO, SLI, 監視, PWG

インフラっぽい話題が出たら通知が来るようにしてある。

「PWG」は Performance Working Group の略で、パフォーマンス等が悪くなっていないかを定期的に (主に月次開催) チェックする会です。サービスのレスポンス速度やエラー率、発報されたアラートや障害などを3ヶ月ぐらいの目線で確認し、キャパシティプランニングを行ったり、監視設定や根本対策に思いを馳せたりします。

はてなにおける日々の仕事の中にあらわれるMackerelの活用 - Mackerel ブログ #mackerelio でも紹介されていますね。当時とは組織体制はかなり変わっていますが、目的はあまり変わっていません。

mackerel.io

コミュニケーション

(グループウェアのドメイン), 議事録

bot の発言ではなく、人間が URL を貼ったなら、何かしら人に伝えたい意図が含まれた面白い記事なんだろうってところから。実際、キャッチアップしておくと便利な記事がよく流れてきます。

「議事録」は割と興味本意かな。社内のミーティングでは「議事録」ってワードはあまり出ないので、他社さんとの会話が見えることが多い。

マネージャ仕草

cost, コスト, 予実, 予算, 実績, 費用, 数字, 稟議, 申請, release, リリース, キックオフ

はい。

他部署のものでも数字に敏感になっておくと色んな場面で役に立つし、キックオフやリリース時点で話題をキャッチできていると、これも会話の種になる。「デプロイ」はさすがに流速がヤバかったのでキーワードから外した。

マネージャ仕草 人事編

全社, ミッション, ビジョン, バリュー, MVV, 目標, 評価, グレード, 昇格, 昇給, 1on1, 面接, 面談, 異動, 採用, 新卒, インターン, CTO, チーフ, シニア, リーダー, メンター, メンティー, マネージャ, 方針, オンボーディング

1on1 した感想とか、目標設定や評価に対しての感想とかを拾っておきたい。

ふりかえり

ふり返り, 振り返り, 振りかえり, ふりかえり, KPT, YWT

ふりかえりだけでも見ておくとチームの現状が分かるので、覗きに行ってます。

技術

MySQL, RDS, Aurora, redis, Elasticache, Elasticsearch, ruby, rb, rails, レイルズ, レールズ, アジャイル, scrum, スクラム

この辺のワードが出たらいっちょかみしたい技術ワード。

こんなに登録していて仕事できるの

かろうじてなんとかなってるんじゃないかな……。

集中したいときはガン無視しているので、呼ばれても返事してないし、何ならときどきミーティングすっぽかしてる orz

Gemfile の Dual Boot 方法

Gemfile 内で切り替える

こういう ENV で切り替えるやつとか、eval_gemfile を使うやつとか。

https://github.com/discourse/discourse/blob/v2.6.2/Gemfile#L9-L16

def rails_master?
  ENV["RAILS_MASTER"] == '1'
end

if rails_master?
  gem 'arel', git: 'https://github.com/rails/arel.git'
  gem 'rails', git: 'https://github.com/rails/rails.git'
else
  # NOTE: Until rubygems gives us optional dependencies we are stuck with this needing to be explicit
  # this allows us to include the bits of rails we use without pieces we do not.
  #
  # To issue a rails update bump the version number here
  gem 'actionmailer', '6.0.3.3'
  gem 'actionpack', '6.0.3.3'
  gem 'actionview', '6.0.3.3'
  gem 'activemodel', '6.0.3.3'
  gem 'activerecord', '6.0.3.3'
  gem 'activesupport', '6.0.3.3'
  gem 'railties', '6.0.3.3'
  gem 'sprockets-rails'
end

Gemfile.lock に差分が出るので、アプリケーション (Gemfile.lock をリポジトリに含める) の場合はイマイチだと思う。

ライブラリの場合は Gemfile.lock は含めないことの方が多いので、大いにやると良さそう。マトリックステストも ENV を置くだけなので簡単。

eval_gemfile と言えば dry-rb や thredded 等で、開発用の gem を別ファイルに追い出している文化も面白いよね。(余談です)

これも余談だけど、一時期、Gemfile.local を読み込ませるために

eval_gemfile(__FILE__ + ".local") if File.exist?(__FILE__ + ".local")

と書くのが流行っていた時期もあって、これも Gemfile.lock に差分が出るので僕は苦手だったなって記憶が蘇ってきた。

各環境の Gemfile を用意する

各環境用の Gemfile を用意して、 BUNDLE_GEMFILE 環境変数を設定する。これも gem でよく見る。

# gemfiles/rails_5_2.gemfile
source "https://rubygems.org"

gem "rails", "~> 5.2.0"
gemspec path: '../'
# .github/workflows/main.yml
strategy:
  matrix:
    gemfile:
      - rails_5_2
      - rails_6_0
      - rails_6_1
env:
  BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile

gem は基本的に gemspec に依存が書かれていて、一部の指定が厳しい部分だけ *.gemfile で上書きすれば良いので、この形が自然に取れる。

アプリケーションの場合は eval_gemfile を利用して、共通部分は Gemfile.common とか Gemfile.global とかに抽出して同じことをやる。

3 ファイル (current 用, next 用, common) 必要なことと、common に何を追い出すかを考えるのがめんどくさいことがイマイチ。

Gemfile.next の symlink を置いて切り替える

RailsConf 2018 で発表されていた方法。

speakerdeck.com

僕は Getting Ready for Rails 6.0: How to Dual Boot - FastRuby.io | Rails Upgrade Service に書いてあるので知った。

fastruby.io

Gemfile に next? を定義して、symlink 経由で呼ばれたかどうかで切り替える。

def next?
  File.basename(__FILE__) == "Gemfile.next"
end

if next?
  gem 'rails', git: 'https://github.com/rails/rails.git', branch: 'main'
else
  gem 'rails', '~> 6.1.0'
end
  • next? 用の lock ファイルは Gemfile.next.lock になる
    • 差分が別ファイルになるのでコミットできる
  • Gemfile だけで一元管理できる

という、上であげた 2 つの方法のデメリットをそれぞれ潰すことができている。

Shopify/bootboot

id:takkan_m が教えてくれた。

https://github.com/Shopify/bootboot

symlink を使わず、Bundler の plugin として実装したもの。

if ENV['DEPENDENCIES_NEXT']
  ...
end

の内容が Gemfile_next.lock に書き出される。

アプリ内の日付変更線をズラす系の実装

例えば日記を書くときに、午前 2 時に書いたものは前日分としたいことがある。またユーザがメチャクチャ多いサービスでは、0:00 を回ったら翌日のログインボーナスを配る、としていると、まだユーザが多い時間にサーバの処理が要求されて大変なので、28:00 を日付変更線にしたいことがある。

こういうときには

module AppTime
  def self.beginning_of_day(time)
    t = time.change(hour: 4)
    t <= time ? t : t - 1.day
  end
end

を作って、

AppTime.beginning_of_day(Time.current)

を使うと「アプリ内の日付変更線では何日なのか」が取れる。

# 02:00 は前日扱い
time = Time.zone.parse("2021-01-31 02:00")
AppTime.beginning_of_day(time)
# => Sat, 30 Jan 2021 04:00:00.000000000 JST +09:00

僕は Date 苦手なので何でも Time で処理したい派だけど、こういうメソッドがあっても便利だと思う。

module AppTime
  def self.today
    to_date(Time.current)
  end

  def self.to_date(time)
    beginning_of_day(time).to_date
  end
end
# 02:00 は前日扱い
time = Time.zone.parse("2021-01-31 02:00")
AppTime.to_date(time)
=> Sat, 30 Jan 2021

また、「今日日記を書いたか?」や「今日ログインボーナスを受け取ったか?」を以下のようなメソッドを用意することで表現できる。

module AppTime
  # AppTime.all_day(Time.current) は以下の Range を返す
  # => Sun, 31 Jan 2021 04:00:00.000000000 JST +09:00..Mon, 01 Feb 2021 03:59:59.999999999 JST +09:00
  def self.all_day(time)
    beginning_of_day(time)..end_of_day(time)
  end

  def self.end_of_day(time)
    (beginning_of_day(time) + 1.day - 1.second).change(usec: Rational(999999999, 1000))
  end
end
class User < ApplicationRecord
  has_many :diaries
end

class Diary < ApplicationRecord
  belongs_to :user
  scope :of_day, ->(time) { where(published_at: AppTime.all_day(time)) }
end
user.diaries.of_day(Time.current).exists?
# Dairy Exists? (2.2ms)  SELECT 1 AS one FROM `diaries` WHERE `diaries`.`user_id` = 1 AND `diaries`.`published_at` BETWEEN '2021-01-30 19:00:00' AND '2021-01-31 18:59:59' LIMIT 1

肝は「DB には UTC で保存して、アプリ内では JST で扱う、というのは変えない」「日付変更線に沿った Time や Range を返すモジュールを用意することで、保存時に 04:00 を入れるし、検索時には 04:00〜04:00 (UTC では 19:00〜19:00) で検索する」です。

アプリケーション側で変換するのだと限界もあって、日記だと Habit Tracker っぽい (いわゆる「草」) 見せ方をしたいし、その実装に groupdate gem を使うことが多いと思うけど、そういうのはアプリ内日付変更線に基づいた日付カラムを DB に入れてしまうと楽だと思います。

エクスキューズ

ここから始めてリファクタリングして育てていきましょう、です。 この記事ではモジュールにしたけど、クラスにしてアプリ内日付変更線を持ったインスタンスを作っていくこともあるだろう。

all_day 難しい

ActiveSupport の実装に寄せて end_of_dayall_day03:59:59.999999999 を返して書いたけど、時間を区切るというのは半開区間でないと困るんですよ。

↑↑でも

`diaries`.`published_at` BETWEEN '2021-01-30 19:00:00' AND '2021-01-31 18:59:59'

SQL として見えてるけど、BETWEEN では表現したくなくて、

WHERE '2021-01-30 19:00:00' <= published_at AND published_at < '2021-01-31 19:00:00'

であって欲しい。

時間は連続なので、切断点はどちらかのみに所属しなければならない=閉区間だと表現できないのだ。詳しくはデデキントカットを見てくれ。何故かニコニコ大百科が詳しい。

dic.nicovideo.jp

じゃあどう実装すれば良いかで言うと、終端を含まない ... で作られた Range を渡すといい感じになります。

module AppTime
  def self.all_day(time)
    beginning_of_day(time)...beginning_of_day(time + 1.day)
  end
end
user.diaries.of_day(Time.current).exists?
# Diary Exists? (2.0ms)  SELECT 1 AS one FROM `diaries` WHERE `diaries`.`user_id` = 1 AND `diaries`.`published_at` >= '2021-01-30 19:00:00' AND `diaries`.`published_at` < '2021-01-31 19:00:00' LIMIT 1

おうちのスマートホーム化メモ

f:id:onk:20210130180604p:plain

現状

各部屋のグッズはこんなモン。

  1. 玄関
    • 天井に人感センサー電球
    • 外には Netatmo Weather の室外モジュール
  2. 寝室
  3. 仕事部屋
  4. LDK

以上で

  • 照明
    • Philips Hue、Nature Remo、SwithBot 指ロボットを駆使して、すべて声が届くようになった
    • ほとんど Google Next Mini/Hub 経由で声で操作している
      • 物理スイッチの場所に移動する必要が無いのは Life Changing
  • エアコン
    • 付属のリモコンは収納の奥にしまって、スマートスピーカー経由か、スマホの Nature Remo アプリ経由で操作している
      • 最高に便利。リモコン探さなくて良いし、布団の中から操作できるのは Life Changing
    • Nature Remo で測った温度を Mackerel に送ることで、付いているかどうかをいつでも把握できるようになった
  • ホットカーペット
    • スマートスピーカー&スマートプラグ経由で操作している
      • 「ねぇグーグル、カーペット消して」
    • だいたい手を伸ばした方が早いが、布団に入った後に声で消せるのはかなり便利
  • テレビ
    • 「ねぇグーグル、おやすみ」で以下が発動するようにしている

とそれぞれを操作するようになった。

特に照明とエアコンは、スマホ or 声で操作できない状態には絶対に戻りたくないぐらい便利で良い。

ルーティンで操作するのもピタゴラスイッチを眺めているかのような気持ち良さがある。発話の必要すらなく全自動になるともっと便利なんだが、例えばベッドに感圧センサー仕込むとか、スマートウォッチの睡眠をトリガーにするとかは独り暮らしじゃないと上手くいかない。。2 人以上の生活空間では人感センサーは無価値だと思う。

次にやっておきたいこと

  • Philips Hue のシーンをスマートスピーカー経由で部屋ごとに設定したい
    • 「ねぇグーグル、月の光に設定して」と言うと 2 部屋とも暗くなる
    • 細かく調整するときはスマホ開いてる
  • 寝室、仕事部屋にスマートスピーカーが 1 つしか無い
    • 寝室の Google Nest Mini のみで操作しているので、仕事部屋のものを操作するのは少し面倒
      • 「ねぇグーグル、仕事部屋の照明を消して」的な操作になる
    • 部屋ごとにスマートスピーカーある状態にする方が自然だよなぁ。買い足すか。とはいえ置く場所とケーブル周りが……。壁に埋め込みたい
  • スマートロックもやりたかったんだけど、新型コロナウイルスの影響で外出すること自体が減ったのでメリットが少ない
    • GPS 連動で家の何かを弄ることも無い
  • タイマー等を用いて何かをプログラミングすることは今のところ需要がない……?
    • 規則正しい生活をしていない問題がな。。
  • 隣の部屋との会話をスマートスピーカー経由で行うのは便利そうだけど、まだ使いこなしていない
    • 移動してノックするよりも声かけやすい、はず
  • Wi-Fi を必要とする子が多すぎ問題
    • 30 台弱が 1 AP にぶら下がっている
    • そろそろメッシュ Wi-Fi 導入するか……?

センサー

Nature Remo 3 には温度、湿度、人感、照度センサーが付いているので、せっかくだし有効活用したいよね。

ということで雑に Mackerel に投げている。Lambda を EventBridge でスケジュール実行。

値を取得しだしたら CO2 濃度も測りたくなったので Netatmo WeatherStation も購入した。

f:id:onk:20210130224556p:plain

CO2 濃度も、気圧変化も、アラートだけは投げてるんだけど自分の体は鈍感なのでそもそも影響が分かっていないが、測ること自体が目的なのでそれでいいのだ。GARMIN の body battery をライフログとして投げているので、そのうち突き合わせて眺めてみよう。

今年買ったもの2020

去年 に引き続き、今年買ったものコーナー。

はてなに入社して明らかに変わったのが「日常をブログに残すようになった」ことで、その中でも「今年買ってよかったもの」はついタグを追ってしまうし追ったら買っちゃうし経済がどんどん回ってしまう。よくない。

だいたい買った順です。

といった感じで現在の机はこんな。

f:id:onk:20201231143058j:plain
まったく飾らずに今写真撮った。缶コーヒーは正月の巣籠もり用

机周りが一通り揃った (モニタ、椅子、キーボード、マウス) ので、来年は IYH することは減るはず、きっと、多分。

って去年言ったけど、モニタも椅子もキーボードもマウスも買ったね。そしてスマートスピーカーを起点にリモコンが全部発話で動かせるように変わった一年だった。

GitHub の新規リポジトリ作成時にサジェストされる名前

GitHub で新規リポジトリを作ろうとすると、リポジトリ名をサジェストされる。

f:id:onk:20201229005510p:plain
bug-free-robot めっちょ良い名前を引いた

docker contianer の命名規則を思い出した。

deeeet.com

せっかくなのでどんな名前が出てくるのか集めてみよう。HTML に含まれているので、スクレイピングの出番。

while :; do
  curl -H 'Cookie: user_session=xxxx' 'https://github.com/new' | ggrep -oP '(?<=<strong class="reponame-suggestion js-reponame-suggestion">).+(?=</strong>)'
  sleep 10
done

grep -oPワンライナーの頻出テクニック *1Macgrep-P に対応していないので brew install grep して使う。

これを動かすと以下のような名前が取れる。

didactic-engine
expert-potato
solid-giggle
furry-succotash
supreme-doodle
psychic-guacamole
crispy-doodle
probable-fiesta
sturdy-journey
stunning-octo-journey
glowing-journey
fuzzy-barnacle
laughing-octo-funicular
upgraded-parakeet
jubilant-engine
psychic-goggles
sturdy-octo-couscous
crispy-couscous
psychic-potato
congenial-sniffle

xxx(-yyy)*-zzz で構成されそう。

xxx の部分は super, upgraded, solid, shiny, fuzzy といった形容詞が並ぶ。

(-yyy)* の部分はほとんど octo。時々 palm, rotary, computing, duper, free。稀に super-octo-palm-tree のように 2 個出てくる。

zzz の部分は tree, phone, umbrella, machine, enigma 等、名詞っぽい。 特に何かに関連する名詞かどうかは分かんなかった。

意外と使われている。

ところで何のためにリポジトリを作ろうと思ってたんだっけ……。


300 件ほど集めたのでもうちょっと取れたデータ眺めてみたけど、bug-free は xxx-yyy ではなくこれで xxx のようだ。まぁ bug だけで接頭語にするとバグ量産してしまいそうだしね。また computing-machinepalm-treerotary-phone もこの組み合わせしか無いので yyy-zzz ではなく zzz 側っぽい。

すると yyy で octo 以外はあと duper だけなんだけど、これも supersuper-duper がそれぞれ xxx にありそうだ。

というわけで xxx(-octo)?-zzz という生成ルールなのではないか。

いやそれが分かったから何だって話だけど。

クエリパラメータのデリミタに ; を使うこともできる

本記事は、はてなエンジニア Advent Calendar 2020 の 18 日目の記事です。昨日は id:YaaMaa さんでした。

yaamaa-memo.hatenablog.com

社内チャットではこの話で盛り上がったときにトライ木も作られており、良い頭の体操になっていました。


さて、本題。

Hatena::Let を眺めていて、こんな URL に気づいた。

http://let.st-hatelabo.com/onk/let.iframe?code_id=g5G0uOeEqfcA;key=

クエリパラメータにセミコロン……!

パッと考えるとこれは

{
  code_id => "g5G0uOeEqfcA;key="
}

となりそうで、というか Ruby で実際にパースするとそうなる。

uri = URI("http://let.st-hatelabo.com/onk/let.iframe?code_id=g5G0uOeEqfcA;key=")
URI.decode_www_form(uri.query)
#=> [["code_id", "g5G0uOeEqfcA;key="]]

「こんなカッコ良くもない URL で独自規格作らないでよ」が最初の感想だったんだけど、ソースコードを読むと普通に $r->req->param('code_id')$r->req->param('key') で取得している。

param の実装である Plack::Request::_query_parameters を見に行くと

sub _query_parameters {
    my $self = shift;
    $self->env->{'plack.request.query_parameters'} ||= parse_urlencoded_arrayref($self->env->{'QUERY_STRING'});
}

となっていて、WWW::Form::UrlEncoded::parse_urlencoded_arrayref がパーサ。パーサの実装は

if ( src[i] == '&' || src[i] == ';') {

https://github.com/kazeburo/WWW-Form-UrlEncoded-XS/blob/0.26/lib/WWW/Form/UrlEncoded/XS.xs#L246

&; の両方をクエリパラメータのデリミタとしているし、テストコードにも含まれている!

'a=b&c=d'     => ["a","b","c","d"]
'a=b;c=d'     => ["a","b","c","d"]
'a=1&b=2;c=3' => ["a","1","b","2","c","3"]
'a==b&c==d'   => ["a","=b","c","=d"]
'a=b& c=d'    => ["a","b","c","d"]
'a=b; c=d'    => ["a","b","c","d"]
'a=b; c =d'   => ["a","b","c ","d"]
'a=b;c= d '   => ["a","b","c"," d "]
'a=b&+c=d'    => ["a","b"," c","d"]
'a=b&+c+=d'   => ["a","b"," c ","d"]
'a=b&c=+d+'   => ["a","b","c"," d "]
'a=b&%20c=d'  => ["a","b"," c","d"]
'a=b&%20c%20=d' => ["a","b"," c ","d"]
'a=b&c=%20d%20' => ["a","b","c"," d "]
'a&c=d'       => ["a","","c","d"]
'a=b&=d'      => ["a","b","","d"]
'a=b&='       => ["a","b","",""]
'a=&'         => ["a","","",""]
'&'           => ["","","",""]
'='           => ["",""]
''            => []

https://github.com/kazeburo/WWW-Form-UrlEncoded-XS/blob/0.26/t/01_parse.t#L26

ふえぇぇって思ってググるQUERY_STRING 中のパラメータの区切りは必ずしも '&' ではない - 理系学生日記 に行き着いて、

We recommend that HTTP server implementors, and in particular, CGI implementors support the use of ";" in place of "&" to save authors the trouble of escaping "&" characters in this manner.

https://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.2.2

という記述を見つけた。

えーーじゃあ Ruby でさっきダメだったのは何!!!って話になったのでまず Rack を見に行ったら id:Nyoho さんが 2015 年に PR 出していてW3C の同じ記述を参照しながら

Fix to use semicolons as separators for GET not for POST.

と GET リクエストのときのパース時のみ ; でも分割できるように修正している。当時 tDiary にも Issue が上がっていて

http://www.tamoot.net/d/?year=2015;month=1Q;category=Ruby

という URL がパースできなくなったトノコト。なるほど、tDiary にもセミコロン区切りがあったのね。CGI 時代の歴史的経緯っぽい香りがする。

閑話休題。「Rack は通すんじゃん」って思いつつ URI.decode_www_form の実装を見に行くと

# This refers http://url.spec.whatwg.org/#concept-urlencoded-parser,
# so this supports only &-separator, and doesn't support ;-separator.

https://github.com/ruby/ruby/blob/v2_7_2/lib/uri/common.rb#L443-L444

とわざわざ「; は対応しないよ」ってコメントが書いてある。

更に blame すると

lib/uri/common.rb (URI.decode_www_form): follow current URL Standard.

- # This refers http://www.w3.org/TR/html5/forms.html#url-encoded-form-data
+ # This refers http://url.spec.whatwg.org/#concept-urlencoded-parser ,
+ # so this supports only &-separator, don't support ;-separator.

https://github.com/ruby/ruby/commit/4a50d44

とのことで、 WHATWGapplication/x-www-form-urlencoded の仕様に則ったらしい。

http://url.spec.whatwg.org/#concept-urlencoded-parser

こちらでは & のみがデリミタと定義されている。

というわけで、application/x-www-form-urlencoded で POST したときは仕様があるけど、URL では query string のパース方法までは決まっていないんじゃないかなぁ。*1

; を使っても良いと解釈できずに URL Escape してしまうクローラがいる等の弊害もあり、今となっては敢えて使う必要はなさそう *2 なのでただの雑学ですが、最近見た 10 年モノの面白コードの話でした。


はてなエンジニア Advent Calendar 2020、明日 19 日目は id:Pasta-K さんです。

*1:この辺りはまだ調べ切れてないんだけど、RFC 3986 には書いてなかった

*2:なので直しました