/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.apache.nifi.processors.slack; import org.apache.nifi.annotation.behavior.DynamicProperty; import org.apache.nifi.annotation.behavior.InputRequirement; import org.apache.nifi.annotation.documentation.CapabilityDescription; import org.apache.nifi.annotation.documentation.Tags; import org.apache.nifi.annotation.lifecycle.OnScheduled; import org.apache.nifi.components.PropertyDescriptor; import org.apache.nifi.components.ValidationContext; import org.apache.nifi.components.ValidationResult; import org.apache.nifi.components.Validator; import org.apache.nifi.expression.AttributeExpression; import org.apache.nifi.flowfile.FlowFile; import org.apache.nifi.processor.AbstractProcessor; import org.apache.nifi.processor.ProcessContext; import org.apache.nifi.processor.ProcessSession; import org.apache.nifi.processor.Relationship; import org.apache.nifi.processor.util.StandardValidators; import javax.json.Json; import javax.json.JsonArrayBuilder; import javax.json.JsonObject; import javax.json.JsonObjectBuilder; import javax.json.JsonReader; import javax.json.JsonWriter; import javax.json.stream.JsonParsingException; import java.io.DataOutputStream; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; @Tags({"put", "slack", "notify"}) @CapabilityDescription("Sends a message to your team on slack.com") @InputRequirement(InputRequirement.Requirement.INPUT_REQUIRED) @DynamicProperty(name = "A JSON object to add to Slack's \"attachments\" JSON payload.", value = "JSON-formatted string to add to Slack's payload JSON appended to the \"attachments\" JSON array.", supportsExpressionLanguage = true, description = "Converts the contents of each value specified by the Dynamic Property's value to JSON and appends it to the payload being sent to Slack.") public class PutSlack extends AbstractProcessor { public static final PropertyDescriptor WEBHOOK_URL = new PropertyDescriptor .Builder() .name("webhook-url") .displayName("Webhook URL") .description("The POST URL provided by Slack to send messages into a channel.") .required(true) .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .addValidator(StandardValidators.URL_VALIDATOR) .sensitive(true) .build(); public static final PropertyDescriptor WEBHOOK_TEXT = new PropertyDescriptor .Builder() .name("webhook-text") .displayName("Webhook Text") .description("The text sent in the webhook message") .required(true) .expressionLanguageSupported(true) .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .build(); public static final PropertyDescriptor CHANNEL = new PropertyDescriptor .Builder() .name("channel") .displayName("Channel") .description("A public channel using #channel or direct message using @username. If not specified, " + "the default webhook channel as specified in Slack's Incoming Webhooks web interface is used.") .required(false) .expressionLanguageSupported(true) .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .build(); public static final PropertyDescriptor USERNAME = new PropertyDescriptor .Builder() .name("username") .displayName("Username") .description("The displayed Slack username") .required(false) .expressionLanguageSupported(true) .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .build(); public static final PropertyDescriptor ICON_URL = new PropertyDescriptor .Builder() .name("icon-url") .displayName("Icon URL") .description("Icon URL to be used for the message") .required(false) .expressionLanguageSupported(true) .addValidator(StandardValidators.URL_VALIDATOR) .build(); public static final PropertyDescriptor ICON_EMOJI = new PropertyDescriptor .Builder() .name("icon-emoji") .displayName("Icon Emoji") .description("Icon Emoji to be used for the message. Must begin and end with a colon, e.g. :ghost:") .required(false) .expressionLanguageSupported(true) .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .addValidator(new EmojiValidator()) .build(); public static final Relationship REL_SUCCESS = new Relationship.Builder() .name("success") .description("FlowFiles are routed to success after being successfully sent to Slack") .build(); public static final Relationship REL_FAILURE = new Relationship.Builder() .name("failure") .description("FlowFiles are routed to failure if unable to be sent to Slack") .build(); private final SortedSet<PropertyDescriptor> attachments = Collections.synchronizedSortedSet(new TreeSet<PropertyDescriptor>()); public static final List<PropertyDescriptor> descriptors = Collections.unmodifiableList( Arrays.asList(WEBHOOK_URL, WEBHOOK_TEXT, CHANNEL, USERNAME, ICON_URL, ICON_EMOJI)); public static final Set<Relationship> relationships = Collections.unmodifiableSet( new HashSet<>(Arrays.asList(REL_SUCCESS, REL_FAILURE))); @Override public Set<Relationship> getRelationships() { return this.relationships; } @Override public final List<PropertyDescriptor> getSupportedPropertyDescriptors() { return descriptors; } @Override protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(final String propertyDescriptorName) { return new PropertyDescriptor.Builder() .name(propertyDescriptorName) .required(false) .addValidator(StandardValidators.createAttributeExpressionLanguageValidator(AttributeExpression.ResultType.STRING, true)) .addValidator(StandardValidators.ATTRIBUTE_KEY_PROPERTY_NAME_VALIDATOR) .expressionLanguageSupported(true) .dynamic(true) .build(); } // Validate the channel (or username for a direct message) private String validateChannel(String channel) { if ((channel.startsWith("#") || channel.startsWith("@")) && channel.length() > 1) { return null; } return "Channel must begin with '#' or '@'"; } @OnScheduled public void initialize(final ProcessContext context) { attachments.clear(); for (Map.Entry<PropertyDescriptor, String> property : context.getProperties().entrySet()) { PropertyDescriptor descriptor = property.getKey(); if (descriptor.isDynamic()) { attachments.add(descriptor); } } } @Override public void onTrigger(final ProcessContext context, final ProcessSession session) { FlowFile flowFile = session.get(); if ( flowFile == null ) { return; } JsonObjectBuilder builder = Json.createObjectBuilder(); String text = context.getProperty(WEBHOOK_TEXT).evaluateAttributeExpressions(flowFile).getValue(); if (text != null && !text.isEmpty()) { builder.add("text", text); } else { // Slack requires the 'text' attribute getLogger().error("FlowFile should have non-empty " + WEBHOOK_TEXT.getName()); flowFile = session.penalize(flowFile); session.transfer(flowFile, REL_FAILURE); return; } String channel = context.getProperty(CHANNEL).evaluateAttributeExpressions(flowFile).getValue(); if (channel != null && !channel.isEmpty()) { String error = validateChannel(channel); if (error == null) { builder.add("channel", channel); } else { getLogger().error("Invalid channel '{}': {}", new Object[]{channel, error}); flowFile = session.penalize(flowFile); session.transfer(flowFile, REL_FAILURE); return; } } String username = context.getProperty(USERNAME).evaluateAttributeExpressions(flowFile).getValue(); if (username != null && !username.isEmpty()) { builder.add("username", username); } String iconUrl = context.getProperty(ICON_URL).evaluateAttributeExpressions(flowFile).getValue(); if (iconUrl != null && !iconUrl.isEmpty()) { builder.add("icon_url", iconUrl); } String iconEmoji = context.getProperty(ICON_EMOJI).evaluateAttributeExpressions(flowFile).getValue(); if (iconEmoji != null && !iconEmoji.isEmpty()) { builder.add("icon_emoji", iconEmoji); } try { // Get Attachments Array if (!attachments.isEmpty()) { JsonArrayBuilder jsonArrayBuiler = Json.createArrayBuilder(); for (PropertyDescriptor attachment : attachments) { String s = context.getProperty(attachment).evaluateAttributeExpressions(flowFile).getValue(); JsonReader reader = Json.createReader(new StringReader(s)); JsonObject attachmentJson = reader.readObject(); jsonArrayBuiler.add(attachmentJson); } builder.add("attachments", jsonArrayBuiler); } JsonObject jsonObject = builder.build(); StringWriter stringWriter = new StringWriter(); JsonWriter jsonWriter = Json.createWriter(stringWriter); jsonWriter.writeObject(jsonObject); jsonWriter.close(); URL url = new URL(context.getProperty(WEBHOOK_URL).getValue()); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setDoOutput(true); DataOutputStream outputStream = new DataOutputStream(conn.getOutputStream()); String payload = "payload=" + URLEncoder.encode(stringWriter.getBuffer().toString(), "UTF-8"); outputStream.writeBytes(payload); outputStream.close(); int responseCode = conn.getResponseCode(); if (responseCode >= 200 && responseCode < 300) { getLogger().info("Successfully posted message to Slack"); session.transfer(flowFile, REL_SUCCESS); session.getProvenanceReporter().send(flowFile, context.getProperty(WEBHOOK_URL).getValue()); } else { getLogger().error("Failed to post message to Slack with response code {}", new Object[]{responseCode}); flowFile = session.penalize(flowFile); session.transfer(flowFile, REL_FAILURE); context.yield(); } } catch (JsonParsingException e) { getLogger().error("Failed to parse JSON", e); flowFile = session.penalize(flowFile); session.transfer(flowFile, REL_FAILURE); } catch (IOException e) { getLogger().error("Failed to open connection", e); flowFile = session.penalize(flowFile); session.transfer(flowFile, REL_FAILURE); context.yield(); } } private static class EmojiValidator implements Validator { @Override public ValidationResult validate(final String subject, final String input, final ValidationContext context) { if (input.startsWith(":") && input.endsWith(":") && input.length() > 2) { return new ValidationResult.Builder().subject(subject).input(input).valid(true).build(); } return new ValidationResult.Builder().input(input).subject(subject).valid(false) .explanation("Must begin and end with a colon") .build(); } } }