JavaScriptを動かすことができるWordpress用チャットボット

一応プラグインが存在しますが、いろいろとあるので（特にチャットボットでJSを動かしたい）
単純なサーチ系のを作ってみました

入力キーワードのHTMLはすべてエスケープして受け付けませんが
botはほぼすべてのHTMLタグを返すことができます。

あなたの実装＆運用能力で、月額１万は少なくともかかるチャットボット費用を節減することができます

ライセンス MIT (C) neet.co.jp

## 利用例（まだbeta版）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149448/257183fe-2063-939d-0d2f-9c80caf6daa1.png)

:::note info
永遠の18才を実現するために、誕生日の西暦はJavaScriptで描画しています
:::

### 永遠の１８才のコード(2/10 誕生日用）
```
＊＊！<script>var rnow = new Date(); var ryear = rnow.getFullYear(); var rmonth = rnow.getMonth() + 1; var rdate = rnow.getDate(); var rino=ryear - 18;if(rmonth == 1) { rino--; } if(rmonth == 2 && rday < 10) { rino--; } $('.rtanjoubi').text(rino);</script><span class="rtanjoubi"></span>年2月10日産まれだよ！

```

:::note info
終了と入力されたら、location.hrefもしていいはず
:::

## 簡単に使い方
検索キーワードは半角カンマで区切ってください

HTMLタグを埋め込むには、テキストにして、かつ管理者で操作を行ってください

## コード

