Public Slack Discourse Bot - Post to Discourse
@pd
code:
data:privatelast updated:1 year ago
today
Build integrations remarkably fast!
You're viewing a public workflow template.
Sign up to customize, add steps, modify code and more.
Join 800,000+ developers using the Pipedream platform
steps.
trigger
HTTP API
Deploy to generate unique URL
This workflow runs on Pipedream's servers and is triggered by HTTP / Webhook requests.
steps.
slack
auth
to use OAuth tokens and API keys in code via theauths object
(auths.slack)
code
Write any Node.jscodeand use anynpm package. You can alsoexport datafor use in later steps via return or this.key = 'value', pass input data to your code viaparams, and maintain state across executions with$checkpoint.
async (event, steps, auths) => {
1
2
3
4
5
6
7
8
}
9
const { WebClient } = require('@slack/web-api');
const web = new WebClient(auths.slack.oauth_access_token);

return await web.conversations.replies({
  channel: event.body.event.channel,
  ts: event.body.event.thread_ts,
})
steps.
map_user_ids_to_names
auth
to use OAuth tokens and API keys in code via theauths object
(auths.slack)
code
Write any Node.jscodeand use anynpm package. You can alsoexport datafor use in later steps via return or this.key = 'value', pass input data to your code viaparams, and maintain state across executions with$checkpoint.
async (event, steps, auths) => {
1
2
3
4
5
6
7
8
9
10
11
12
}
13
const { WebClient } = require('@slack/web-api');
const web = new WebClient(auths.slack.oauth_access_token);

const userIDs = new Set(steps.slack.$return_value.messages.map(m => m.user))

this.userMap = {}
for (const user of userIDs) {
  const userMetadata = await web.users.profile.get({ user })
  this.userMap[user] = userMetadata.profile.real_name_normalized
}
steps.
discourse_topic
auth
to use OAuth tokens and API keys in code via theauths object
code
Write any Node.jscodeand use anynpm package. You can alsoexport datafor use in later steps via return or this.key = 'value', pass input data to your code viaparams, and maintain state across executions with$checkpoint.
async (event, steps) => {
1
2
3
4
}
5
// Take whatever text the user included when calling @Discourse Bot,
// e.g. @Discourse Bot This is the topic title
this.name = steps.trigger.event.body.event.text.replace(/<@U01QX068GS0>\s*/, '')
steps.
format_discourse_posts
auth
to use OAuth tokens and API keys in code via theauths object
(auths.discourse)
code
Write any Node.jscodeand use anynpm package. You can alsoexport datafor use in later steps via return or this.key = 'value', pass input data to your code viaparams, and maintain state across executions with$checkpoint.
async (event, steps, auths) => {
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
}
81
const axios = require("axios")
const FormData = require("form-data");
const he = require("he");

this.posts = []
for (const message of steps.slack.$return_value.messages) {
  const { user, text = '', files = [] } = message

  // If the text contains the user mention of the Discourse Bot,
  // do not include it in posts
  if (text.includes('<@U01QX068GS0>')) continue

  // Strip any other mentions of the form <@user>,
  // which don't make sense for Discourse
  const strippedText = text.replace(/<@[^>]*>\s*/, '')

  // Code from Slack shows up like ```code```, and Discourse won't
  // display the text on the first and last line correctly, so we
  // have to add newlines between the backticks
  const correctlyFormattedCode = strippedText.replace(/```(.*?)```/gs, "```\n$1\n```")

  // Replace *text* with **text**
  const boldedText = correctlyFormattedCode.replace(/\*([^*]*)\*/gs, '**$1**')
  
  // Replace Slack-style links _without_ text with just the link
  const textWithNormalLinks = boldedText.replace(/<([^|>]*)>/gs, '$1')
  // Replace Slack-style links w/text with Markdown links
  const textWithMarkdownLinks = textWithNormalLinks.replace(/<([^|]*)\|([^>]*)>/gs, '[$2]($1)');

  // If files exist, download them and upload them to Discourse,
  // adding the uploaded image via Markdown
  let post = he.decode(textWithMarkdownLinks);
  for (const { url_private, name: filename } of files) {
    try {
      const file = (await axios({
        url: url_private,
        method: "GET",
        headers: {
          Authorization: `Bearer ${process.env.DISCOURSE_BOT_TOKEN}`,
        },
        responseType: 'arraybuffer',
        maxContentLength: Infinity,
        maxBodyLength: Infinity
      })).data

      const formData = new FormData()
      formData.append('files[]', file, { filename })

      const headers = {
        ...formData.getHeaders(),
        "Api-Username": "system",
        "Api-Key": `${auths.discourse.api_key}`,
        "Accept": "application/json",
      }

      const { short_url } = (await axios({
        url: `https://${auths.discourse.domain}/uploads.json`,
        method: "POST",
        headers,
        data: formData,
        params: {
          type: "composer",
          synchronous: true,
        },
        maxContentLength: Infinity,
        maxBodyLength: Infinity
      })).data

      post += `\n\n![${filename}](${short_url})`
    } catch (err) {
      console.log(`Failed to download / upload ${url_private}: ${err}`)
    }
  }

  this.posts.push({
    text: post,
    user,
  })
}
steps.
get_permalink
auth
to use OAuth tokens and API keys in code via theauths object
(auths.slack)
code
Write any Node.jscodeand use anynpm package. You can alsoexport datafor use in later steps via return or this.key = 'value', pass input data to your code viaparams, and maintain state across executions with$checkpoint.
async (event, steps, auths) => {
1
2
3
4
5
6
7
8
}
9
const { WebClient } = require('@slack/web-api');
const web = new WebClient(auths.slack.oauth_access_token);
return await web.chat.getPermalink({
  channel: event.body.event.channel,
  message_ts: event.body.event.thread_ts,
})
steps.
post_topic_to_discourse
auth
to use OAuth tokens and API keys in code via theauths object
(auths.discourse)
code
Write any Node.jscodeand use anynpm package. You can alsoexport datafor use in later steps via return or this.key = 'value', pass input data to your code viaparams, and maintain state across executions with$checkpoint.
async (event, steps, auths) => {
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
}
71
const axios = require("axios")

