二次元画像をランダムで返すLINEBOT作ってみたよ。

#Androidアプリに思いを馳せて
少し前にAndroidアプリにkonachanAnimeというものがありました。
タグを指定してGelbooru、Yande.re、Danbooru、Konachanから画像を表示して保存できるものでした。
そういえばあれってどういう仕組みなんだろうかとちらっと調べたらどうやらMoebooru(DanbooruAPI)というものを使って検索しているようでした。
すごいお世話になっていましたが、現在ではKonachan以外はアクセスできないようで、どうもKonachan以外はAPIを使うためにアカウントが必要になったようです。
KonachanについてはGETとPOSTでなんとかできる的なことが書いてあったので、最近PHPでLINEのBOT作ってるし、勉強がてらPHPでAPIからURLでも取れないかなーと思って触ってみました。

#はじめに

- やりたいこと
 - LINEで特定の語をつぶやくと適当に画像を検索してくれる。
 - そうじゃなくてもランダムで送ってくれる。
 - LINEから検索もできるといい感じ。また、R18指定もできるとなお良い。
 - konachanを使った画像検索ツールのようなイメージ

#情報収集
とりあえず情報がほぼゼロなので、[Konachan.com](https://konachan.com/)へ行き、APIの説明を見ます。

>In the Danbooru API, a URL is analogous to a function name. You pass in the function parameters as a query string. Here's an extremely simple example: /post.xml?limit=1.

非常に簡単そうです。
APIページを読み進めていくと、とりあえずパラメータを設定してGETすれば結果のXMLを返す的な感じらしいということがわかります。
通常の検索結果（long_hair)
https://konachan.com/post?tags=long_hair