```functions.php
date_default_timezone_set('Asia/Tokyo');
parse_str($_SERVER['QUERY_STRING'], $queue);

if($queue['jo'] != "") {
	require 'json.php';
	jsonexec($queue);
	exit;
}


function searchChat( $keyword ) {
	$args = array(
			'posts_per_page' => 10000,// デフォはかなり多め
			'orderby' => 'date',// 更新日
			'order' => 'DESC',// 降順
			'post_type' => 'chatbot',// カスタム投稿タイプ
			'post_status' => 'publish',// 公開状態
			'suppress_filters' => true,
	);

	$chatarray = array();
	$titlearray = array();

	$loop = new WP_Query( $args );
	$get_num = $loop->post_count;

	$keyword = mb_convert_kana($keyword, "asHc");
	$keyword =strtolower($keyword);
	$input_keywords=explode(' ', $keyword);

	// タイトル完全合致だけで検索
	if( $loop->have_posts() ){
		while ( $loop->have_posts() ){
			$loop->the_post();
			$title = get_the_title();
			$desc = get_post_meta( $loop->post->ID , 'chatbot_desc' , true );

			$t = mb_convert_kana($title, "asHc");
			$t=strtolower($t);
			if($keyword === $t) {
				$chatarray[] = array(
					'rsstitle'=>$title,
					'rssdesc'=>$desc,
				);
				return $chatarray;
			}
		}
	}
	wp_reset_postdata();

	// descで検索
	if( $loop->have_posts() ){
		while ( $loop->have_posts() ){
			$loop->the_post();
			$title = get_the_title();
			$desc = get_post_meta( $loop->post->ID , 'chatbot_desc' , true );
			$sd = mb_convert_kana($desc, "asHc");
			$sd = strtolower($sd);
			foreach($input_keywords as $ik) {
				$flg=0;
				if(strpos($sd, $ik) !== false) {
					foreach ($titlearray as $tt) {
						if($title == $tt) {
							$flg++;
						}
					}

					if(!$flg) {
						$chatarray[] = array(
							'rsstitle'=>$title,
							'rssdesc'=>$desc,
						);
						$titlearray[] = $title;
					}
				}
			}
		}
	}

	wp_reset_postdata();

	// キーワード一部一致で検索
	if( $loop->have_posts() ){
		while ( $loop->have_posts() ){
			$loop->the_post();
			$title = get_the_title();
			$content = get_the_content();
			$thumbnail_id = get_post_thumbnail_id();
			$search = get_post_meta( $loop->post->ID , 'chatbot_search' , true );
			$desc = get_post_meta( $loop->post->ID , 'chatbot_desc' , true );

			$search_keywords=explode(',', $search);

			foreach($search_keywords as $se){
				$se = mb_convert_kana($se, "asHc");
				$se=strtolower($se);
				foreach($input_keywords as $ik){
					if(strpos($se, $ik) !== false) {
						$flg=0;
						foreach ($titlearray as $tt) {
							if($title == $tt) {
								$flg++;
							}
						}

						if(!$flg) {
							$chatarray[] = array(
								'rsstitle'=>$title,
								'rssdesc'=>$desc,
							);
							$titlearray[] = $title;
						}
					} else {
						if(strpos($ik, $se) !== false) {
							$flg=0;
							foreach ($titlearray as $tt) {
								if($title == $tt) {
									$flg++;
								}
							}

							if(!$flg) {
								$chatarray[] = array(
									'rsstitle'=>$title,
									'rssdesc'=>$desc,
								);
								$titlearray[] = $title;
							}
						}
					}
				}

			}
		}

	}
	wp_reset_postdata();
	return $chatarray;
}

function create_post_type_banner(){
	$args = [
			'label' => 'チャットbot',//投稿タイプの名前
			'labels' => array(
					'singular_name' => 'チャットbot',//投稿タイプの名前
					'menu_name' => 'チャットbot',//メニュー（画面の左）に表示するラベル
					'add_new_item' => '新規投稿を追加',//新規作成ページの左上に表示されるタイトル
					'add_new' => '新規追加',//メニュー（画面の左）の「新規」の位置に表示するラベル
					'new_item' => '新規投稿',//一覧ページの右上にある新規作成ボタンのラベル
					'edit_item'=>'投稿を編集',//編集ページの左上にあるタイトル
					'view_item' => '投稿を表示',//編集ページの「○○を表示」ボタンのラベル
					'not_found' => '投稿は見つかりませんでした',//カスタム投稿を追加していない状態で、カスタム投稿一覧ページを開いたときに表示されるメッセージ
					'not_found_in_trash' => 'ゴミ箱に投稿はありません。',//カスタム投稿をゴミ箱に入れていない状態で、カスタム投稿のゴミ箱ページを開いたときに表示されるメッセージ
					'search_items' => '投稿を検索',//一覧ページの検索ボタンのラベル
			),

			'public' => false,//ユーザーが管理画面で入力するか設定
			'publicly_queryable' => false,//カスタム投稿タイプの機能でページを生成するかどうかを指定
			'show_ui' => true,//管理画面にこのカスタム投稿タイプのページを表示するか設定
			'show_in_menu' => true,//管理画面にメニュー出すか設定
			'query_var' => true,
			'has_archive' => true,//「true」に指定すると投稿した記事の一覧ページ（投稿タイプのトップページ）を作成することができる
			'hierarchical' => true,//カスタム投稿に固定ページのような親子関係（階層）を持たせるか設定
			'menu_position' => 5,//カスタム投稿のメニューを追加する位置を整数で指定
			'menu_icon' => 'dashicons-images-alt2',
			'rewrite' => array('slug' => 'chatbot'),//リライト設定

			'supports' => array (
					'title', // タイトル
					//'editor', // 本文
					'author', // 作成者
					//'thumbnail', // アイキャッチ画像
					//'excerpt', // 抜粋
					//'comments', // コメント一覧
					//'trackbacks', // トラックバック送信
					//'custom-fields', // カスタムフィールド
					//'revisions', // リビジョン
					//'page-attributes'
			)

	];
	register_post_type("chatbot",$args);
}

add_action( "admin_init", "metaboxs_init_chatbot" );

function metaboxs_init_chatbot () { // 投稿編集画面にメタボックスを追加する
	add_meta_box( 'chatbot_infomation_id', 'チャットbot', 'chatbot_infomation', 'chatbot' );
	add_action( 'save_post', 'save_chatbot_infomation' );
}

function chatbot_infomation () {
	global $post;
	static $active;
	wp_nonce_field(wp_create_nonce(__FILE__), 'chatbot_nonce');

	echo '<label for="chatbot_search">検索キーワード</label><br><input type="text" name="chatbot_search" id="chatbot_search" value="'.get_post_meta($post->ID, 'chatbot_search', true).'" style="width:100%"><br>';

	wp_editor(get_post_meta($post->ID, 'chatbot_desc', true)
		, "chatbot_desc", $settings = array() );

}

function save_chatbot_infomation ($post_id) {
	$my_nonce = isset($_POST['chatbot_nonce']) ? $_POST['chatbot_nonce'] : null;
	if(!wp_verify_nonce($my_nonce, wp_create_nonce(__FILE__))) {
		return $post_id;
	}
	if(defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { return $post_id; }
	if(!current_user_can('edit_post', $post_id)) { return $post_id; }

	$data["chatbot_search"] = $_POST['chatbot_search'];
	$data["chatbot_desc"] = $_POST['chatbot_desc'];

	$project_id="11";	// たぶん11

//	require_once( './wp-admin/includes/taxonomy.php' );
	$args = array(
	    'cat_name' => $data["banner_title"],
	    'category_description' => "自動追加",
	    'category_nicename' => $data["banner_slag"],
	    'category_parent' => $project_id
	);
	$new_id = wp_insert_category( $args );


	foreach($data as $key => $val){
		if(get_post_meta($post_id, $key) == ""){
			add_post_meta($post_id, $key, $val, true);
		}elseif($data != get_post_meta($post_id, $key, true)){
			update_post_meta($post_id, $key, $val);
		}elseif($data == ""){
			delete_post_meta($post_id, $key, get_post_meta($post_id, $key, true));
		}
	}
}
```

