{"id":13707651,"url":"https://github.com/sol-eng/plumber-slack","last_synced_at":"2026-03-11T12:06:21.667Z","repository":{"id":101029347,"uuid":"145036901","full_name":"sol-eng/plumber-slack","owner":"sol-eng","description":"Plumber and the Slack API integration.","archived":false,"fork":false,"pushed_at":"2021-11-12T13:17:02.000Z","size":1667,"stargazers_count":17,"open_issues_count":1,"forks_count":4,"subscribers_count":6,"default_branch":"main","last_synced_at":"2024-11-13T16:41:54.537Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sol-eng.png","metadata":{"files":{"readme":"README.Rmd","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2018-08-16T20:38:31.000Z","updated_at":"2023-05-12T20:54:51.000Z","dependencies_parsed_at":null,"dependency_job_id":"39d967d5-5d65-4932-a844-83132f8613a7","html_url":"https://github.com/sol-eng/plumber-slack","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sol-eng%2Fplumber-slack","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sol-eng%2Fplumber-slack/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sol-eng%2Fplumber-slack/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sol-eng%2Fplumber-slack/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sol-eng","download_url":"https://codeload.github.com/sol-eng/plumber-slack/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252617038,"owners_count":21777118,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-02T22:01:38.662Z","updated_at":"2026-03-11T12:06:21.661Z","avatar_url":"https://github.com/sol-eng.png","language":"HTML","funding_links":[],"categories":["HTML"],"sub_categories":[],"readme":"---\ntitle: \"Plumber and the Slack API\"\noutput: github_document\n---\n\n```{r setup, include=FALSE}\nknitr::opts_chunk$set(eval = FALSE)\n\nknitr::read_chunk(\"plumber.R\")\n```\n\n*See [Slack and Plumber on R\nViews](https://rviews.rstudio.com/2018/08/30/slack-and-plumber-part-one/)\nfor a discussion of this code.*\n\nThis asset shows how [`plumber`](https://www.rplumber.io) can be used to \nbuild a [Slack slash command](https://api.slack.com/slash-commands). The API \nis built on top of a [simulated customer dataset](customer-data-sim.Rmd) that \ncontains details about customer call history. The slash command provides \naccess to customer status report as well as customer success rep reports \ndirectly from within Slack. The goal of this integration is to highlight the \nstrengths of `plumber` and how it can be used to *reliably and securely \nintegrate R with other products and services*.\n\n[![](images/slash-command-preview.png)](https://colorado.rstudio.com/rsc/slack-plumber/__swagger__/)\n\n## Using\n\nOnce you have created the slack app and the slash command as described\nin [Getting Started](#getting-started), you can access the\n[API](https://colorado.rstudio.com/rsc/slack-plumber/__swagger__/) from\nwithin the slack interface.\n\n### Commands\n\n    /cs help\n    /cs status \u003ccustomer_id\u003e\n    /cs rep \u003crep_name\u003e\n    /cs region \u003cregion_name\u003e\n\n### Examples\n\nTry the following examples using the `/cs` command with your\n[slackbot](https://get.slack.help/hc/en-us/articles/202026038-An-introduction-to-Slackbot).\n\n    /cs status 10\n\n\u003cimg src=\"images/cs-status-10.png\" width=\"300\"\u003e\n\n    /cs rep Lovey Torp MD\n\n\u003cimg src=\"images/cs-rep-torp.png\" width=\"300\"\u003e\n\n    /cs region east\n\n\u003cimg src=\"images/cs-region-east.png\" width=\"300\"\u003e\n\n### Simulated data\n\nThe API pulls data from a simulated customer dataset using the\n[`wakefield`](https://github.com/trinker/wakefield) package and the\n[`charlatan`](https://github.com/ropensci/charlatan) package. You can\nuse the following levels for `rep`, `region`, and `id`.\n\n| Rep                        | Region | ID  |\n| -------------------------- | ------ | --- |\n| Lovey Torp MD              | North  | 1   |\n| Marla Nitzsche             | South  | 2   |\n| Miss Halle Heidenreich DVM | East   | 3   |\n| Miss Jayne Sanford         | West   | 4   |\n| Ms. Arely Strosin PhD      |        | …   |\n| Sannie Schuppe DDS         |        | 100 |\n\n### Details\n\nInstead of registering a different command for each endpoint, the first\nargument provided to the slash command is the endpoint while the\nsubsequent argument(s) (if necessary) provide additional data to be\npassed to the specified endpoint. This way, a single slash command\nserves multiple endpoints without polluting the slash command namespace.\n\nTo access a customer status report, enter `/cs status \u003cid\u003e` in Slack,\nwhere `id` is a valid customer ID from the simulated data. The customer\nstatus report includes the customer name, total calls, date of birth,\nand a plot call totals for the last 20 weeks. The color of the message\nis an indication of customer health. Green indicates the customer has no\nissues while red indicates the customer has a high volume of calls,\nindicating a potential problem.\n\nHelp for all available commands can be accessed by entering `/cs help`\nor simply `/cs` into Slack.\n\n---\n\n## Getting Started\nIn order to build a Slack app, you must have a Slack account and [follow the\ndirections](https://api.slack.com) for creating a Slack app. The app will be\ntied to a specific workspace, so select a Slack workspace you anticipate\nbelonging to long term. By default, your app will only be available to this\nworkspace, although it's possible to expand access to the app later on.\n\nOnce the app has been created in Slack, create a new slash command through\nwhich the end user will interact with the app.\n\n![](images/slash-command-creation.png)\n\nSpecific details for building slash commands can be found [here](https://api.slack.com/slash-commands).\nThis will be a helpful reference through the remainder of the walk through.\n\n## Plumbing the API\nIf you haven't already, install the `plumber` package via `install.packages\n(\"plumber\")`. The [`plumber.R`](plumber.R) file uses `plumber` to define all\nof the API filters and endpoints leveraged by the Slack app. Here, we'll go\nthrough each piece of the API to describe the code and introduce helpful\nresources.\n\nIn this scenario, we're building an API that interacts with a known request.\nThat is, we must build the API so that it can properly handle the request that\ncomes from Slack. This is different from building an API that others will write\nrequests for because in this instance, we have no control over the request.\nInstead, the API must be designed to properly interact with the Slack request.\nIn order to promote this type of development, it is helpful to know how Slack\nmakes requests and what is contained in those requests. [This section](https://api.slack.com/slash-commands#app_command_handling)\nof the Slack documentation contains helpful details about the request Slack\nmakes in response to a slash command. In short, the request contains a url\nencoded data payload containing details about the slash command that was\ninvoked. An example request looks like the following:\n\n```\ntoken=gIkuvaNzQIHg97ATvDxqgjtO\n\u0026team_id=T0001\n\u0026team_domain=example\n\u0026enterprise_id=E0001\n\u0026enterprise_name=Globular%20Construct%20Inc\n\u0026channel_id=C2147483705\n\u0026channel_name=test\n\u0026user_id=U2147483697\n\u0026user_name=Steve\n\u0026command=/weather\n\u0026text=94070\n\u0026response_url=https://hooks.slack.com/commands/1234/5678\n\u0026trigger_id=13345224609.738474920.8088930838d88f008e0\n```\n\nDue to the way `plumber` handles data from incoming requests, there are two\nmethods we can use to access this data within the API. First, this data will\nbe parsed and arguments matched to the functions defined in the API. So, we\ncould write a function that takes `user_name` as an argument and the `user_name`\nvalue from the request data would be passed into the function by `plumber`\nautomatically. The other method is to access the entire data of the request\nusing `req$postBody`. With this information in mind, we are prepared to start\ncreating our API filters and endpoints.\n\n### Setup\n```{r api-setup}\n```\n\nHere we setup the environment for the API by loading the appropriate packages\nand loading the simulated data. The [`config`](https://github.com/rstudio/config) \npackage is used to store parameters that change based on the location of the\nAPI (if it's local or deployed on RStudio Connect).\n\nThere are two utility functions that are defined here. Both are used to authorize\nrequests to plot endpoints and will be described in greater detail later.\n\nWe also provide the API title and description. Now, we're ready to start defining \nthe filters and endpoints of the API.\n\n### `@filter route-endpoint`\n```{r filter-route-endpoint}\n```\n\nThis filter is responsible for parsing the `text` field of the incoming request\nand routing the request to the appropriate endpoint. Additional details provided\nin `text` are added to the request object (`req`) as `req$ARGS`. This filter\nalso routes authorized requests made to `/` to the `/help` endpoint. This way,\nsomeone in Slack can simply enter `/cs` to get help for the command.\n\n### `@filter logger`\n```{r filter-logger}\n```\n\n\nThis filter is lifted straight from the [`plumber` docs](https://www.rplumber.io/docs/routing-and-input.html#filters).\nIt simply logs information about incoming requests and is helpful when\ntroubleshooting API performance and behavior.\n\n### `@filter verify`\n```{r filter-verify}\n```\n\nThis filter is used to to confirm that incoming requests are indeed coming from\nSlack and not an unauthorized source. Details about authenticating Slack requests\ncan be found in [Slack's documentation](https://api.slack.com/docs/verifying-requests-from-slack).\nEssentially, Slack provides a signing secret that is known to us (the app\ndevelopers) and Slack. This signing secret is used in combination with request\ndetails to calculate a signature for each request. That signature is\nverified in this filter to ensure that the request came from Slack. Requests made\nto Swagger endpoints are immediately forwarded without being authorized so that\nthe Swagger UI can still be rendered.\n\n### `@post /help`\n```{r post-help}\n```\n\nThis endpoint posts a message in Slack that provides help for using this\nspecific slash command.\n\n![](images/help-screenshot.png)\n\n### `@post /status`\n```{r post-status}\n```\n\n\nThis endpoint returns a status update for the specified customer. The update\nincludes customer name, total calls, date of birth, and a plot of weekly calls\nfor the previous 20 weeks. The response is serialized as unboxed JSON so that\nit matches the [format defined by Slack](https://api.slack.com/docs/message-attachments).\nSince the `image_url` used by Slack is accessed via a simple GET request, there\nis no baked in authentication. In order to prevent sensitive data being leaked\nthrough customer plots, we use the `encrypt_string()` function defined in the \nutils section of the API setup. This function encrypts the customer id along with\nthe current datetime using the signing secret previously obtained from Slack.\nWhen this endpoint is invoked, the query string is decrypted and the datetime is \ncompared to the current datetime. If more than 5 seconds have passed, the request\nis considered to be unauthorized.\n\n### `@get /plot/history`\n```{r get-plot-history}\n```\n\nThis endpoint returns a plot of the call history for the given customer. One\nchallenge with this endpoint is that we have no control over the request that's\nmade, so it is difficult to authenticate the incoming request (ie, we can't\nsend some secret with the request and verify against it). This endpoint is used\nin the messages we return to Slack, and Slack just views this as an image URL\nto which it makes a `GET` request. In order to ensure that this works as expected,\n`#* @preempt verify` is added to the definition of this endpoint so that the\n`verify` filter doesn't apply here. As previously mentioned, this endpoint is\nsecured using `plot_auth()`, which decrypts the query string and checks the\ntimestamp to ensure that this request is made within 5 seconds of a request to\n`/status`. This effectively places an expiration date on calls to this endpoint.\nThe default expiration is 5 seconds from when the call is made to `/status`.\n\n### `@post /rep`\n```{r post-rep}\n```\n\nThis endpoint returns details about a specific rep's performance, specifically\ntotal clients and calls / client for that rep.\n\n### `@post /region`\n```{r post-region}\n```\n\nThis endpoint posts a Slack message that contains a plot of the trend for a \ngiven region. This plot is secured using the same mechanism as `/plot/history`.\n\n### `@get /plot/region/\u003cregion_name\u003e`\n```{r get-plot-region}\n```\n\nThis endpoint creates a plot for a specific region's performance. Much like the\ncustomer history plot, this endpoint uses `#* @preempt verify` so that the \n`verify` filter does not apply to this endpoint and it makes use of `plot_auth()`\nto ensure only authorized requests are responded to.\n\n## Running Locally\nInteracting with these APIs locally can be a bit of a challenge since most\nrequire data to be passed in the request body. It's also a challenge to mimic\nthe request as it is sent by Slack, especially when it comes to mimicking the\nauthentication mechanism. While traditional tools like [`curl`](https://curl.haxx.se)\ncan be used, I've found that [Postman](https://www.getpostman.com) is a powerful \nand easy to use tool for interacting with APIs. Postman can even leverage\npre-request JavaScript code to mimic the authentication mechanism employed\nby Slack. For example, I use the following JS code to mimic Slack authentication\nin local testing:\n\n```js\n// Define function for creating URI string from data object\n// Lifted from https://stackoverflow.com/questions/14525178/is-there-any-native-function-to-convert-json-to-url-parameters\nfunction urlfy(obj) {\n  return Object\n  .keys(obj)\n  .map(k =\u003e `${encodeURIComponent(k)}=${encodeURIComponent(obj[k])}`)\n  .join('\u0026');\n}\n\n// Set timestamp of request\nvar date = new Date()\nvar timestamp = date.getTime()\npm.globals.set(\"SLACK_TIMESTAMP\", timestamp);\n\n// Build rawBody using urlfy\nvar rawBody = urlfy(request.data)\nvar baseString = [\"v0\", timestamp, rawBody].join(\":\")\n// console.log(baseString)\n\n// Calculate signature\nvar signature = [\"v0=\", CryptoJS.HmacSHA256(baseString, pm.globals.get(\"SLACK_SIGNING_SECRET\"))].join('')\n//console.log(signature)\n\n// Set SLACK_SIGNATURE variable\npm.globals.set(\"SLACK_SIGNATURE\", signature)\n```\n\nThe entire Postman collection I use for interaction with the API is contained\nin the [`postman-api-collection.json`](postman-api-collection.json) file. This\ncollection can be imported into Postman and used to interact with the API either\nlocally or remotely.\n\n## Deployment\nAs mentioned, this API is deployed on RStudio Connect on the colorado demo\nserver. Deployment is done through the publish button in the RStudio IDE. A\nvanity URL was used and then passed into the Slack app settings so that Slack\nknows where to send requests.\n\n![](images/rsc-deploy-screenshot.png)\n\n![](images/slack-app-config.png)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsol-eng%2Fplumber-slack","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsol-eng%2Fplumber-slack","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsol-eng%2Fplumber-slack/lists"}