狂ったお茶会のlog

後で起きる自分のためのメモ

Facebook Messenger に URLをシェアする

やりたいこと

掲題の通り
URLスキームでちょちょいと飛ばせないかな〜と思った

やったこと

まず
fb://messaging
を試した。
こいつは、facebookアプリを経由して、facebookメッセンジャーアプリを開いてくれるURLスキーム

URLスキームは基本、当該アプリがインストールされていなければ動いてくれないので、
facebookアプリは入ってるけどメッセンジャーアプリは入ってない〜という人が多そうな場合はこちらの方がいいかも。
この経路だと、メッセンジャーアプリがインストールされてなければストアに飛ばしてくれる(多分Facebookアプリ自体の実装)。
ただ、URLなどのシェアをする方法がわからなかった。

次に
fb-messenger://
を試した。
こいつは、直接facebookメッセンジャーアプリを開くURLスキーム

メッセンジャーアプリがインストールされてない場合、ストアに飛ばしてくれたりはしないので、そこは自己実装が必要

こいつでリンクをシェアしたい場合
developerサイトに一応やり方が書いてあった
ウェブ - シェア機能 - ドキュメンテーション - 開発者向けFacebook

fb-messenger://share/?link=https://www.facebook.com
↑これで、メッセンジャーでリンクをシェアする画面に飛ぶ(これは例でfacebookへのリンク)

で、どうなったか

後者のやり方でOKだな!と思ったんですが、確認のために何回も同じURLをシェアしていたら、Facebookにスパム扱いされてしまった。

端末によりますが、メッセージ送信しようとした際に、
FBAPIError Domain error 368
であったり
the action attempted has been deemed abusive or is otherwise disallowed
であったりなどのエラーが発生したりする。

んで次の日こういう表示になっていた。
f:id:dormouse666:20190605124825j:plain:w300

上のリンク踏むとこう出る
f:id:dormouse666:20190605124848j:plain:w300

どうも、URLスキームではなく、SDKなどを使っていても、スパム扱いされることもある様子。
URLとか同じ文言のシェアは、Facebookメッセンジャーですることをあまり考えない方が良さそうですね。
完!

参考URL

mac再起動したら、GithubにSSHでアクセスできなくなった

前提

SourcetreeでSSH鍵を作成して、Githubにその鍵を登録してある状態
mac再起動するまでは、正常に使えていた

やりたいこと

macを久々に再起動して、
Sourcetreeでfetchしようとしたら

git@github.com: Permission denied (publickey).
fatal: Could not read from remote repository.
Please make sure you have the correct access rights and the repository exists.

って出たので解決したい。

やったこと

とりあえず、接続試みつつデバッグしてくれるコマンドで確認
ssh -vT git@github.com

↑のログを見た感じ、Offering public key で、.ssh/id_rsa を見に行っている気配
けどこれ接続できないの当たり前で、ソースツリー&githubに登録してある鍵は別の名前の鍵(仮にid_rsa_githubとする)。

色々やったけど、以下結論。

.ssh/config に、Sourcetreeがssh鍵を作るときに情報を勝手に書き込んどいてくれるのだが、その情報がダメそうだった。

こうなってたのを

# --- Sourcetree Generated ---
Host id_rsa_github
        HostName github.com
        User dormouse666
        PreferredAuthentications publickey
        IdentityFile /Users/hogehoge/.ssh/id_rsa_github
        UseKeychain yes
        AddKeysToAgent yes
# ----------------------------


こうしたら治った。

# --- Sourcetree Generated ---
Host github.com
        HostName github.com
        User git
        PreferredAuthentications publickey
        IdentityFile /Users/hogehoge/.ssh/id_rsa_github
        UseKeychain yes
        AddKeysToAgent yes
# ----------------------------


memo:

  • Host を、ssh keyの名前から、github.com へ
  • User を、Githubのユーザ名から、git へ


参考URL

macOS 再起動で GitHub と SSH 接続できなくなる【Sourcetree】

ランチ候補をpostしてくれるslackbotを、Google Apps Scriptで作る

やりたいこと

