祝日や会社の休日に起動しないSlack通知bot

はじめに

平日の18時に行っている夕会用に、開始時間になると曜日ごとのファシリテータをお知らせするbotを運用中。

土日祝日と会社指定の休日は通知飛ばないようにしてます。

祝日判定のためにGoogleAppsScriptとGoogleカレンダーを利用してます。

Googleカレンダーに用意されている日本の祝日カレンダーで祝日を、別に用意した会社の休日カレンダーで年末年始や夏季休暇に対応しています。

コード

function onOpen() {
  if (isHoliday()) {
    return
  }
  deleteAllTriggers();
  setupTriggers(); 
}

function deleteAllTriggers() {
  var allTriggers = ScriptApp.getProjectTriggers();
  for(var i=0; i < allTriggers.length; i++) {
      ScriptApp.deleteTrigger(allTriggers[i]);
  }
}

function getFormatedDate(time){
  var now = new Date();
  var year = now.getFullYear();
  var month = now.getMonth() + 1;
  var date = now.getDate();

  month = ("0" + month).slice(-2);
  date = ("0" + date).slice(-2);

  var strDate = year + "/" + month + "/" + date + " " + time;

  return strDate;
}

function setupTriggers() {
  // 通知時間の設定
  var trigger = new Date(getFormatedDate("18:00"));
  var onChangeTrigger = ScriptApp.newTrigger("notfy")
                  .timeBased()
                  .at(trigger)
                  .create();
  //全トリガー削除するので再設定
  var onChangeTrigger = ScriptApp.newTrigger("onOpen")
      .timeBased()
      .atHour(10)
      .everyDays(1)
      .create();
}

function notfy() {
  var facilitator = getTodayFacilitator();

  postSlack(facilitator);
}

function isHoliday() {
  var japanHoliday = CalendarApp.getCalendarsByName('日本の祝日');
  var companyHoliday = CalendarApp.getCalendarsByName('会社の休日'); 
  var holidays = japanHoliday[0].getEventsForDay(new Date()) + companyHoliday[0].getEventsForDay(new Date()) ;

  return (holidays != '');
}

function getMembers()
{
  var values = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('member').getRange(2, 1, 5, 2).getValues();  
  var members = {'日':'', '月':'', '火':'', '水':'', '木':'', '金':'', '土':''};
 
  for (var i = 0; i < 5; i++) {
     var youbi = values[i][0];
     members[youbi] = values[i][1];
  }
  
  return members
}

function getTodayFacilitator() {

  var now = new Date();
  var week = ['日', '月', '火', '水', '木', '金', '土']; 
  var members = getMembers();

  var facilitator = members[week[now.getDay()]];

  return facilitator;
}

// send to slack
function postSlack(name)
{
  if (name == "") return;

  var payload = {
    "text" : "<!here> " + "今日の当番は" + name + "ですよ",
    "channel" : "#general" // 表示名やアイコンはお好みで
  }

  var options = {
    "method" : "POST",
    "payload" : JSON.stringify(payload)
  }
 
  var url = "https://hooks.slack.com/services/hoge/fuga/piyo"; // 各自のURL
  var response = UrlFetchApp.fetch(url, options);
  var content  = response.getContentText("UTF-8");
}

ポイント

使うまで知らなかったけど、GoogleAppsScriptの時間起動トリガーだと毎日18時っていう指定ができませんでした。

近いのは、毎日18時〜19時の間に起動するというトリガーか特定日の18時というトリガーです。

仕方ないので、毎日10時〜11時の間に起動して、setupTriggers()という関数でその日の18時のトリガーを設定してます。

さらに面倒なことにトリガーを区別するのが大変ぽかったので、一回全部消してから全部設定し直すようにしてます。

Linux上で特殊文字を含む名前のファイルの削除方法

はじめに

コマンドの操作ミスやスクリプト出力の文字化けなどで特殊文字を含む名前でファイルができてしまい、単にrmコマンドでは削除できない時の削除方法。

いざという時のためにメモ。

実験用のファイル

temp $ ls
"ccc.txt    -bbb.txt    aaa.txt
temp $

他にもありえますが用意するのがめんどくさかったのと、今回の削除方法はファイル名によらず使えるのでこれで。

実験

とりあえず用意したファイルを普通に消そうとするとどうなるか見てみます。

先頭がダブルクォートのファイル名

temp $ rm "ccc.txt
>
temp $

次の入力を待たれてしまいます。

先頭がハイフンのファイル名

temp $ rm -bbb.txt
rm: illegal option -- b
usage: rm [-f | -i] [-dPRrvW] file ...
       unlink file