実際に叩いてみる(long_hair)
https://konachan.com/post.xml?tags=long_hair
![image.png](https://qiita-image-store.s3.amazonaws.com/0/238748/c44f6a6c-111b-8c88-86aa-8a68ff342b8a.png)
ものすごい量の文字で圧倒されました。

XMLを配列っぽくするワザがあるらしいとPHPリファレンスさんにあったので真似してみました。

```php:konachantest.php
<?php
$xml = "https://konachan.com/post.xml?tags=long_hair";
$xmlData = simplexml_load_file($xml);
print_r($xmlData);
?>
```
[PHP:simplexml_load_file](http://php.net/manual/ja/function.simplexml-load-file.php)
simplexml_load_fileでXMLを扱える形にするような感じ・・・なのかな。パースやらオブジェクトの意味があまりよくわかっておりませんが、だいたいオブジェクトにするんだなってことぐらいはわかりました。それならアロー演算子でJSON扱う時みたいにとりだせるんじゃないか・・・と考えます。
先のコードだと以下のように出力されました。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/238748/bf0d78f4-98d0-fb5e-e45d-d7f710ab18b2.png)

#値の出力
まず、出力された配列っぽいやつを眺めて値の出力をしてみます。画像のURLを取り出します。

```php:konachantest.php
<?php
$xml = "https://konachan.com/post.xml?tags=long_hair";
$xmlData = simplexml_load_file($xml);
echo $xmlData->post['file_url'];
?>
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/238748/8f7e8b85-c585-5416-feba-3cdb669c078a.png)

割といい線行ってる・・・！って思ってましたが、次で打ちひしがれました。

```php:konachantest.php
<?php
$xml = "https://konachan.com/post.xml?tags=long_hair";
$xmlData = simplexml_load_file($xml);
echo $xmlData->post[0]->file_url;
?>
```
なぜかこれでは何も出力されません。
1時間ほどこねたりたたいたりしましたがダメで、XMLから出てきた配列を再度眺めてみました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/238748/2d026d5f-0cbb-8a2a-d5b2-a5d9c2d7c992.png)

Postの中には35個ぐらいの属性があるようです。これを配列の形で取ろうとすれば取れるようですが、アロー演算子で抜こうとするとなぜかできないようです。
POSTのすぐ下にいる@attributesがきになりグーグル先生にお尋ねしたところ、以下のページがヒットしました。
[有限会社エムティシステム - PHP SimpleXML オブジェクト@(アットマーク)attributes のアクセス](http://web.mt-systems.jp/archives/366)
どうも、attributes()を入れると良いと書かれておりました。

```php:konachantest.php
<?
$xml = "https://konachan.com/post.xml?tags=long_hair&limit=1";
$xmlData = simplexml_load_file($xml);
echo $xmlData->post[0]->attributes()->file_url;
?>
```

確かにこれだとちゃんと出力されました。
師匠に尋ねてみます。
[PHPリファレンス-SimpleXMLElement::attributes](http://php.net/manual/ja/simplexmlelement.attributes.php)

>SimpleXMLElement::attributes — 要素の属性を定義する

そもそも要素はそのままでは扱えないのね・・・。
全くわかりませんでした。

#HTMLで出してみる
```php:konachan.php
<?
$xml = "https://konachan.com/post.xml?tags=long_hair&limit=1";
$xmlData = simplexml_load_file($xml);
echo "<a href=".$xmlData->post[0]->attributes()->file_url."><img src=".$xmlData->post[0]->attributes()->preview_url."></a><br>";
?>

```

そんなんで、こんなことをするとページに1枚のイラストが表示されて、リンクまで貼れてしまいました。ﾔｯﾀﾈ!
これを応用して、

```php:konachan.php
<?
$xml = "https://konachan.com/post.xml?tags=long_hair";
$xmlData = simplexml_load_file($xml);
for ($i=0; $i < 50; ++$i) {
  echo "<a href=".$xmlData->post[$i]->attributes()->file_url."><img src=".$xmlData->post[$i]->attributes()->preview_url."></a><br>";
}
?>
```
とかやると50枚一気に出てきます。
また、konachanを見ていると、検索オプションを指定しているようなので、それに倣ってみます。
limitオプションというものを使うと指定した数だけ出力してくれるようです。

```
https://konachan.com/post.xml?tags=order%3Arandom+rating:safe&limit=1
```
これでお子様にも比較的セーフな画像を1枚ランダムで出してくれます。
こいつで出てきたURLをLINE BOTさんがPOSTすればいいかんぢになるんじゃないかなと思ました。
全検索してから1枚出すよりもkonachanサーバーにやさしいのかな・・・？

#画像の出力について
最初は一旦サーバーにダウンロードして圧縮して少し解像度を小さくして名前を変えてと加工をして1つのファイルをローテーションで使う感じにしたかったのですが、LINE APIの仕様上、画像をBOTから出す場合はURLを指定する（ディレクトリではいけない）というので、単一のファイルの中身が入れ替わると過去に送られてきたすべての画像が同じものに変化するという奇妙な現象が起こります。
そのためkonachanから直接URLを吐き出すようにしました。（PHPサーバー的に負担は少ないけど、問題ないかなぁ・・・。リクエストが増える分、konachanの負担が増えるかもしれない・・・？）
[LINE APIリファレンス-画像メッセージ](https://developers.line.me/ja/docs/messaging-api/reference/#anchor-42120f44e51aa176dd60c34dc3719ff4fccc4b21)
わざわざ太字で"HTTPS"と書いてあるのですが、見落としてました。
そもそも受け取るときもURLの形式ですね。LINEに画像をアップロードするイメージではなく、LINEからサーバーに画像を参照しに行っているんでしょうか。この辺の扱いは触れられていないようなので詳しくはわかりませんが、挙動からそんな気はします。

#LINEBOT作るよ
LINE Developersの登録などはページに行ったらオートメーションでした。
（3日ぐらいLINE@MANAGERとLINEDevelopersの違いと扱いで躓いたのは内緒の話。。。一つにまとめてほしいところです。）
解説されているサイトさんはいっぱいあるので、そちらを参考にしてください。:443を忘れないようにね。
そんなこんなでLINEのコールバックのコードに入れ込んでいきます。

まずはリファレンス通りにお約束を書いていきます。

```php:callback.php
<?php
//HTTPステータス200を返す。
http_response_code(200) ;
echo '200 {}';

//チャンネルアクセストークン
$accessToken = 'アクセストークンだよ。ほげほげ';
//チャンネルシークレット
$channelsecret = 'チャンネルシークレットだよ。ふがふが';
//ユーザーからのメッセージ取得
//リクエストボディを変数に入れてjson形式からPHPの型にデコードする
$json_string = file_get_contents('php://input');
$jsonObj = json_decode($json_string);

//署名の検証
//チャンネルシークレットをキーにしてリクエストボディからハッシュ作成
$hash = hash_hmac('sha256', $json_string, $channelsecret , true);
//シグネチャ作成、作成したハッシュをMIME base64方式でエンコードする。拡張ヘッダからのシグネチャをデコードして比較してもよい。好きなほうで。
$sig = base64_encode($hash);
//拡張ヘッダからAPI側から設定されているシグネチャを取得 （リファレンス参照）
$compSig = getallheaders()['X-Line-Signature'];
//確認してだめだったらエラーログ出力
if ($sig != $compSig) {
	error_log("不正なメッセージだよ。なんだろね。");
	exit;
}

function hatsugen(){
	global $replyToken, $response_format_text, $accessToken;
	$post_data = ["replyToken" => $replyToken,"messages" => [$response_format_text]];
	$ch = curl_init("https://api.line.me/v2/bot/message/reply");
	curl_setopt($ch, CURLOPT_POST, true);
	curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
	curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post_data));
	curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json; charser=UTF-8', 'Authorization: Bearer ' . $accessToken));
	$result = curl_exec($ch);
	curl_close($ch);
}
```

次に、各種の処理をだらだら書いていきます。

```php:callback.php
//参加時の処理
if($EventType == "join"){
	$response_format_text = ["type" => "text", "text" => "こんにちは。二次元画像検索β版です。\nkonachan.comを使用しています。\n使い方はhelpと打ってください。" ];
	hatsugen();
	exit ;
	}