転職したのでランチ場所の情報を蓄積したい
一回行って美味しかった場所&これから行ってみたいと思ってる場所をおすすめしてくれるやつがいい

いい感じにランチを決める Slack ボットを作った

↑こういうのがいいなぁと思った

仕様
・ 「ごはん」と呼んだら候補をslackにpostしてくれる
・ お昼の時間になったら、自動的にも投稿してくれる
スプレッドシートの情報を読んで、候補をランダムに選んでくれる


やったこと

Incoming WebHooks を設定する

https://my.slack.com/services/new/incoming-webhook
で Post to Channel でbotに投稿させたいチャンネルを選んで、
Add Incoming WebHooks Integration ボタンを押下

ここで生成された、 Webhook URL を後で使うのでメモっておく。

Outgoing WebHooks を設定する

https://my.slack.com/services/new/outgoing-webhook
で Add Outgoing WebHooks Integration ボタンを押下して追加

Channel: 発言を拾うチャンネル名を設定
Trigger Word(s): トリガーワードを設定できるが今回は使わなかった。使った方が楽
URL(s): 作成したGASのウェブアプリケーションのurlを設定してSaveする。urlについては後述

上記の設定を変更したい時はここ
https://my.slack.com/apps/manage/custom-integrations

スプレッドシート作る

f:id:dormouse666:20180510201938p:plain
こんな感じにした
店名だけだとわからんので、備考とurlも情報として持つようにする

あと、botが喋るmessageの内容もマスタっぽくシートに持たせておくことにした
f:id:dormouse666:20180510202006p:plain
こんな感じで


Google Apps Script のファイルを作成してコード書く

書いた

var SLACK_WEBHOOK = 'https://hooks.slack.com/services/~~~~~~';  // Incoming WebHooks -> Webhook URLをここに設定する
var SLACK_CHANNEL = '#lunch';
var SPREADSHEET_URL = 'https://docs.google.com/spreadsheets/~~~~~~~~~';  // 作成したスプレッドシートのURL
var EMOJI_ICON = ':fried_shrimp:';
var BOT_NAME = 'lunchBot';
var LUNCH_HOUR = 12;
var LUNCH_MINUTE = 50;
var IS_COFFEE = false;
var IS_GOHAN = false;
var MESSAGE_TYPE_AUTO = 1;
var MESSAGE_TYPE_GOHAN = 2;
var MESSAGE_TYPE_NEMUI = 3;

// お昼になったら自動投稿するためのトリガー
function setTrigger() {
  var triggerDay = new Date();
  triggerDay.setHours(LUNCH_HOUR);
  triggerDay.setMinutes(LUNCH_MINUTE);
  ScriptApp.newTrigger("lunch").timeBased().at(triggerDay).create(); // lunch関数を動かしたいので、それを指定
}

function deleteTrigger() {
  var triggers = ScriptApp.getProjectTriggers();
  for(var i=0; i < triggers.length; i++) {
    if (triggers[i].getHandlerFunction() == "lunch") {
      ScriptApp.deleteTrigger(triggers[i]);
    }
  }
}

var getRows = function (range) {
  return range.getValues().map(function(x) { return x[0]; }).filter(function(x) { return x });
}

Array.prototype.random = function () {
    return this[Math.floor(Math.random() * this.length)]
}

Array.prototype.randomCount = function () {
    return Math.floor(Math.random() * this.length);
}

// 投稿
function postMessage(message, hookPoint) {
  var payload = {
    "text": message,
    "icon_emoji": EMOJI_ICON,
    "username": BOT_NAME,
    "channel": SLACK_CHANNEL
  }
  var options = {
    "method" : "POST",
    "payload" : JSON.stringify(payload),
    "headers": {
      "Content-type": "application/json",
    }
  }
  var response = UrlFetchApp.fetch(hookPoint, options);

  if (response.getResponseCode() == 200) {
    return response;
  }
  return false;
}