temp $ 

rmコマンドのオプションと解釈されてしまいます。

解決方法

いろいろなやり方があるのですが、汎用的な方法を。

手順1 ls -iでinode番号を確認

temp $ ls -i
2991451 "ccc.txt    2991471 -bbb.txt    2991475 aaa.txt
temp $

ファイル名の前に表示されているのがinode番号です。

手順2 確認したinode番号を指定して削除

temp $ rm `find . -inum 2991451`
temp $ ls
-bbb.txt    aaa.txt
temp $ rm `find . -inum 2991471`
temp $ ls
aaa.txt
temp $

削除できました。

実際にやる場合は、事前の削除対象の確認を忘れずに。

おまけ

実験用に使ったパターンのものは別の方法でも削除できます。

先頭がダブルクォートのファイル名

2つ方法があります。

  • ファイル名のダブルクォートをエスケープする
    • rm ¥"ccc.txt
  • ファイル名をダブルクォートで囲んでファイル名のダブルクォートをエスケープする
    • rm "¥"ccc.txt"

先頭がハイフンのファイル名

こちらは、ダブルクォートの方法では消せないので以下の方法を使います。

  • コマンドのオプションに--を指定する
    • rm -- -bbb.txt

iOSDC Japan 2017に行ってきました

子どもができてからというもの、なかなか勉強会やイベントに参加できなくなっていましたが、久々に参加してきました。

iosdc.jp

かなり悩みつつセッションの選択をしたなかで、特に印象に残ったものをあげます。

両OSやるマンという選択

speakerdeck.com

実際に1人でiOS/Android対応されている方の発表。

意外とAndroidも開発してるよって人が多かったのが印象的でした。

違いを意識してプロダクトに落とし込むことが一番重要っていう部分は同意しかなくて、ディレクターさんやデザイナーさんにこそ共有したいなーと思いました。

Android開発もやってみよっかなーという気分になりました。

アプリエンジニアはどのように事業に貢献すべきか

speakerdeck.com

近々、初めてBtoCのサービス開発に関わる予定なのですが、同じようなことに悩むんだろうかと思いつつ聞いていました。

気になったキーワード

  • アプリを使うことがユーザーにとってやりたいことではない
  • ユーザーが喜ぶ体験を理解して、アプリ上で実現する
  • アプリ以外も含めてサービスでありユーザー体験である

何度か読んで、自分なりの考えをまとめてみるつもり。

結婚式を支えた技術 Firebaseを活用したサーバレスiOSアプリケーション開発

speakerdeck.com

結婚式の写真を共有してもらうために1ヶ月でアプリを作ってしまうという素敵なお話。

身近なところにある問題を解決するのは、ユーザーも近くてフィードバックもらいやすいし、なにより解決すれば喜んでもらえるし、いいことずくめですね。

我が家でも、妻からこんなアプリできないの?みたいなことをたまに言われるのですが、子どもと遊ぶのを言い訳にしてしまっていて反省。

とりあえずFirebase触ってみようと思いました。

全体通しての感想

めちゃめちゃ楽しかったです。

以下、楽しかったことを雑にまとめます。

  • オープニングから力の入ったムービーでテンションあがる
  • セッションはどれも面白く、同じ枠で聞きたいものがかぶりまくって泣く泣く選択するという贅沢な状況
  • 懇親会でもないのにビールが配られて、飲みながら聞くLT最高
  • 運営の方々が、ほんとによく動いていて、とにかくスムーズな運営が印象的

ただ、2日目の途中で体調を崩してしまい、途中で帰らざるを得なくなってしまったのが心残りです。

来年も開催されたら、最後まで参加するぞ。懇親会も参加できたらいいな。

Swiftはプロパティもオーバーライドできる

バージョン

  • Swift 3.0.2
  • Xcode 8.2.1

クラスのプロパティ

知らなかっただけなのですが、Swiftはプロパティもオーバーライドできます。

使い方次第で便利に使えそう。

サンプルコード

プロパティのオーバーライドとプロパティ監視を組み合わせて、ボタンが無効化されたらアルファ値を変えるコードです。

import UIKit

class CustomButton: UIButton {
    override var isEnabled: Bool {
        didSet {
            super.isEnabled = isEnabled
            self.alpha = isEnabled ? 1.0 : 0.4
        }
    }
}

let customButton: CustomButton = CustomButton()

print(customButton.isEnabled) // true
print(customButton.alpha) // 1.0
customButton.isEnabled = !customButton.isEnabled

print(customButton.isEnabled) // false
print(customButton.alpha) // 0.4

