ForwardToSlackChannelAction.java

/*
 * Copyright 2016 RedRoma, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package tech.aroma.application.service.reactions.actions;

import com.google.common.base.Strings;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Objects;
import org.apache.thrift.TException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sir.wellington.alchemy.collections.lists.Lists;
import tech.aroma.thrift.Message;
import tech.aroma.thrift.exceptions.InvalidArgumentException;
import tech.aroma.thrift.exceptions.OperationFailedException;
import tech.aroma.thrift.reactions.ActionForwardToSlackChannel;
import tech.sirwellington.alchemy.annotations.access.Internal;
import tech.sirwellington.alchemy.annotations.designs.patterns.StrategyPattern;
import tech.sirwellington.alchemy.annotations.objects.Pojo;
import tech.sirwellington.alchemy.http.AlchemyHttp;
import tech.sirwellington.alchemy.http.HttpResponse;
import tech.sirwellington.alchemy.http.exceptions.AlchemyHttpException;

import static tech.sirwellington.alchemy.annotations.designs.patterns.StrategyPattern.Role.CONCRETE_BEHAVIOR;
import static tech.sirwellington.alchemy.arguments.Arguments.checkThat;
import static tech.sirwellington.alchemy.arguments.assertions.Assertions.notNull;
import static tech.sirwellington.alchemy.arguments.assertions.NetworkAssertions.validURL;

/**
 *
 * @author SirWellington
 */
@Internal
@StrategyPattern(role = CONCRETE_BEHAVIOR)
final class ForwardToSlackChannelAction implements Action
{

    private final static Logger LOG = LoggerFactory.getLogger(ForwardToSlackChannelAction.class);

    private final ActionForwardToSlackChannel slack;
    private final AlchemyHttp http;

    ForwardToSlackChannelAction(ActionForwardToSlackChannel slack, AlchemyHttp http)
    {
        checkThat(slack, http)
            .are(notNull());

        this.slack = slack;
        this.http = http;
    }

    @Override
    public List<Action> actOnMessage(Message message) throws TException
    {
        Action.checkMessage(message);

        checkThat(slack.webhookUrl)
            .throwing(InvalidArgumentException.class)
            .usingMessage("Slack Webhook URL is not a valid URL: " + slack.webhookUrl)
            .is(validURL());

        Payload payload = createPayloadFor(message);

        URL webhookUrl;

        try
        {
            webhookUrl = new URL(slack.webhookUrl);
        }
        catch (MalformedURLException ex)
        {
            LOG.error("Failed to convert Slack Webhook URL: {}", slack.webhookUrl, ex);
            throw new OperationFailedException("Could not convert URL: " + ex.getMessage());
        }

        LOG.debug("Sending Message Payload to {} for Message {}", webhookUrl, message.title);

        http.go()
            .post()
            .body(payload)
            .accept("application/json", "text/plain", "text/javascript")
            .onSuccess(this::onSuccess)
            .onFailure(this::onFailure)
            .at(webhookUrl);

        return Lists.emptyList();
    }

    private void onSuccess(HttpResponse response)
    {
        LOG.debug("Successfully sent Slack Message | {}", response);
    }

    private void onFailure(AlchemyHttpException ex)
    {
        LOG.error("Failed to post Slack Message to {}", slack, ex);
    }

    private Payload createPayloadFor(Message message)
    {

        Field titleField = new Field();
        titleField.title = message.title;
        titleField.value = message.body;

        Field deviceField = new Field();
        deviceField.title = "From Device";
        deviceField.value = message.hostname;

        Attachment attachment = new Attachment();

        switch (message.urgency)
        {
            case HIGH:
                attachment.color = Attachment.COLOR_HIGH;
                break;
            case MEDIUM:
                attachment.color = Attachment.COLOR_MEDIUM;
                break;
            default:
                attachment.color = Attachment.COLOR_LOW;
                break;
        }

        attachment.fields = Lists.createFrom(titleField, deviceField);

        Payload payload = new Payload();
        payload.attachments.add(attachment);
        payload.text = String.format("*%s* - %s", message.applicationName, message.title);

        if (!Strings.isNullOrEmpty(slack.slackChannel))
        {
            payload.channel = slack.slackChannel;
        }

        return payload;
    }