// メイン
function lunch() {  

    if(!IS_GOHAN && !IS_COFFEE) {
      deleteTrigger(); //自動投稿トリガーを消す
    }
  
  var spreadsheet = SpreadsheetApp.openByUrl(SPREADSHEET_URL);
  var sheet0 = spreadsheet.getSheets()[0]; //一番左のシート: 店羅列用
  var sheet1 = spreadsheet.getSheets()[1]; //次のシート: mesasge用
  
  // message 該当タイプをランダムで
  var messageAll = sheet1.getRange(3, 2, sheet1.getLastRow(), sheet1.getLastColumn()).getValues();
  var messageList = [];
  var message = "";
  var type = MESSAGE_TYPE_AUTO;
  
  if(IS_GOHAN) {
    type = MESSAGE_TYPE_GOHAN;
  }
  
  if(IS_COFFEE) {
    type = MESSAGE_TYPE_NEMUI;
  }
  
  for (var i in messageAll) {
    var mtype = messageAll[i][1]; //1つなら数値、カンマ区切りならstring判断になる
    var typeList = [];
    if(mtype == null) {
      break;
    }
    if(isNaN(mtype)) { 
      typeList = mtype.split(','); //文字列。カンマ区切りで配列に変換
    } else {
      typeList.push(mtype); //数値なのでそのままぶち込む
    }

    for (var ii in typeList) {
      if (typeList[ii] == type) {
        messageList.push(messageAll[i][0]); //textだけ格納
      }
    }   
  }
  
  message += messageList.random();
  if(!IS_GOHAN && !IS_COFFEE) {
    message += " <!channel>"; //自動投稿の時だけメンション飛ばす
  }
  
  // 3行目〜
  // B列: ご飯
  // C列: 備考
  // D列: url
  var bs = getRows(sheet0.getRange(3, 2, sheet0.getLastRow()));
  var cs = getRows(sheet0.getRange(3, 3, sheet0.getLastRow()));
  var ds = getRows(sheet0.getRange(3, 4, sheet0.getLastRow()));
  var cnt1 = bs.randomCount();
  var b = bs[cnt1];
  var c = cs[cnt1];
  var d = ds[cnt1];
  
  // F列: 軽食
  // G列: 備考
  // H列: url
  var fs = getRows(sheet0.getRange(3, 6, sheet0.getLastRow()));
  var gs = getRows(sheet0.getRange(3, 7, sheet0.getLastRow()));
  var hs = getRows(sheet0.getRange(3, 8, sheet0.getLastRow()));
  var cnt2 = fs.randomCount();
  var f = fs[cnt2];
  var g = gs[cnt2];
  var h = hs[cnt2];
  
  // J列: 遠出
  // K列: 備考
  // L列: url
  var js = getRows(sheet0.getRange(3, 10, sheet0.getLastRow()));
  var ks = getRows(sheet0.getRange(3, 11, sheet0.getLastRow()));
  var ls = getRows(sheet0.getRange(3, 12, sheet0.getLastRow()));
  var cnt3 = js.randomCount();
  var j = js[cnt3];
  var k = ks[cnt3];
  var l = ls[cnt3];
  
  if(!IS_COFFEE){
      message += "\n\n\n:fried_shrimp: " + "*" + b + "*" + " :fried_shrimp:";
      message += "\n:speech_balloon: " + c + "\n" + d;
  }
  message += "\n\n\n:coffee: " + "*" + f + "*" + " :coffee:";
  message += "\n:speech_balloon: " + g + "\n" + h;
  if(!IS_COFFEE){
      message += "\n\n\n:mushroom: " + "*" + j + "*" + " :mushroom: [遠出枠]";
      message += "\n:speech_balloon: " + k + "\n" + l;
  }
  message += "\n\n\n候補を編集する → <" + SPREADSHEET_URL + "|:memo:>";

  postMessage(message, SLACK_WEBHOOK);
}

// Outgoing WebHooks を設定すると、ユーザからのpostがあると走る処理
function doPost(e) {
  // 自分自身ははじく
  if(e.parameter.user_id == "USLACKBOT"){
    return;
  }
  
  // Outgoing WebHooks の Trigger Word(s) でも設定できるんだけど
  if(e.parameter.text.indexOf("ごはん") > -1){
    IS_GOHAN = true;
    lunch();
  }
  
  // おまけ
  if(e.parameter.text.indexOf("ねむい") > -1){
    IS_COFFEE = true;
    lunch();
  }
}

