diff --git a/README.md b/README.md index 8d0ff73..4dd684a 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ For more details about **Functions** take a look at [Functions on SAP Help porta |[hello-timer](./examples/hello-timer)| Basic example | A function that is triggered according to a CRON expression based schedule | |[qrcode-producer](./examples/qrcode-producer)| Basic example | A function produces the current timestamp as QR code | |[s4sdk](./examples/s4sdk)| Advanced example | A function leverages the `SAP Cloud SDK for JavaScript` to interact with the `BusinessPartner` API exposed by an `SAP S/4HANA` system | +|[slack-classify-image](./examples/slack-classify-image)| Advanced example, requires Slack integration | An image post in Slack triggers a function. The function classifies the image via `SAP Leonardo` | |[weather](./examples/weather)| Advanced example, requires `OpenWeatherMap` account | Two functions representing a simple web page that handles user input and displays the result | |[kafka-producer](./examples/kafka-producer)| Advanced example, requires Kafka broker instance | A function is triggered by a message and produces a message on a Kafka topic | diff --git a/examples/slack-classify-image/README.md b/examples/slack-classify-image/README.md new file mode 100644 index 0000000..99f1ba5 --- /dev/null +++ b/examples/slack-classify-image/README.md @@ -0,0 +1,42 @@ +# Example: Classify an Image posted in Slack with Leonardo + +A function is triggered by an image upload to a Slack channel. The function receives an event about the image upload. It then invokes the __`SAP Leonardo`__ image classification API with the image and waits for the response. The response itself contains a prediction about the possible content of the image. The function then posts the prediction to the channel. + +## Requirements + +* A Slack application, for example within a test workspace, is required. +The Slack application should define a bot user and be subscribed to the __`file_shared`__ bot event. +Bot events are used, because image classification should be limited to channels where the bot user is invited. + + +## Deployment +First, create a deployment file to provide credentials. +Run inside the project directory: +```bash +faas-sdk init-values -y values.yaml +``` +Update the generated file: +* An api key for the image classification can be obtained at [Leonardo image classification overview](https://api.sap.com/api/image_classification_api/overview). +* Also specify the Slack bot user token. + +And finally, deploy the project : +```bash + xfsrt-cli faas project deploy -y ./deploy/values.yaml -v +``` + +## Configure Slack Endpoint +Go to the Configuration UI of your Slack application, navigate to __`Event Subscriptions`__ and toggle __`Enable Events`__. Configure the HTTP trigger URL as the corresponding slack bot event request endpoint URL. +The HTTP trigger URL can be retrieved from: +``` +xfsrt-cli faas project get slack +``` +The `artifacts` array contains an object with the URL in its `name` property (and a `reference` to HTTP trigger `slack-handler`) + +## Test +Install the Slack application to your workspace. In Slack, create a channel. Invite the bot to your channel. +Click on the attachment upload button inside the channel, select an image of your choice and upload it. +As a response, you should get an `SAP Leonardo` powered probability estimation of what is contained in the image. + +## License +Copyright (c) 2020 SAP SE or an SAP affiliate company. All rights reserved. +This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the [LICENSE file](../LICENSE.txt). diff --git a/examples/slack-classify-image/data/services/leonardoapikey b/examples/slack-classify-image/data/services/leonardoapikey new file mode 100644 index 0000000..ac8522f --- /dev/null +++ b/examples/slack-classify-image/data/services/leonardoapikey @@ -0,0 +1 @@ +xxx \ No newline at end of file diff --git a/examples/slack-classify-image/data/services/slacktoken b/examples/slack-classify-image/data/services/slacktoken new file mode 100644 index 0000000..ac8522f --- /dev/null +++ b/examples/slack-classify-image/data/services/slacktoken @@ -0,0 +1 @@ +xxx \ No newline at end of file diff --git a/examples/slack-classify-image/faas.json b/examples/slack-classify-image/faas.json new file mode 100644 index 0000000..e6622e6 --- /dev/null +++ b/examples/slack-classify-image/faas.json @@ -0,0 +1,28 @@ +{ + "project": "slack", + "version": "0.0.1", + "runtime": "nodejs10", + "library": "./lib", + "secrets": { + "slack-classify-image": { + "source": "./data/services" + } + }, + "functions": { + "slack-handler": { + "module": "slack-event.js" + }, + "slack-classify": { + "module": "classify-image.js", + "secrets": [ + "slack-classify-image" + ] + } + }, + "triggers": { + "slack-handler": { + "type": "HTTP", + "function": "slack-handler" + } + } +} \ No newline at end of file diff --git a/examples/slack-classify-image/lib/classify-image.js b/examples/slack-classify-image/lib/classify-image.js new file mode 100644 index 0000000..e8646e3 --- /dev/null +++ b/examples/slack-classify-image/lib/classify-image.js @@ -0,0 +1,106 @@ +'use strict'; +/** @typedef {import("@sap/faas").Faas.Event} Faas.Event +* @typedef {import("@sap/faas").Faas.Context} Faas.Context +*/ + +const request = require('request'); +const SlackWebClient = require('@slack/client').WebClient; + +module.exports = classifyImage; + +/** + * @param {Faas.Event} event + * @param {Faas.Context} context + * @return {Promise} + */ +async function classifyImage(event, context) { + + // https://api.sap.com/api/image_classification_api/overview + const LEONARDO_API_KEY = await context.getSecretValueString('slack-classify-image', 'leonardoapikey'); + const LEONARDO_ENDPOINT = 'https://sandbox.api.sap.com/mlfs/api/v2/image/classification'; + const leonardo = { + endpoint: LEONARDO_ENDPOINT, + apiKey: LEONARDO_API_KEY, + }; + + // https://api.slack.com/slack-apps#creating_apps + const SLACK_CLIENT_TOKEN = await context.getSecretValueString('slack-classify-image', 'slacktoken'); + const slack = { + client: new SlackWebClient(SLACK_CLIENT_TOKEN), + token: SLACK_CLIENT_TOKEN, + }; + + let file, cType; + + switch (event.data.event.type) { + case 'file_shared': // https://api.slack.com/events/file_shared + return slack.client.files.info({ file: event.data.event.file_id, count: 0 }) + // check created file + .then((fileDesc) => new Promise((resolve, reject) => { + file = fileDesc.file; + request({ + url: file.url_private, + encoding: null, + headers: { 'Authorization': 'Bearer ' + slack.token } + }, (err, response, body) => { + if (err) reject(err); + cType = response.headers['content-type']; + if (!cType.startsWith('image')) reject(new Error(`file mime type ${cType}not supported`)); + + resolve(body); + }); + })) + // post final image to leonardo + .then((imgFinal) => new Promise((resolve, reject) => { + request.post({ + url: leonardo.endpoint, + headers: { + 'Accept': 'application/json', + 'APIKey': leonardo.apiKey, + }, + formData: { + files: { + value: imgFinal, + options: { + contentType: cType, + filename: file.name + } + } + }, + preambleCRLF: true, + postambleCRLF: true, + + }, (err, response, body) => { + if (err) reject(err); + if (response.statusCode === 200) { + resolve(JSON.parse(body)); + } + else { + reject(); + } + }); + })) + + // create slack message from leonardo prediction + .then((data) => { + const results = (data.predictions.length === 0) ? [] : data.predictions[0].results.reduce(function (result, current) { + result.push(`${current.label} with score ${current.score}`); + return result; + }, []); + const message = `Leonardo thinks image ${file.name} contains ${results.length ? ':\n' + results.join('\n') : ' nothing'}`; + + return slack.client.chat.postMessage({ + channel: file.channels[0], + text: message + }); + }) + + .catch((err) => { + console.log('error during image classification ' + err); + }); + default: + console.log(`unknown event "${event.data.event.type}" received`); + } + +} + diff --git a/examples/slack-classify-image/lib/slack-event.js b/examples/slack-classify-image/lib/slack-event.js new file mode 100644 index 0000000..de9a4c3 --- /dev/null +++ b/examples/slack-classify-image/lib/slack-event.js @@ -0,0 +1,22 @@ +'use strict'; +/** + * @namespace Faas + * @typedef {import("@sap/faas").Faas.Event} Faas.Event + * @typedef {import("@sap/faas").Faas.Context} Faas.Context + */ + +/** + * @param {Faas.Event} event + * @param {Faas.Context} context + * @return {Promise<*>|*} + */ +module.exports = function (event, context) { + if (event.data.challenge) { + return { challenge: event.data.challenge }; + } + context.callFunction('slack-classify', { // do not await + type: 'application/json', + data: event.data + }); +}; + diff --git a/examples/slack-classify-image/package.json b/examples/slack-classify-image/package.json new file mode 100644 index 0000000..bb42afe --- /dev/null +++ b/examples/slack-classify-image/package.json @@ -0,0 +1,19 @@ +{ + "dependencies": { + "@slack/client": "5.0.2", + "request": "2.88.2" + }, + "devDependencies": { + "@sap/faas": ">=0.7.0" + }, + "files": [ + "data", + "lib", + "faas.json", + "package.json" + ], + "scripts": { + "test": "npm run all-tests", + "all-tests": "node ./node_modules/mocha/bin/_mocha test/**/test*.js --colors -csdlJson --slow 200" + } +}