    @Override
    public String toString()
    {
        return "ForwardToSlackChannelAction{" + "slack=" + slack + ", http=" + http + '}';
    }

    @Pojo
    @Internal
    static class Payload
    {

        private String icon_url = "https://raw.githubusercontent.com/RedRoma/Aroma/develop/Graphics/Logo.png";
        private String username = "Aroma";
        private String text = null;
        private String channel;
        private Boolean mrkdwn = true;

        private List<Attachment> attachments = Lists.create();

        @Override
        public int hashCode()
        {
            int hash = 7;
            hash = 17 * hash + Objects.hashCode(this.icon_url);
            hash = 17 * hash + Objects.hashCode(this.username);
            hash = 17 * hash + Objects.hashCode(this.text);
            hash = 17 * hash + Objects.hashCode(this.attachments);
            return hash;
        }

        @Override
        public boolean equals(Object obj)
        {
            if (this == obj)
            {
                return true;
            }
            if (obj == null)
            {
                return false;
            }
            if (getClass() != obj.getClass())
            {
                return false;
            }
            final Payload other = (Payload) obj;
            if (!Objects.equals(this.icon_url, other.icon_url))
            {
                return false;
            }
            if (!Objects.equals(this.username, other.username))
            {
                return false;
            }
            if (!Objects.equals(this.text, other.text))
            {
                return false;
            }
            if (!Objects.equals(this.attachments, other.attachments))
            {
                return false;
            }
            return true;
        }

        @Override
        public String toString()
        {
            return "Payload{" + "icon_url=" + icon_url + ", username=" + username + ", text=" + text + ", attachments=" + attachments + '}';
        }

    }

    @Pojo
    @Internal
    static class Attachment
    {

        private static final String COLOR_LOW = "#037AFF";
        private static final String COLOR_MEDIUM = "#F8E71C";
        private static final String COLOR_HIGH = "#FB3E3C";

        private String fallback;
        private String pretext;
        private String color;
        private List<Field> fields = Lists.create();

        @Override
        public int hashCode()
        {
            int hash = 5;
            hash = 71 * hash + Objects.hashCode(this.fallback);
            hash = 71 * hash + Objects.hashCode(this.pretext);
            hash = 71 * hash + Objects.hashCode(this.color);
            hash = 71 * hash + Objects.hashCode(this.fields);
            return hash;
        }

        @Override
        public boolean equals(Object obj)
        {
            if (this == obj)
            {
                return true;
            }
            if (obj == null)
            {
                return false;
            }
            if (getClass() != obj.getClass())
            {
                return false;
            }
            final Attachment other = (Attachment) obj;
            if (!Objects.equals(this.fallback, other.fallback))
            {
                return false;
            }
            if (!Objects.equals(this.pretext, other.pretext))
            {
                return false;
            }
            if (!Objects.equals(this.color, other.color))
            {
                return false;
            }
            if (!Objects.equals(this.fields, other.fields))
            {
                return false;
            }
            return true;
        }

        @Override
        public String toString()
        {
            return "Attachment{" + "fallback=" + fallback + ", pretext=" + pretext + ", color=" + color + ", fields=" + fields + '}';
        }

    }

    @Pojo
    @Internal
    static class Field
    {

        private String title;
        private String value;
        private boolean isShort = false;

        @Override
        public int hashCode()
        {
            int hash = 3;
            hash = 97 * hash + Objects.hashCode(this.title);
            hash = 97 * hash + Objects.hashCode(this.value);
            hash = 97 * hash + (this.isShort ? 1 : 0);
            return hash;
        }

        @Override
        public boolean equals(Object obj)
        {
            if (this == obj)
            {
                return true;
            }
            if (obj == null)
            {
                return false;
            }
            if (getClass() != obj.getClass())
            {
                return false;
            }
            final Field other = (Field) obj;
            if (this.isShort != other.isShort)
            {
                return false;
            }
            if (!Objects.equals(this.title, other.title))
            {
                return false;
            }
            if (!Objects.equals(this.value, other.value))
            {
                return false;
            }
            return true;
        }

        @Override
        public String toString()
        {
            return "Field{" + "title=" + title + ", value=" + value + ", isShort=" + isShort + '}';
        }

    }

}