postMessage
doPost
はGASが持ってる関数。

途中でやらかしたのが、botの返信メッセージに「ごはん」が含まれるとそれに自分で反応して無限ループする。
ので、自分自身には反応しないようにする。
doPost で得ている e.parameter の中身、botからの送信だと

user_id: USLACKBOT
user_name: slackbot

で来ているっぽい。それを弾くようにしておく。

e.parameter.text.indexOf("ごはん") > -1
↑これは、部分一致。「ごはん」という言葉が含まれていれば反応する。


書いたコードを実行するために

GASのメニュー -> 公開 -> Webアプリケーションとして導入 -> 新しいバージョンを保存 -> 導入

で、ここで URLが生成されるので、それを先述した Outgoing WebHooks の URL(s) に設定する。

注意点として、上記手順後にコードを変更したら、
GASのメニュー -> 公開 -> Webアプリケーションとして導入 -> プロジェクト バージョン -> 新規作成 -> 更新 ボタン押下
を毎回しないとbotに反映されない。
ちょっとめんどくさい。


自動投稿するならトリガーを設定する

月水金だけお昼の時間になったら自動で投稿してほしいので、そういった設定をする。

var LUNCH_HOUR = 12;
var LUNCH_MINUTE = 50;

↑の、自動投稿してほしい時間以前に、 setTrigger が走るようにしておけばOK
f:id:dormouse666:20180510202134p:plain

こんな感じ。


こうなる

自動投稿時
この時だけメンションをくれる
f:id:dormouse666:20180510202201p:plain


「ごはん」で呼んだ時
f:id:dormouse666:20180510202224p:plain


ねむい時はコーヒーを勧めてくる
f:id:dormouse666:20180510202246p:plain


以上


参考URL

いい感じにランチを決める Slack ボットを作った
SlackのIncoming Webhooksを使い倒す <-Incoming WebHooks
SlackのOutgoing Webhooksを使って投稿に反応するbotを作る <-Outgoing WebHooks
Google Apps Scriptの日毎のトリガーで時間をもっと細かく設定する <-トリガー
GoogleAppsScriptメモ:トリガーの利用 <-トリガー
Google Apps Script 実践メモ(スプレッドシート) <-スプレッドシートへのアクセス
SlackのIncomingWebhooksとOutgoingWebhooksを使って電子工作と連携させてみよう

twitterBotで、ぐるなびAPIを使ってリプライする

やりたいこと

TwitterBotをつくった - 狂ったお茶会のlog

上記で作ったtwitterBotに、おすすめのご飯を教えてくれる機能をつけたい。

やったこ

ぐるなびAPIを取得

ぐるなび Web Service - トップページ

ここでアカウントが作れます。 api仕様とかも見れる。

書いた処理

  • 「ごはん」とリプライすると、渋谷マークシティ近辺の飲食店をぐるなびで探して持ってくる
  • 「ごはん hogehoge」とリプライすると、渋谷マークシティ近辺の飲食店をぐるなびでhogehogeで検索して持ってくる
  • 複数指定したい場合は「ごはん hogehoge、fugafuga」みたいに「、」で区切って指定