const instance = axios.create({
  baseURL: `https://${auths.discourse.domain}`,
  headers: {
    "Api-Username": "system",
    "Api-Key": `${auths.discourse.api_key}`,
    "Accept": "application/json",
    "Content-Type": "application/json",
  },
  validateStatus: () => true,
});

// First, create the topic with the first message in the thread
const [firstPost, ...posts] = steps.format_discourse_posts.posts

// Map Slack channel to Discourse category, defaulting to General
// if the channel mapping isn't present
const categories = {
  'C01QGRD83KQ': 5, // #discourse test channel
  'C01DFPZNKJN': 10, // Announcements
  'CMZG4EBJ9': 9, // Bugs
  'C01E5KCTR16': 11, // Dev
  'CMN2V5EAF': 7, // Feature Requests
  'CN1BUB92B': 1, // General
  'CPTJYRY5A': 5, // Help
  'CMN2WBL1H': 8, // Share your Work / Show & Tell
  'C046G5HMU75': 1, // Test channel, send to General
}

const category = categories[steps.trigger.event.body.event.channel] ?? 1

// There's no mapping of Slack users -> Discourse users, since not everyone
// has an account in each system. But we've created fake users ("User 1", "User 2", etc.)
// in Discourse, and for each topic, we map the user who posted in Slack to one of 
// those fake users. So user A's Slack posts always show up as User 1 in Disourse,
// and user B's posts always show up as User 2 in Discourse. We found that having
// every post show up tied to the "system" user didn't look great, hence this implementation.
const slackUserIDs = Object.keys(steps.map_user_ids_to_names.userMap)

const discourseAPIUsername = category === 10 ? 'dylburger' : `user-${slackUserIDs.indexOf(firstPost.user) + 1}`

this.data = (await instance.post('/posts.json', {
  title: steps.discourse_topic.name,
  category,
  tags: ['community-slack'], // to help categorize slack generated topics from Discourse ones
  raw: `**This topic was automatically generated from Slack. You can find the original thread [here](${steps.get_permalink.$return_value.permalink})**.
  
${firstPost.text}`,
}, {
  headers: {
    "Api-Username": discourseAPIUsername,
  }
})).data

// Then create the remaining posts under the same topic
this.postData = []
for (const { text, user } of posts) {
  const resp = (await instance.post('/posts.json', {
    topic_id: this.data.topic_id,
    raw: text,
  }, {
    headers: {
      "Api-Username": discourseAPIUsername,
    }
  }))
  console.log(`Post status: ${resp.status}`)
  this.postData.push(resp)
}
steps.
share_discourse_url_in_thread
auth
to use OAuth tokens and API keys in code via theauths object
(auths.slack)
params
Text

Text of the message to send. See Slack's formatting docs for more information. This field is usually required, unless you're providing only attachments instead. Provide no more than 40,000 characters or risk truncation.

Posted thread to Discourse: https://pipedream.com/community/t/{{steps.post_topic_to_discourse.data.topic_slug}}/{{steps.post_topic_to_discourse.data.topic_id}}
string ·params.text
Channel

Channel, private group, or IM channel to send message to. Can be an encoded ID, or a name. See below for more details.

{{steps.trigger.event.body.event.channel}}
string ·params.channel
Thread ts

Provide another message's ts value to make this message a reply. Avoid using a reply's ts value; use its parent instead.

{{steps.trigger.event.body.event.thread_ts}}
string ·params.thread_ts
Optional
code
Write any Node.jscodeand use anynpm package. You can alsoexport datafor use in later steps via return or this.key = 'value', pass input data to your code viaparams, and maintain state across executions with$checkpoint.
async (event, steps, params, auths) => {
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
}
32
const { WebClient } = require('@slack/web-api')

const web = new WebClient(auths.slack.oauth_access_token)
await web.chat.postMessage({
  attachments: params.attachments,
  unfurl_links: params.unfurl_links,
  text: params.text,
  unfurl_media: params.unfurl_media,
  parse: params.parse,
  as_user: params.as_user || false,
  mrkdwn: params.mrkdwn || true,
  channel: params.channel,
  username: params.username,
  blocks: params.blocks,
  icon_emoji: params.icon_emoji,
  link_names: params.link_names,
  reply_broadcast: params.reply_broadcast || false,
  thread_ts: params.thread_ts,
  icon_url: params.icon_url,
})

try {
  await web.reactions.add({
    channel: params.channel,
    timestamp: params.thread_ts,
    name: 'white_check_mark'
  })
} catch {
  $end("Already reacted")
}