とんちゃんといっしょ

Cloudに関する技術とか日常とかについて書いたり書かなかったり

Slackで発言していない人をチャンネルからkickするスクリプト

Slackで発言していない人をチャンネルからkickするスクリプトを雑に書いたので置いておく。

動作としては

  1. 対象のチャンネルのメンバを取得
  2. 発言者のリストを取得(今回はBigQueryからUser IDを抽出してきたファイルを利用[下記参照])
  3. 1と2の差分から未発言のユーザリストを作成
  4. 3で作成したユーザリストのメンバを対象チャンネルからkick
require 'json'
require 'slack-ruby-client'

Slack::Web::Client.configure do |config|
  config.token = ENV['SLACK_API_TOKEN']
  raise 'Missing ENV[SLACK_API_TOKEN]!' unless config.token

  STDOUT.sync = true

  config.logger = Logger.new(STDOUT)
  config.logger.level = Logger::INFO
end

CHANNEL_ID = '' # TARGET_CHANNEL_ID
FILE_NAME  = '' # DATA_FILE_PATH

client = Slack::Web::Client.new
res = client.channels_info(channel: CHANNEL_ID)
channel_members = res['channel']['members']

post_members = open(FILE_NAME) do |data|
  members = JSON.load(data)
  members.map{|member| member['user']}
end

kick_members = channel_members - post_members
kick_members.each do |member|
  user = client.users_info(user: member)
  client.channels_kick(channel: CHANNEL_ID, user: member)
end
[
  {
    "user": "U0331B9R5",
  },
  {
    "user": "UEFRP7DU5",
  },
  ...
  {
    "user": "U0DDUMYM7",
  },
  {
    "user": "U3YJ3FWC9",
  }
]

本当は発言している人のリストをBigQueryから直接とってこればよかったのだが、 今回は雑にファイルに落とし込んだものを利用してみた。

※発言者のリストを作るための処理はこっちを参照。 mahito.hatenablog.com

無事にチャンネルのお掃除ができたのでよし!

Slackチームつなげるワームホールを開発した話

この記事はSlack Advent Calendar 2018の8日目の記事です。

TL;DR

  • Slack有料版のゲスト機能相当を無料で実現する、複数Slackチーム間のチャンネルをつなげるプログラムを開発(下図参照)
  • 環境は紆余曲折を経て現在はGCP上でGCE, Cloud PubSub, Cloud Datastore, docker, slack-ruby-client, Slack RTM APIを使って実現
  • 使い方はレポジトリのREADMEを参照
  • 今後も機能拡張や安定稼働に向けて開発は続けていくつもりだけど実装ペースは気分次第

f:id:mazinlabs:20181204155652p:plainf:id:mazinlabs:20181204155726p:plain
左:Slackチーム A、右:Slackチーム B

レポジトリ

github.com

背景

とあるグループ会社に所属するエンジニア有志のSlackがあり、 グループ会社を卒業するとそのSlackチームも卒業し、OB/OGのSlackチームに移動する決まりになっている。

しかしながら、卒業した人から「困ったときに聞ける人がいるのが良かった」や「情報交換の機会が減った」などという声が上がっていたため、なにか方法がないかと検討したが

私「あるWorkspaceの特定のチャンネルにだけ別Workspaceの人達を入れたいな!」

Slack「できるよ!お金払えば!!」

  • とあるSlackチームのユーザ数(2018/12/4時点):772人
  • 現在のアクティブユーザ数(2018/12/4時点): 396人
  • Slackのスタンダードプランの1userあたりのお値段: 850円 / 月

f:id:mazinlabs:20181204151227p:plain
とあるSlackチームが課金する場合の額

払えるか!!!

というわけで、無料で有料相当の機能を実現すべくSlack APIを使ったメッセージ転送に思い至る。

コンセプト

「あるSlackチームの特定のチャンネルで発言したものが、別のチームの同じチャンネルに転送されて会話ができる」

これだけです。

某Slackチームには #blackhole という発言が一定時間で消えていく人気チャンネルがあるので、2点間をつなぐ、つまりwormholeだなと安直な感じで名前をつけてみた。なおwikipediaで見ても大体あってる。

