java8 spring-boot を練習しながら facebook messenger bot を書いてみた

# java8 + spring-boot で facebook messenger bot を書きました

http://qiita.com/amanoiverse/items/742a963be069e1902269 の fb messenger 版

- callback は https://...:443/fbmessengerbot に
- 必要情報は環境変数で
- jackson, httpclient, lombok 使用
- try catch はノーポリシー + 過剰な例外回避

## controller

```java
@RestController
@val
public class FBMessengerBotController {
	@Autowired
	FBMessengerBotService botService;

	@RequestMapping(value = "/fbmessengerbot", method = RequestMethod.GET)
	String verify(HttpServletRequest request) throws RuntimeException {
		return botService.verify(request);
	}
	@RequestMapping(value = "/fbmessengerbot", method = RequestMethod.POST)
	String message(HttpServletRequest request) throws RuntimeException {
		return botService.sentToMessenger(request);
	}
}
```

## model

```java
@Data
public class FBMessengerBotWebhook {
	String object;
	List<FBMessengerBotWebhookEntry> entry;
}
@Data
public class FBMessengerBotWebhookEntry {
	BigDecimal id;
	BigDecimal time;
	List<FBMessengerBotWebhookEntryMessaging> messaging;
}
@Data
public class FBMessengerBotWebhookEntryMessaging {
	Map<String, BigDecimal> sender;
	Map<String, BigDecimal> recipient;
	BigDecimal timestamp;
	FBMessengerBotWebhookEntryMessagingMessage message;
}
@Data
public class FBMessengerBotWebhookEntryMessagingMessage {
	String mid;
	BigDecimal seq;
	String text;
}

@Data
public class FBMessengerBotWebhookRecipient {
	Map<String, BigDecimal> recipient;
	Map<String, String> message;

}
```

## service

```java
@val
@Slf4j
@Service
public class FBMessengerBotService {
	final String FBMESSENGERBOT_VERIFY_TOKEN = System.getenv("FBMESSENGERBOT_VERIFY_TOKEN");
	final String FBMESSENGERBOT_ACCESS_TOKEN = System.getenv("FBMESSENGERBOT_ACCESS_TOKEN");
	final String FBMESSENGERBOT_ENDPOINT = "https://graph.facebook.com/v2.6/me/messages";

	public String verify(HttpServletRequest request) {
		try {
			val paramMap = request.getParameterMap();

			// debug
			val mapStream = paramMap.entrySet().stream();
			mapStream.forEach(e -> log.info(String.format("%s:%s", e.getKey(), String.join("", e.getValue()))));

			if (paramMap.get("hub.mode")[0].equals("subscribe")
					&& paramMap.get("hub.verify_token")[0].equals(FBMESSENGERBOT_VERIFY_TOKEN)) {
				return String.join("", paramMap.get("hub.challenge"));
			}
		} catch (Exception ex) {
			ex.printStackTrace();
		}
		return "failed";
	}

	public String sentToMessenger(HttpServletRequest request) {
		try {
			val jb = new StringBuffer();
			request.getReader().lines().forEach(jb::append);
			log.info(jb.toString());

			val mapper = new ObjectMapper();
			val botResponse = mapper.readValue(jb.toString(), FBMessengerBotWebhook.class);
			log.error("!!!" + ToStringBuilder.reflectionToString(botResponse));
			botResponse.getEntry().forEach(e -> e.getMessaging().stream().forEach(this::sendMessage));
		} catch (UnrecognizedPropertyException ex) {
			;
		} catch (Exception e) {
			e.printStackTrace();
		}
		return "ok";
	}

	void sendMessage(FBMessengerBotWebhookEntryMessaging messaging) {
		try {
			val message = messaging.getMessage();
			val text = message.getText();
			log.info(text);
			Stream<String> stream = Arrays.stream(text.split(""));
			val builder = new URIBuilder(FBMESSENGERBOT_ENDPOINT);
			builder.setParameter("access_token", FBMESSENGERBOT_ACCESS_TOKEN);
			val recipient = new FBMessengerBotWebhookRecipient();
			recipient.setRecipient(messaging.getSender());
			val post = new HttpPost(builder.build());
			post.setHeader("Content-Type", "application/json; charset=UTF-8");
			try (val httpclient = HttpClients.createDefault()) {
				stream.forEach(e -> sendOneRequest(httpclient, post, recipient, e));
			}
		} catch (IOException e) {
			e.printStackTrace();
		} catch (URISyntaxException e) {
			e.printStackTrace();
		}
	}

	void sendOneRequest(CloseableHttpClient client, HttpPost post, FBMessengerBotWebhookRecipient recipient,
			String oneString) {
		try {
			Map<String, String> message = new HashMap<>();
			message.put("text", oneString);
			recipient.setMessage(message);

			val mapper = new ObjectMapper();
			val json = mapper.writeValueAsString(recipient);
			log.info(json);

			post.setEntity(new StringEntity(json, StandardCharsets.UTF_8));
			val res = client.execute(post);

			Arrays.stream(res.getAllHeaders()).forEach(e -> log.info(e.toString()));
			try (val br = new BufferedReader(
					new InputStreamReader(res.getEntity().getContent(), StandardCharsets.UTF_8))) {
				br.lines().forEach(e -> log.info(e.toString()));
			} catch (IOException e) {
				e.printStackTrace();
			}
			log.info("JSON:" + json);
			log.info("STATUSLINE:" + res.getStatusLine().toString());
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}
```


## build.gradle

```groovy
dependencies {
	compileOnly('org.projectlombok:lombok')
	compile('org.springframework.boot:spring-boot-starter-web')
	compile('com.fasterxml.jackson.core:jackson-databind:2.7.0')
	compile('com.fasterxml.jackson.core:jackson-annotations:2.7.0')
	compile('com.fasterxml.jackson.core:jackson-core:2.7.0')
	compile('org.apache.httpcomponents:httpasyncclient:4.1.1')
	compile('org.apache.httpcomponents:httpcore-nio:4.4.4')
}

task wrapper(type: Wrapper) {
	gradleVersion = '2.12'
}
```


- なんか、bot自身の発言でも callback が来るので、 json パースエラーを catch して握りつぶす。
- Organize Import すると、`Unexpected error in organize imports. See log for details AST must not be null` っていわれる(ここが一番はまった)。**回避するには val を全部あきらめる。**Organize Import ないと生きていけない。
    - lombok version は 1.6.8 かな。。。。過去の changelog で AST... についての修正的なのが書いてあるが、最新でもだめだった。
    - ということで、今後は val 全面的にやめようと思う。