```chat.php
<?php
/**
 * Template Name: チャットボット
 */

wp_enqueue_style('chat',
	get_template_directory_uri() . "/css/chat.min.css",
    array(),
    date_i18n( 'YmdHis', filemtime( dirname(__FILE__). '/css/chat.min.css' ) )
);

wp_enqueue_script('chat',
	get_template_directory_uri() . "/js/chat.min.js",
    array(),
    date_i18n( 'YmdHis', filemtime( dirname(__FILE__). '/js/chat.min.js' ) )
);

get_header("nolink");

?>
<section class="chatlog">
</section>
<section class="chatinput">
	<form onsubmit="return false;">
		<input type="text" name="text" placeholder="聞いてみたいことを簡単に書いてみてね" maxlength="30" autocomplete="off">
		<input type="button" name="send" value="送信">
	</form>
</section>

<?php get_footer("nohtml"); ?>
```

:::note info
footerは１文字も出力しないようにしたほうがよさそうです
:::

```json.php
<?php
/* chat bot用のJSON */

function jsonexec($queue) {
/* JSONでchat botを出力 */
	header('Content-type: application/json; charset="UTF-8"');
	header('Cache-Control: max-age=0');
	header('Cache-Control: no-store, no-cache, must-revalidate' );
	header('Cache-Control: post-check=0, pre-check=0', FALSE );
	header('Pragma: no-cache');

	
	if($queue['jo'] == "chat") {
		$chatarray=searchChat($queue['chat']);
	}

	foreach($chatarray as $chat){
		$joarray[] = [
			"t"=>$chat['rsstitle'],
			"d"=>$chat['rssdesc'],
		];
	}

	usort($joarray, "sortchat");

	echo json_encode($joarray, JSON_UNESCAPED_UNICODE |  JSON_UNESCAPED_SLASHES);

}

function sortchat($a, $b) {
	if($a['t'] == $b['t']) {
		return 0;
	}
	return $a['t'] < $b['t'] ? -1 : 1;
}


?>
```


```chat.sjs
$(function() {
	var botname="bot",
		botimage="image.svg",
		username="あなた",
		userimage="user.svg";
		chatstartmsg="チャットボットの開始！";
		chatfailmsg="サーバーエラーです！",
		chatnotfoundmsg="回答がありません！";

	$.ajaxSetup({async: true});

	bottalk(chatstartmsg);

	$('input[type="text"]').keydown(function(e){
		if(e.keyCode === 27)
			$('input[type="text"]').val("");

		if(e.keyCode === 13)
			send();
	});

	$('input[type="button"').on("keydown click", function(e) {
		send();
	});

	function send() {
		var chat=escape_html($('input[type="text"]').val());

		if(chat == "") return;

		usertalk(chat);
		$('input[type="text"]').val("");

		chatjson(chat);
	}

	function chatjson(keyword) {
		var url=
			sprintf(
				"/?jo=chat&chat=%s"
				, encodeURI(keyword)
			);

		$.getJSON(url)
			.done(function(json){ // jsonの読み込みに成功した時
				setTimeout(function() {
					chathtml(json, keyword);
				}, 500);
			})
			.fail(function(){ // jsonの読み込みに失敗した時
				bottalk(chatfailmsg);
			});
	}

	function chathtml(json, keyword) {
		if(json == null) {
			bottalk(chatnotfoundmsg);
		} else if(json.length == 1) {
			if(json[0].t == keyword)
				bottalk(json[0].d);
			else
				bottalk(sprintf("<strong>%s</strong><br>%s", json[0].t, json[0].d));
		} else {
			var str="";
			json.forEach(function(t) {
				str += '<a class="talk" href="#' + encodeURI(t.t) + '">' + t.t + "</a><br>";
			});
			bottalk(str);
		}
	}

	$(document).on('click', '.talk', function() {
//alert("a");
		var destinationPage = $(this).attr('href');
//alert(destinationPage);
		destinationPage = decodeURI(destinationPage.replace('#', ''));
		usertalk(destinationPage);
		chatjson(destinationPage);
		return false;
	});

	var chatid=1;

	function usertalk(chat) {
		var html='\
<div class="user" id="chat' + chatid + '">\
<div>' + chat + '</div>\
<img src="' + userimage + '">\
</div>';
		$(".chatlog").append(html);
		chatscroll(chatid);
		chatid++;
	}

	function bottalk(chat) {
		html='\
<div class="bot" id="chat' + chatid + '">\
<img src="' + botimage + '">\
<div>' + chat + '</div>\
</div>';
		$(".chatlog").append(html);
		chatscroll(chatid);
		chatid++;
	}

	function chatscroll(chatid) {

		chatAreaHeight = $('.chatlog').get(0).scrollHeight;
		$('.chatlog').animate({scrollTop: chatAreaHeight}, 300);

	}

function escape_html (string) {
	if(typeof string !== 'string') {
		return string;
	}
	return string.replace(/[&'`"<>]/g, function(match) {
		return {
			'&': '&amp;',
			"'": '&#x27;',
			'`': '&#x60;',
			'"': '&quot;',
			'<': '&lt;',
			'>': '&gt;',
		}[match]
	});
}
});
```

```chat.scss
:root {
	--chat-color: #222;
	--bg-color: #bed887;
	--link-color: #000;
	--link-bg-color: #eeffaa;
}