UIViewからUITableViewのカスタムセルを作るとボタンタップイベントが取れない

現象

UITableVIewのカスタムセルにボタンを置いて、タップ用の処理を書いたがタップイベントを検知できない。

原因

ボタンの上のレイヤーに、UITableViewCellContentView というビューが描画されており、このビューに邪魔されてタップイベントが検知できていなかったため。

f:id:y_sumida:20170301094905p:plain

根本原因

カスタムセルのxibファイルを、UITableViewCell ではなく、 UIView を使って作成していたため。

UITableViewCell で作ったxibは、UITableViewCellContentView の上に自分の配置したパーツが描画されています。

f:id:y_sumida:20170301094946p:plain

ちなみに、xibファイルの新規作成すると、デフォルトで UIView が置いてあるというXCodeのおせっかい機能のがほんとの原因かもしれません。

f:id:y_sumida:20170301095134p:plain

対策

カスタムセルを作る際には、xibファイルのデフォルトで置いてある UIView を消して、UITableViewCell を置くと良いです。

f:id:y_sumida:20170301095039p:plain

または、新規ファイルを作成する際に、クラスファイルから作成して、 Cocoa Touch Class を選択して、次の画面で Also create XIB file にチェックを入れると、勝手に UITableViewCell を配置したxibファイルを生成してくれます。

f:id:y_sumida:20170301095245p:plain

f:id:y_sumida:20170301095341p:plain

AutoLayoutに慣れてる人には当たり前なのかもしれないですが、ちょっとわかりにくいなーと思いました。

2016年の振り返り

今年もあんまりブログ書けなかったけど、振り返りはしておく。

お仕事全般

こまごまPHPもやったけど、ほぼSwiftでiOSアプリ開発ばっかりだった。

チームでのアプリ開発を経験できたり、RxSwift触ってみたり楽しかった。

プチSCMBCとかを開催して、ちょっとずつプルリクエスト方式での開発ができるようになってきた。

Qiita:Team

去年に引き続き、積極的に投稿するようにしていた。

だんだん書いてくれる人が増えてきて嬉しい。

少しは役に立っているのかも。

社内勉強会

Bitbucketを使うことが増えてきたので、プチSCMBCみたいなことをした。 コンフリクト祭りが楽しかった。

全職種向けにバージョン管理についての勉強会を開催して日付zipがなぜダメなのかを伝えた。

社外勉強会など

ほぼ参加できなかったけど、まあ、これは仕方ない。

プライベート

第二子である次男が生まれて家の中がさらに賑やかになった。

長男が保育園に通うようになって、より時間を効率的に使う必要が出てきた。

早めに出社して勉強してたのが、子どもを寝かしつけてからの時間に変わった。

たまに一緒に寝落ちしちゃうのどうにかしたい。

来年の目標

  • Qiita:Teamをディレクターさん、デザイナーさんも使うようになってきたのでそっち向けの記事も書く
  • Swift環境での自動テストを導入する
  • iOSアプリ開発に偏った1年だったので、Web開発もうちょっとがんばる
  • 何かしらアウトプットする

配列の要素をオプショナルバインディング

はじめに

コードレビューで、ちょっと不思議なコードがあったのでメモ。

結論

配列の添字アクセスをオプショナルバインディングしてもビルドエラーにならないけど、わかりにくいのでやめたほうがいい。

コード

var hoge:[Int] = []

// hogeを更新する処理など

if let n: Int = hoge[0] { //ランタイムエラー
    print(n)
}
else {
    print("nothing")
}

一見すると良さそうですが、配列が空っぽなのでコメントした行で fatal error: Index out of range になります。

当たり前といえば当たり前ですね。

改善案

配列でこういった判定を行うなら、空配列の判定したほうがわかりやすい気がします。

if hoge.isEmpty {
    print("nothing")
}
else {
    print(hoge[0])
}

そもそも、配列アクセスでnilが返る可能性を想定したコードになっていますが、hogeInt型の配列であって、Optional<Int>型の配列ではないので、オプショナルバインディングは適切ではないです。

なんでビルドエラーにならないのかは謎です。

どうしてもオプショナルバインディングしたい場合

if let n: Int = hoge.first {
    print(n)
}
else {
    print("nothing")
}

今回の場合は、配列の最初の要素にアクセスしたいので、Array クラスの first プロパティでOK。

first はオプショナル型なので問題なし。

似たようなプロパティとして最後の要素を参照する last もあります。

どちらも正確には Array クラスに適用している CollectionType プロトコルの特徴ですが。