<?php
/**
*  ごはん@ぐるなび
*/
function replyGohan($TwitterOAuth, $value, $gohanAccesskey, $gohanText)
{
    // エンドポイントとパラメータ設定
    $endpoint = 'http://api.gnavi.co.jp/RestSearchAPI/20150630/';
    $hitNum = 50;
    $gohanParams = [
        'keyid' => $gohanAccesskey,
        'format' => 'json',
        'latitude' => '35.657988',  //渋谷マークシティの緯度&経度 エリアコードの方がいいかもしらんが
        'longitude' => '139.698056',
        'range' => '5',             // 緯度/経度からの検索範囲(半径) 5:3000m
        'hit_per_page' => $hitNum,  // 取得件数 どうも50が上限
        'freeword' => $gohanText,   //フリーワード検索「,」区切りで複数ワードが検索可能(10個まで)
    ];
    $gohanUrl = $endpoint.'?'.http_build_query($gohanParams, '', '&');

    // API実行
    $gohanJson = file_get_contents($gohanUrl);

    // 取得した結果をオブジェクト化
    $gonahObj  = json_decode($gohanJson);

    // エラーハンドリング
    if(empty($gonahObj->rest)) //失敗するとrestキーが存在しない
    {
        $errorCode = $gonahObj->error->code;
        $errorMessage = $gonahObj->error->message;
        if(!empty($errorMessage))
        {
            $errorMessage = "".$errorMessage."」とのこと";
        }
        error_log(print_r($gonahObj, true));

        $resMessage = '@'.$value->user->screen_name.' '."すません…".$errorCode."エラーです…\n".$errorMessage;
        $response = $TwitterOAuth->post('statuses/update', array('status' => $resMessage, 'in_reply_to_status_id'=>$value->id_str));

        // エラー出力
        if($TwitterOAuth->getLastHttpCode() != 200)
        {
            error_log(print_r($response, true));
        }

        return;
    }

    // 0件の時
    $gohanNum = $gonahObj->total_hit_count;
    if($gohanNum == 0)
    {
        $resMessage = '@'.$value->user->screen_name.' '.'0件です(´・ω・`)';
        $response = $TwitterOAuth->post('statuses/update', array('status' => $resMessage, 'in_reply_to_status_id'=>$value->id_str));
        return;
    }

    // 成功してたら1件だけ取得
    $i = 0;
    $rand = rand(0, $hitNum-1);
    foreach($gonahObj->rest as $restArray)
    {
        $gohanName = $restArray->name;
        $gohanUrl = $restArray->url;

        if($i >= $rand)
        {
            break;
        }
        $i++;
    }

    if(!empty($gohanText))
    {
        $gohanText = "".preg_replace("/,/", "", $gohanText)."」は";
    }

    $credit = 'Supported by ぐるなびWebService(http://api.gnavi.co.jp/api/scope/)';
    $gohanStr = $gohanText.$gohanNum."件あった、オススメはこれかな\n".$gohanName."\n".$gohanUrl."\n\n".$credit;
    $resMessage = '@'.$value->user->screen_name.' '.$gohanStr;
    $response = $TwitterOAuth->post('statuses/update', array('status' => $resMessage, 'in_reply_to_status_id'=>$value->id_str));

    // エラー出力
    if($TwitterOAuth->getLastHttpCode() != 200)
    {
        error_log(print_r($response, true));
    }
}
?>

シンタックスハイライトしてもらおうと思って<?php ?>で囲んだけどなんか色がすごい)

使うとこういう感じ

f:id:dormouse666:20171018194238p:plain

Github

github.com



解決できなかったこ

フリーワード検索自体の精度がどうも今一つであり、「カレー」で検索しても鳥貴族とかを持ってきちゃったりすることが結構多い。
業態マスタ取得API とかも使わないとダメかな。。。



参考URL

TwitterBotをつくった

今更なはなし

4月、世の中がマストドンで盛り上がっている中、ツイッターbotをぺちぺち作った( @sennpai_bot )。
本人の許可は得たが、そのうち消えるかもしれないし消えないかもしれません。

github.com

テキストファイルで動くめっちゃ原始的なbot
テキストファイルじゃなくてDBからマスタデータ読み込むようにしたいと思いつつまだ何もしていません。

詰まったところ

使ってるサーバのPHPが古かった

twitteroauth のPHPの記載が新しくなってて5.3だと使えなかった。
4月にPHPのバージョンを5.3から5.6にあげたのはこれのせい。

Status is a duplicate.

ツイッター民にはおなじみの同じ投稿はできませんエラー。
botだとどうしても引っかかることが多い。
めんどくさかったので投稿頻度を下げる&投稿内容を増やすことで解決(?)

ツイッターからapiが返ってこない時

bot公開してしばらくして、ツイッター自体が落ちたタイミングがあった。
その際に statuses/user_timeline が返却されてこなかったため、延々とリプライ返しまくるバグが出た。
emptyチェックを入れて解決。

参考URL