ワームホール (wormhole) は、時空構造の位相幾何学として考えうる構造の一つで、時空のある一点から別の離れた一点へと直結する空間領域でトンネルのような抜け道である。 ワームホール - Wikipedia

ただ、2点間ではなく複数Slackチームがつながるような設計にしてあったりする。

現在実装されている機能

  • メッセージ処理(CRUD)の転送
  • リアクション転送(文字として転送される)
  • Thread対応

実装されていない機能

  • ファイル転送
  • join / left を検知した通知
  • member listの共有

実装1

最初期の実装は以下を使って実装していた

このときのメッセージ転送の設計こうなっていたらしい

f:id:mazinlabs:20181204160426p:plain
wormholeの初期のメッセージング設計

雑な発表資料

speakerdeck.com

レポジトリ

github.com

当初はBOTの名前とアイコンで転送していたのだが、postMessage APIにユーザ名やアイコンを指定できることがわかったので、発言したユーザの名前とアイコンを使うようにしたことで一気に使いやすくなった。 そして当初うまく動いているように見えたのだが、数日ごとにRabbitMQのプロセスが張り付いて再起動を余儀なくされることに。。。

RabbitMQなんて捨ててやる!時代はマネージドなメッセージングサービスだという思いにいたりCloudnを捨ててマネージドなメッセージングサービスかつ無料で使えそうなGCPにお引越しをして実装2に移行。

実装2

GCP上で以下を使って実装

  • Hubot(CoffeeScript)
  • Docker
  • Cloud Compute Engine
  • Cloud PubSub
  • Cloud Datastore
  • Cloud Build
  • Cloud Container Registry
  • Slack RTM API

f:id:mazinlabs:20181204163142p:plain
実装2と実装4のメッセージングの設計

Githubのレポジトリを更新するとCloud Buildが動いてContainer Registryにコンテナを登録してくれるように。 RTM APIで受けたメッセージをCloud PubSubにPublishし、wormholeとして接続される側がそれをSubscribeする。 Cloud Datastoreでメタデータを管理し、Postされたメッセージが更新、削除された際にその変更削除を転送先に伝搬させる際に使われる。 だいたい$1/月ぐらいで運用できている。

github.com

これはかなり安定して動かせていたものの、やはり時おりHubotが謎のエラーで落ちる。 あとCofeeScriptで書いていたので書きづらい。 毎度コンテナを復旧させるのが面倒なので、リクエストに応じて処理をするEvent drivenなCloud Functionsのほうが良いのではと思い実装3を検討

実装3

  • Nodejs
  • Cloud PubSub
  • Cloud Datastore
  • Cloud Functions

f:id:mazinlabs:20181204163740p:plain
実装3のメッセージング設計

当時はまだCloud FunctionsにPythonが出てきていなかったのでNodeで書いてみるものの、 Functionごとの環境変数の設定や同じ関数名が動かせない(といううろ覚えの記憶)と、 やりたいことができないのでCoud Functionsを断念。(コードもレポジトリ管理する前に見切った)

結果として自分が得意なRubyにしたほうが一から実装してもあとの機能追加がきっと楽だと思って実装そのものを乗り換えることに。

実装4

現在動かしているのがこのバージョン

  • slack-ruby-client (Ruby)
  • Docker
  • Cloud Compute Engine
  • Cloud PubSub
  • Cloud Datastore
  • Cloud Build
  • Cloud Container Registry
  • Slack RTM API

github.com

メッセージングの設計自体は実装2からさほぼ変更はなし。

slack-ruby-clientのRTM clientが定期的にログも出さずにコネクションが切断されるという問題(下記リンク参照)があったものの、エラーハンドリングなどで対応して現在安定稼働中。

Slack-side disconnects · Issue #208 · slack-ruby/slack-ruby-client · GitHub

ただ、やはりRTM clientの動作が不安定なのとCloud Functionsの機能追加などで実装3のEvent APIベースに戻そうかと悩んでいるところ。

今後の予定

直近はreactionのremoveの挙動が多分不思議な事になってるのでそれの修正をするが、 slack-wormholeのissuesにあげてることぐらいはやりたいなと思ってる。

