package com.integralblue.availability.service.impl;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.actuate.endpoint.InfoEndpoint;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.stereotype.Service;
import org.springframework.web.util.UriComponentsBuilder;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.integralblue.availability.model.Availability;
import com.integralblue.availability.model.FreeBusyStatus;
import com.integralblue.availability.model.Room;
import com.integralblue.availability.properties.ApplicationProperties;
import com.integralblue.availability.service.AvailabilityService;
import com.integralblue.availability.service.SlackMessageService;
import com.ullink.slack.simpleslackapi.SlackSession;
import com.ullink.slack.simpleslackapi.SlackUser;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
@Service
@Slf4j
public class SlackMessageServiceImpl implements SlackMessageService {
@Autowired
protected ApplicationProperties applicationProperties;
@Autowired
@Qualifier("slackAvailabilityService")
private AvailabilityService availabilityService;
@Autowired
private InfoEndpoint infoEndpoint;
@Autowired(required=false)
private SlackSession slackSession;
@Autowired
private ObjectMapper objectMapper;
private static final Pattern ROOM_LIST_PATTERN = Pattern.compile("rooms (.+?)\\s*", Pattern.CASE_INSENSITIVE);
private static final String DISPLAY_AVAILABILITY_NOT_TODAY_FORMAT_PATTERN = "h:mm a 'on' EEE";
private static final String DISPLAY_AVAILABILITY_TODAY_FORMAT_PATTERN = "h:mm a";
private static final Pattern SLACK_AT_USER_REFERENCE = Pattern.compile("@(.+)");
private static final Pattern SLACK_BRACKETED_USER_REFERENCE = Pattern.compile("<@(.+)>");
private static final Pattern EMAIL_ADDRESS = Pattern.compile("([^<>@\"'|]+@[^<>@\"'|]+)");
private static final Pattern MAILTO_EMAIL_ADDRESS = Pattern.compile("<mailto:([^<>@\"'|]+@[^<>@\"'|]+)\\|.*?>");
@Override
public String respondToMessage(Optional<SlackUser> optionalMessageSender, @NonNull String message) {
final TimeZone timeZone = optionalMessageSender.map(s -> {
return TimeZone.getTimeZone(s.getTimeZone());
}).orElse(TimeZone.getDefault());
try {
if("help".equalsIgnoreCase(message)){
return "Determine a user, room, or list of rooms availability:\n" +
"You can send any of these commands by direct messaging the bot directly, sending an @ message to the bot, or using the /avail command:\n" +
"`rooms [list name]` Get the availability for all rooms in a given list\n" +
"`list of [emailaddress] or [@slackusername]` Get the availability of a room or user by email address or slack name\n" +
"for example:\n" +
"`rooms boston` will tell you the availability of all rooms in the boston list\n" +
"`@bob.jones bill.gates@microsoft.com` will tell you the availability of Bob and Bill";
}
if("info".equalsIgnoreCase(message)){
return " ```" + objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(infoEndpoint.invoke()) + " ```";
}
StringBuilder ret = new StringBuilder();
ret.append("All times are in your timezone (" + timeZone.getDisplayName() + ")\n");
final Matcher roomListMatcher = ROOM_LIST_PATTERN.matcher(message);
if(roomListMatcher.matches()){
ret.append(getTextForRoomList(roomListMatcher.group(1), timeZone));
return ret.toString();
}
List<String> emailAddresses = new ArrayList<>();
for(String s : message.split(" ")){
final Matcher slackAtUserReferenceMatcher = SLACK_AT_USER_REFERENCE.matcher(s);
if(slackAtUserReferenceMatcher.matches()){
// convert the Slack @ reference to an email address
if(slackSession==null){
ret.append("Sorry, but this integration is not configured to be able to look up slack references");
}else{
SlackUser slackUser = slackSession.findUserByUserName(slackAtUserReferenceMatcher.group(1));
if(slackUser == null){
ret.append("No slack user was found with the username `" + slackAtUserReferenceMatcher.group(1) + "`\n");
}else{
emailAddresses.add(slackUser.getUserMail());
}
}
continue;
}
final Matcher slackBracketedUserReferenceMatcher = SLACK_BRACKETED_USER_REFERENCE.matcher(s);
if(slackBracketedUserReferenceMatcher.matches()){
// convert the Slack <@> reference to an email address
if(slackSession==null){
ret.append("Sorry, but this integration is not configured to be able to look up slack references");
}else{
SlackUser slackUser = slackSession.findUserById(slackBracketedUserReferenceMatcher.group(1));
if(slackUser == null){
ret.append("No slack user was found with the userid `" + slackBracketedUserReferenceMatcher.group(1) + "`\n");
}else{
emailAddresses.add(slackUser.getUserMail());
}
}
continue;
}
final Matcher emailAddressMatcher = EMAIL_ADDRESS.matcher(s);
if(emailAddressMatcher.matches()){
emailAddresses.add(emailAddressMatcher.group(1));
continue;
}
final Matcher mailtoEmailAddressMatcher = MAILTO_EMAIL_ADDRESS.matcher(s);
if(mailtoEmailAddressMatcher.matches()){
emailAddresses.add(mailtoEmailAddressMatcher.group(1));
continue;
}
ret.append("`" + s + "` is not an email address or slack @ reference\n");
}
if(! emailAddresses.isEmpty()){
ret.append(getTextForUsersStatus(emailAddresses, timeZone));
}
return ret.toString();
} catch (Exception e) {
log.error("Error while trying to process message: {}", message, e);
return "Error: `" + e.getLocalizedMessage() + "`";
}
}
private String getTextForRoomList(String roomList, TimeZone timeZone) {
return availabilityService.getRoomListAvailability(roomList)
.map(roomToOptionalAvailability -> "*" + linkToRoomList(roomList) + "*\n"
+ roomToOptionalAvailability.entrySet().stream()
.map(entry -> "> " + optionalAvailabilityToEmoji(entry.getValue())
+ linkToRoom(entry.getKey()) + ": "
+ entry.getValue()
.map(availability -> availability.getStatusAtStart().toString()
+ (availability.getStatusAtStart() == FreeBusyStatus.FREE ? ""
: (" Next available at " + formatNextFree(availability.getNextFree(), timeZone))))
.orElse("does not exist (error)"))
.collect(Collectors.joining("\n")))
.orElse("room list " + roomList + " was not found");
}
private String optionalAvailabilityToEmoji(Optional<Availability> optionalAvailability) {
return optionalAvailability.map(availability -> {
switch (availability.getStatusAtStart()) {
case BUSY:
return ":no_entry:";
case FREE:
return ":white_check_mark:";
case TENTATIVE:
return ":question:";
default:
throw new IllegalStateException();
}
}).orElse(":exclamation:");
}
private String getTextForUsersStatus(List<String> emailAddresses, TimeZone timeZone) {
return availabilityService.getAvailability(emailAddresses, new Date(), new Date()).entrySet().stream()
.map(entry -> entry.getValue().map(availability -> {
String returnText = optionalAvailabilityToEmoji(Optional.of(availability))
+ linkToUser(entry.getKey()) + " is " + availability.getStatusAtStart() + ".";
Date nextAvailable = availability.getNextFree();
if (availability.getStatusAtStart() != FreeBusyStatus.FREE) {
returnText += " Next available at " + formatNextFree(nextAvailable, timeZone) + ".";
}
return returnText;
}).orElse("user " + entry.getKey() + " not found")).collect(Collectors.joining("\n"));
}
private String formatNextFree(Date nextFree, TimeZone timeZone){
DateFormat displayDateFormat;
if(LocalDateTime.ofInstant(nextFree.toInstant(), timeZone.toZoneId()).toLocalDate().equals(LocalDate.now(timeZone.toZoneId()))){
displayDateFormat = new SimpleDateFormat(DISPLAY_AVAILABILITY_TODAY_FORMAT_PATTERN);
}else{
displayDateFormat = new SimpleDateFormat(DISPLAY_AVAILABILITY_NOT_TODAY_FORMAT_PATTERN);
}
displayDateFormat.setTimeZone(timeZone);
return displayDateFormat.format(nextFree);
}
private String linkToRoomList(String roomList) {
return "<"
+ UriComponentsBuilder.fromUri(applicationProperties.getBaseUri()).path("list/{roomList}/availability").buildAndExpand(Collections.singletonMap("roomList", roomList)).toUriString()
+ "|" + roomList + ">";
}
private String linkToUser(String email) {
return "<"
+ UriComponentsBuilder.fromUri(applicationProperties.getBaseUri()).path("user/{user}/availability").buildAndExpand(Collections.singletonMap("user", email)).toUriString()
+ "|" + email + ">";
}
private String linkToRoom(Room room) {
return "<"
+ UriComponentsBuilder.fromUri(applicationProperties.getBaseUri()).path("user/{user}/availability").buildAndExpand(Collections.singletonMap("user", room.getEmailAddress())).toUriString()
+ "|" + room.getName() + ">";
}
}