PHP+OAuthでTwitterのBotを作ってみる - SDN Project
PHP + OAuthで Twitter botをつくってみよう - PHP入門 - Webkaru
PHPからTwitterツイート(2015年2月版) - Qiita
Twitter メンション(@)に反応してリプライを返すbotを作る vol.2 - Hell Yeah!!

PHPのバージョンを5.3から5.6にアップデートした

やりたいこと

サーバ(CentOS)に入ってるPHPのversionを上げたい。
なんか5.4あたりから配列定義が変わったらしく、使いたいライブラリでエラーが出たため。
いま入ってるやつはボンヤリyum installしたものなので古い。

もともと入ってたやつチェック

# rpm -qa | grep php
php-common-5.3.3-46.el6_7.1.x86_64
php-xml-5.3.3-46.el6_7.1.x86_64
php-pear-1.9.4-4.el6.noarch
php-cli-5.3.3-46.el6_7.1.x86_64
php-pdo-5.3.3-46.el6_7.1.x86_64
php-gd-5.3.3-46.el6_7.1.x86_64
php-mcrypt-5.3.3-4.el6.x86_64
php-devel-5.3.3-46.el6_7.1.x86_64
php-mbstring-5.3.3-46.el6_7.1.x86_64
php-5.3.3-46.el6_7.1.x86_64
php-mysql-5.3.3-46.el6_7.1.x86_64


やったこと

CentOSのversionをみたところ6.7

# cat /etc/redhat-release
CentOS release 6.7 (Final)
CentOS6系のepslリポジトリを入れる
# yum install epel-release

これはでももう入ってた。

CentOS6系のremiリポジトリを入れる
# rpm -Uvh http://rpms.famillecollet.com/enterprise/remi-release-6.rpm

こっちは入ってなかったのでDLされた。

インストール済みの古いphpを削除する
# yum remove php-*
php5.6をインストールする (無駄なパッケージもあるかもしれない)
# yum install --enablerepo=remi,remi-php56 php php-devel php-mbstring php-pdo php-gd php-mysql php-mcrypt php-xml php-pear
いれたやつのバージョンチェック
# php --version
PHP 5.6.30 (cli) (built: Jan 19 2017 08:09:42)
Copyright (c) 1997-2016 The PHP Group
Zend Engine v2.6.0, Copyright (c) 1998-2016 Zend Technologies

できた〜

apacheリスタート(php.ini を変更するなら、その後またする)
# service httpd restart


phpの設定ファイルも変更する

場所は以下なので開いて編集

# vi /etc/php.ini

今回変更したのは以下

error_log = /var/log/php_errors.log
date.timezone = “Asia/Tokyo”        ([Date] 項目)
mbstring.language = Japanese        ([mbstring] 項目)
mbstring.internal_encoding = UTF-8  ([mbstring] 項目)

mbstring.internal_encoding もなんだけど、
mbstring.http_input とかは今後非推奨のようなのでそのまま放っておいた。
PHP: 実行時設定 - Manual

ログファイルの実体がなかったから一応viで作っておいたのと、
書き込み権限がないとエラーが書き込まれてくれないとのことだったので、一応chmod 666をしておいた。

ログは以下で見れる。

less /var/log/php_errors.log

以上


参考URL

CentOS6/CentOS7にPHP5.6/PHP7をyumでインストール - Qiita
PHP: 実行時設定 - Manual
TwitterOAuth.php on line 334 のsyntax errorの原因と修正 - 半地下備忘録
PHP 5.4から配列定義は超簡単に、そして落とし穴も | yohgaki's blog

計算をするときに注意すること

はじめに

初歩的な話なんだが、やってしまったので書いておく。

やらかしたこと

int count = 200;
int value = 10;
int point =  count * (value / 100);

value を百分率にして count にかけたものを最終的な point として扱うということをしたかった。
のだが、 int は小数点以下を切り捨てて扱うため、 (value / 100) を先に計算しちゃうと、0.1なので0になる。
0を乗算しても0なので、 point は0となる。

やるべきだったこと

int count = 200;
int value = 10;
int point =  (count * value) / 100;

計算の順番を変えて、最後に除算をする。
これだと point は20となる。

もしくは、型を int じゃなくて float とかにしとけば小数点が許容される。

以上