もし使う人がいてバグ報告や要望があれば暇を見て実装したり直したりしたいところ。

まとめ

紆余曲折を経たもののSlackチーム間をSlack APIを使って無料でつなぐプログラムを開発した。

今後も改良や修正は続けていくつもりなので利用者からのフィードバックお待ちしております!

Herokuでの文字化けを直してみた

FluentdのPluginを書こうかなと思って公式HPのプラグイン作成を解説しているページを見たところ、サンプルコード内の日本語が文字化けしてた。

 

プラグインを書く | Fluentd(現在はPR出して修正済み)

 

どうも調べてみると、Herokuの上でCodeRayを使うと文字化けしてしまうらしい。

 

Herokuにデプロイすると、CodeRayでソースコードの日本語が文字化けする - SKKTM Lab Blog

 

ぐぐってみても日本語で直し方を書いてる人がいなかったので、調べて直してみたのでそのメモ。

 

ちなみに先に結論だけ述べておくと

  1. .buildpacksにRubyとlibxml2のbuildpackのURLを記載
  2. Gemfile.lockのnokogiriを1.6以上にする
  3. HerokuのConfigにBUILDPACK_URLにheroku-buildpack-multiを指定
  4. デプロイ

やり方はPRを見ていただければ何となくわかると思う。

update libxml2 and nokogiri to fix japanese comment rendering on heroku by Mahito · Pull Request #119 · fluent/fluentd-docs · GitHub

 

で、調べてから結論に至るまでは以下のとおり。

 

  1. CodeRayのソースコードを読む

    coderay/lib/coderay at master · rubychan/coderay · GitHub

    しかしどうもCodeRayが原因じゃないので別の原因を調べてみる

  2. rack-codehighligherを調べる

    wbzyl/rack-codehighlighter · GitHub

    fluentd-docの中でCoderayが直接呼ばれてるような形跡がなかったので調べてみたところ、rack-codehighligerが関係していたので、Mac上のローカル環境でデバッグするも再現しないのでHeroku上でデバッグ
    すごい時間がかかったがどうやら中でNokogiriがパースしてるところで文字化けが起きてる模様

    https://github.com/wbzyl/rack-codehighlighter/blob/64bf50520cb1d563432832ae30d85bbd94cec5bd/lib/rack/codehighlighter.rb#L40

  3. Nokogiriとlibxml2を調べる
    Nokogiriのバグっぽいということで追いかけていったが最終的にlibxml2に渡して帰ってきたらバグってることまでは突き止めたので同じ事例がないか調べてみたところこちらでヒット

    ruby on rails - Nokogiri adds characters during parsing on Heroku - Stack Overflow

    原因はHerokuのlibxml2がバグのあるバージョンを使っていることらしい。

  4. Nokogiriのバージョンを上げる
    Fluentd-docはNokogiriの1.5が使われていたが、1.6にするとNokogiriのインストール時にlibxml2を独自にビルドして使うことができるようになったのでbundle update nokogiriをして解決!
    ・・・と思ったらHeroku上でlibxml2がビルドされた形跡がなく問題解決に至らず

  5. buildpackでlibxml2をバージョンアップ
    buildpackをつかえばlibxml2のバージョンがあげられるということでやってみた。
    ・・・アプリが動かなくなったのでこれもボツ

  6. heroku-buidpack-multiを利用してRubyとlibxml2を導入してnokogiriをバージョンアップ
    PaaSといえば個人的にはこの人ということで@jacopenさんに相談したところ、libxml2を入れただけだとRubyが動かないのでbuildpack-multiでRubyとlibxml2を入れないとダメと教えていただいたのでその通りやってみたらようやくサンプルコードの日本語の文字化けが治った。

というわけで、1日ぐらいかかったけど問題も無事に解決できてPRをだしてマージ してもらって問題解決に貢献できた。

そしてこれを記事にしておいたので今後ググればこの記事がヒットして問題解決につながればと思う次第。

 

Herokuさんlibxml2のバージョン上げませんかね・・・?

shared_examples_forが便利