body.Dark {
	--chat-color: #eee;
	--bg-color: #4b6514;
	--link-color: #fff;
	--link-bg-color: #5d9200;
	--hover-color: #7db20f;
}

body {
	position: relative;
}

.chatlog {
	position: fixed;
	background: #088;
	margin: 0;
	padding: 0;
	display: block;
	width: 100vw;
	top: var(--header-total-size-with-border);
	height: calc(100vh - var(--header-total-size-with-border));
	overflow-y: scroll;
	padding-bottom: 2rem;
//	margin-bottom: 2rem;

	@include maq(sp) {
		height: calc(100vh - var(--header-total-size-with-border) - 8rem);
	}

	.bot {
		text-align: left;
		margin-left: 3rem;
		position: relative;
		margin-bottom: 1rem;

		@include maq(sp) {
			margin-left: 1rem;
		}

		img {
			margin: 0;
			padding: 0;
			vertical-align: top;
			margin-top: 0.5rem;
			width: 3rem;
			height: 3rem;
			background: #fff;
			border-radius: 50%;
		}
		// 吹き出し：https://saruwakakun.com/html-css/reference/speech-bubble
		div {
			vertical-align: top;
			display: inline-block;
			margin: 1rem 1rem 1.5rem 1rem;
			padding: 0.4rem 0.7rem;
			min-width: 3rem;
			max-width: 80vw;
			color: var(--chat-color);
			font-size: 0.8rem;
			background: var(--bg-color);
			border-radius: 0.5rem;

			a {
				background: var(--link-bg-color);
				color: var(--link-color);
				border-radius: 15%;
				padding: 0.2rem;
				margin: 0.1rem;
				text-decoration: none;
				&:hover {
					background: var(--hover-color);
				}
			}
		}
	}

	.user {
		text-align: right;
		margin-right: 3rem;
		margin-bottom: 1rem;
		position: relative;

		@include maq(sp) {
			margin-right: 1rem;
		}

		img {
			margin: 0;
			padding: 0;
			vertical-align: top;
			margin-top: 0.5rem;
			margin-bottom: 1rem;
			width: 3rem;
			height: 3rem;
			background: #fff;
			border-radius: 50%;
		}

		div {
			vertical-align: top;
			display: inline-block;
			margin: 1rem 1rem 1.5rem 0;
			padding: 0.4rem 0.7rem;
			min-width: 3rem;
			max-width: 80vw;
			color: var(--chat-color);
			font-size: 0.8rem;
			background: var(--bg-color);
			border-radius: 0.5rem;
		}
	}

}

.chatinput {
	position: fixed;
	height: 3rem;

	margin: 0;
	padding: 0;
	padding-bottom: 0.5rem;
	bottom: 0;
	background: #880;

	input[type="text"] {
		margin: 0;
		padding: 0;
		font-size: 1.5rem;
		background: #808;
		color: #fff;
		width: calc(100vw - 6rem);
	}

	input[type="button"] {
		margin: 0;
		padding: 0;
		font-size: 1.5rem;
		background: #808;
		color: #fff;
		width: 4rem;
	}
}

footer {
	position: fixed;
	top: -9999px;
	margin: 0;
	padding: 0;
	background: transparent;
	height: 0;

	p {
		margin: 0;
		padding: 0;
	}
}
```

## 応用

以下のようなコードを入れれば、ユーザー側のチャットに入力できます

```
<a class="talk" href="#URLエンコードしたキーワードか完全合致のタイトル">example</a>
```

## さいごに

コンパイルしてエラー出ると思いますが、価を付記してください
（特にJavaScript用sprintf）