//画像をランダムで1枚投稿する。
if($text == 'randomimage'){
	$xml = "https://konachan.com/post.xml?tags=order%3Arandom+rating:safe&limit=1";
	$xmlData = simplexml_load_file($xml);
	$imgurl = $xmlData->post[0]->attributes()->sample_url;
	$tumburl = $xmlData->post[0]->attributes()->preview_url;
	$response_format_text = ["type" => "image", "originalContentUrl" => "$imgurl", "previewImageUrl" => "$tumburl"];
	hatsugen();
//指定した単語のR18画像をランダムで1枚投稿する。
}elseif(strpos($text,'r18search:') !== false) {
	$searchtext = str_replace('r18search:','',$text);
	$xml = "https://konachan.com/post.xml?tags=$searchtext+order%3Arandom+rating:questionableplus&limit=1";
	$xmlData = simplexml_load_file($xml);
	//検索結果がない場合の処理。なにもないよ！
	if($xmlData == "" ){
		$response_format_text = ["type" => "text", "text" => "Nobody here but us chickens!"];
		hatsugen();
		exit;
	}
	$imgurl = $xmlData->post[0]->attributes()->sample_url;
	$tumburl = $xmlData->post[0]->attributes()->preview_url;
	$response_format_text = ["type" => "image", "originalContentUrl" => "$imgurl", "previewImageUrl" => "$tumburl"];
	hatsugen();
//指定した単語の全年齢画像をランダムで1枚投稿する。
}elseif(strpos($text,'search:') !== false) {
	$searchtext = str_replace('search:','',$text);
	$xml = "https://konachan.com/post.xml?tags=$searchtext+order%3Arandom+rating:safe&limit=1";
	$xmlData = simplexml_load_file($xml);
	//検索結果がない場合の処理。なにもないよ！
	if($xmlData == "" ){
		$response_format_text = ["type" => "text", "text" => "Nobody here but us chickens!"];
		hatsugen();
		exit;
	}
	$imgurl = $xmlData->post[0]->attributes()->sample_url;
	$tumburl = $xmlData->post[0]->attributes()->preview_url;
	$response_format_text = ["type" => "image", "originalContentUrl" => "$imgurl", "previewImageUrl" => "$tumburl"];
	hatsugen();
//helpと言われたときに使い方を発言する。
}elseif($text == 'help'){
	$response_format_text = ["type" => "text", "text" => "randomimageって打つと適当に画像ひっぱってくるよ。\nsearch:検索結果文字列（英語）で検索したやつをランダムで一個だすよ。\n例:)search:fate/Fstay_night\nスペースは\"_\"（アンダーバー）にしてね。\n続きはhelp2と打ってね。" ];
	hatsugen();
}elseif($text == 'help2'){
	$response_format_text = ["type" => "text", "text" => "ワイルドカードの*を使うとその単語を含んだものを検索します。\n例:)search:*haruhi*でharuhiを含んだ検索をするよ。\nr18search:で18禁画像を検索するよ。"];
	hatsugen();
}
?>
```
徹夜の頭ではこの程度が限界のようです。
もうちょっとはっきりしていれば関数にある程度まとめたりすると思います。
なにより可読性とメンテ性に乏しいので、はっきりしているときに書き換えをしていこうと思います。

動きとしては、

- 「randomimage」でなにか適当に画像一枚
- 「search:(文字列)」で全年齢の(文字列)の画像検索
- 「r18search:(文字列)」でR18の(文字列)の画像検索
- 検索結果がなければ「Nobody here but us chickens!」と発言
- helpで使い方教えてくれます。親切。

自分がやりたかったことは実装できました。ただ、レスポンスが遅いのが気になります。本家もそんなに早くないから許容かなぁ。
Google画像検索とかBing画像検索なんかも連携もしたかったのですが、どうもあの辺はいまでは使えなくなっているようでした。ｶﾅｼｲ。

レスポンスは遅いですが、なんだかんだ使えてます。
今後欲しい機能として、英語圏のサービスなのでtagがわからない場合もあり、部分一致でtagの検索をするのも欲しいかな。
それか、tag自体を検索できるようにするか。
APIの説明読むとtagもリスト化できるみたいなのでやってみようと思います。