RSpecのテストをグルーピングして実行できる#shared_examples_forが便利。

Shared example group - Example groups - RSpec Core - RSpec - Relish

これを使えばテストコードが簡潔になるのでテストコードの量を減らすことができる。

require "set"

shared_examples_for "a collection" do
  let(:collection) { described_class.new([7, 2, 4]) }

  context "initialized with 3 items" do
    it "says it has three items" do
      collection.size.should eq(3)
    end
  end

  describe "#include?" do
    context "with an an item that is in the collection" do
      it "returns true" do
        collection.include?(7).should be_true
      end
    end

    context "with an an item that is not in the collection" do
      it "returns false" do
        collection.include?(9).should be_false
      end
    end
  end
end

describe Array do
  it_behaves_like "a collection"
end

describe Set do
  it_behaves_like "a collection"
end

ブロックを渡してやることもできるしパラメータを渡してやることもできる。

require "set"

shared_examples_for "a collection" do
  let(:collection) { described_class.new([7, 2, 4]) }

  context "initialized with 3 items" do
    it "says it has three items" do
      collection.size.should eq(3)
    end
  end

  describe "#include?" do
    context "with an an item that is in the collection" do
      it "returns true" do
        collection.include?(7).should be_true
      end
    end

    context "with an an item that is not in the collection" do
      it "returns false" do
        collection.include?(9).should be_false
      end
    end
  end
end

describe Array do
  it_behaves_like "a collection"
end

describe Set do
  it_behaves_like "a collection"
end
require "set"

shared_examples_for "a collection object" do
  describe "<<" do
    it "adds objects to the end of the collection" do
      collection << 1
      collection << 2
      collection.to_a.should eq([1,2])
    end
  end
end

describe Array do
  it_should_behave_like "a collection object" do
    let(:collection) { Array.new }
  end
end

describe Set do
  it_should_behave_like "a collection object" do
    let(:collection) { Set.new }
  end
end

で、ここまでは公式に書いてあるので自分の持ってるRSpecを書きなおしてみたら動かなかった。
ちなみにこんな感じのコードを実行すると以下のようにundefined methodって言われる。

require "set"

shared_examples_for "a collection object" do
  describe "<<" do
    it "adds objects to the end of the collection" do
      collection << 1
      collection << 2
      collection.to_a.should eq([1,2])
    end
  end
end

describe Array do
  it 'sample_examples_for_test' do
    it_should_behave_like "a collection object" do
      let(:collection) { Array.new }
    end
  end
end

describe Set do
  it_should_behave_like "a collection object" do
    let(:collection) { Set.new }
  end
end
Failures:

  1) Array sample_examples_for_test
     Failure/Error: it_should_behave_like "a collection object" do
     NoMethodError:
       undefined method `it_should_behave_like' for #
     # ./sample_spec.rb:15:in `block (2 levels) in '

色々考えて試行錯誤を重ねた結果ようやく自分のミスに気づく。
it句の中にit_should_behave_likeって書いても動かないらしい。


といわけで、RSpecを書いてる中で上記のようにundefined methodって言われたら書く場所が違うことを疑ってみましょう!


それはともかくshared_examples_for便利!

本日の教訓

ActiveRecordを継承したモデルで#saveでも#save!でもデータが保存できなくなる自体に陥った。


で、1時間ぐらい格闘した結果、原因判明。


ActiveRecord継承したモデルを作った際に、モデルの内部でcreateとかupdateのメソッドを実装すると、
既存のcreateやupdateのメソッドがオーバーライドされてDBにデータが保存できなくなるらしい。

  1 # coding: utf-8
  2 
  3 class Book < ActiveRecord::Base
  4   attr_accessible :price, :title
  5   
  6   def create
  7     # 宣言をするとデータが保存できなくなる
  8   end
  9   
 10   def update
 11     # 宣言をするとデータが保存できなくなる
 12   end
 13 end

みんな気をつけろよ!


ちなみにsuper()でオーバライドするメソッドが呼び出せるので、次のようにすれば解決できる気がしなくもない(未検証)

  1 # coding: utf-8
  2 
  3 class Book < ActiveRecord::Base
  4   attr_accessible :price, :title
  5   
  6   def create
  7     # super()を呼ぶとオーバーライドしたメソッドが呼び出される
  8     super()
  9   end
 10   
 11   def update
 12     # super()を呼ぶとオーバーライドしたメソッドが呼び出される
 13     super()
 14   end
 15 end

まぁ、おとなしくメソッド名を変えるのが吉かな。。。

東京Ruby会議10で当日スタッフしてきました

Ruby会議2011に引き続き、東京Ruby会議10で当日スタッフをさせて頂きました。


セッションの内容は見れたものもあり、見れなかったものもあるので詳細は参加者の方々にレポートはお任せして、スタッフ業をさせていただいた感想を終わって1週間経つけど書いてみる。


実はRuby会議2011の時の参加レポートをブログに書いていないはずなので、そのへんのところから振り返っておく。(Ruby会議2011のレポートはとあるSNSには上げたものの、ブログに書くのを忘れてたので、今度拾ってきて書くつもり)


事の発端をたどると、Android Bazar and Conference 2011 winterのボランティアスタッフに参加したことから始まる。
その頃は「OSSのコミュニティに貢献したい」という今考えると自分でもよくわからないモチベーションがあって、当時興味のあったAndroidのイベントでボランティアスタッフの募集があったので申し込んだら当選した。


当選してボランティアスタッフをしたものの、なんか自分の考えていたコミュニティと違うなーと思っているところに、懇親会でRuby関西でお世話になっていた@kanasan とお会いし、話してみたところコミュニティの雰囲気が違うんじゃないかという話になった。
そしてRuby会議が今年で最後なのでそちらの当日スタッフをしてみてはどうかという話を受けて考えていた所に、Chiba.rbで@bash0C7さんからもRuby会議の当日スタッフのお誘いがあり、Ruby会議2011の当日スタッフにも申し込んで参加させてもらった。


Ruby会議2011の当日スタッフをさせていただいて改めて思ったのは、Rubyのコミュニティが自分にとって楽しい場所だということだった。


イベントを盛り上げようとするスタッフと、そこに来てくれるスピーカーと参加者、イベントには来れないけどTLで反応する人たち。
そういう人たちが集まる場所が自分にとってとても楽しい場所なんだという実感が持てたので、また機会があればスタッフ業がしたいなと思っていた所、運良く千葉で東京Ruby会議10が開かれるということになり、当日スタッフとして申し込んで参加させていただいた。


会期前日から前日設営と蟹工船(ノベリティのセッティング)を行い、1日目を終え、2日目を雪で中断し、最後までやりきれなかったなーと思っていたところへの3日目の開催。


今回は前日設営+会期3日間の計4日を当日スタッフとして参加させていただいて、やっぱりRubyのコミュニティがいいな―と思った。


スタッフ、スピーカー、参加者がみんな会議を楽しもうという感じがあり、2日目を雪で中断したことはすごく残念だったが、それに対してスタッフへの感謝の言葉がTLに流れていたり、都内の有志で会議を続けたりとみんなで会議を作ってるんだと思うとすごくワクワクした。


きっと他にもこういうコミュニティはあるのかもしれないけれど、自分にとってはRubyのコミュニティが一番楽しくてワクワクする場所なんだと思う。なので、機会があり続ける限りイベントにはスタッフなり一般の参加者、もしくはスピーカーとして参加していこうと改めて思った。


東京Ruby会議10は自分がRubyのコミュニティが好きで、これからもそこにいる人達と楽しくRubyを通して交流して行きたいと再度思わせてくれる楽しい会議でした。


東京Ruby会議10に参加されたスピーカーの皆さん、参加者の皆さん、そしてスタッフの皆さん、3日間+事前事後懇親会+準公式懇親会といろいろお疲れ様でした。


また、どこかの会議で皆様にお会いできるのを楽しみにしております!


※会期2日目の入り口に作った置いておいたお迎えの方

LTしてきたので資料をSlideShareに上げました

とある場所で機会をもらってLTしてきたので資料を公開します。


内容は今流行のBigData(笑)を集める手段としてTwitterのStreaming APIを用いる方法の紹介になります。