├── index.js ├── test ├── field.test.js ├── confirm.test.js ├── option-group.test.js ├── button.test.js ├── select.test.js ├── attachment.test.js └── message.test.js ├── src ├── set-values.js ├── field.js ├── confirm.js ├── mixin-setters.js ├── option-group.js ├── button.js ├── message.js ├── attachment.js └── select.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── package.json └── README.md /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Message = require('./src/message') 4 | 5 | // creates a new slack message 6 | module.exports = (msg) => { 7 | var message = new Message(msg) 8 | 9 | return message 10 | } 11 | -------------------------------------------------------------------------------- /test/field.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('ava').test 4 | const Field = require('../src/field') 5 | const value = 'a value' 6 | 7 | test('Field() constructor w/ string ', t => { 8 | var cfm = Field(value) 9 | t.truthy(cfm) 10 | t.deepEqual(cfm.data.title, value) 11 | }) 12 | -------------------------------------------------------------------------------- /test/confirm.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('ava').test 4 | const Confirm = require('../src/confirm') 5 | 6 | const title = 'confirm_title' 7 | const text = 'Button Text' 8 | 9 | test('Confirm().toJSON() ', t => { 10 | var cfm = Confirm({ title, text }) 11 | t.truthy(cfm) 12 | t.deepEqual(cfm.toJSON(), cfm.json()) 13 | }) 14 | -------------------------------------------------------------------------------- /src/set-values.js: -------------------------------------------------------------------------------- 1 | const camelcase = require('camelcase') 2 | 3 | module.exports = (obj, values) => { 4 | // populate any properties passed into constructor 5 | Object.keys(values || {}).forEach(prop => { 6 | var val = values[prop] 7 | var propFn = camelcase(prop) 8 | 9 | if (typeof obj[propFn] === 'function') { 10 | obj[propFn](val) 11 | } 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4' 4 | - '6' 5 | notifications: 6 | slack: 7 | secure: KF6oKsIXnFym5rbJ8tt5Nq3flha8xaOLvu0SkFMjpC9oqRd9y8h/AM2BdUU2gVUUy28VuZeYKKL0BEA2X6dHiG9NLabFUlwefoiQLD5CjKVNIYOkG18hN98ijeCM+VwFy8Lj50xxaCpvy7y+sVcCjPuH9ZAboLFiZ7/0nx1uwGkTJDFCGDb+8NIGWsvpKlCbNBjkQblw7GdLegCCZiZuEa1H029eTQtSCKpdViCnWRFO9YKSO6gmsVWdTeRDdbWEbC6tfVBzt8s18o+38ZtdillZNMEW0+XX/hakRFTAP5A5Druhdp+Wpe1cTVPnTdFCNIKifnHFHShS07THv1e07KzYtvxAOvZmdLA4ptjBIHL17+2UYQ5vZ8tWrxNGEb7KhnmJ2Jw2o6/F35AxEt+osJ7/CaJI5jShu8k8j2NM7YyuYMIrwlo0vwK6bOQbF/V3kQue7WaP/5noBQzCCH5d9UpsoaO5CoMlnp0uvAD8qZ+hpOX/zcwATT0nll8w/oWM87AVzne86703r4vpDM6zuo2P+cUBf/XBV83sXujgPro7Gcb/wJQlWfXM2xX73iyuNXZi8rW+ODRTFAz8YSzNa4jgwz8u+5RHyGxIouFWaVnjedv1hkkigmneuERQX/8dWwHsbnvs5V/rr0o4Q+FQBn/I4SC72ae8/Ge+nz74n4A= 8 | after_script: 9 | - npm run coveralls 10 | -------------------------------------------------------------------------------- /src/field.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const mixinSetters = require('./mixin-setters') 4 | const setValues = require('./set-values') 5 | 6 | class Field { 7 | 8 | constructor (values) { 9 | if (typeof values === 'string') { 10 | values = { title: values } 11 | } 12 | this._attachment = null 13 | this.data = {} 14 | 15 | setValues(this, values) 16 | // Adds scope of `this` to each setter fn for chaining `.get()` 17 | this.addSetterScopes() 18 | } 19 | 20 | attachment (attachment) { 21 | this._attachment = attachment 22 | 23 | return this 24 | } 25 | 26 | end () { 27 | return this._attachment 28 | } 29 | 30 | json () { 31 | return Object.assign({}, this.data) 32 | } 33 | 34 | toJSON () { 35 | return this.json() 36 | } 37 | } 38 | 39 | // props for Slack API - true gets a generic setter fn 40 | const PROPS = { 41 | title: true, 42 | value: true, 43 | short: true 44 | } 45 | 46 | mixinSetters(Field.prototype, PROPS) 47 | 48 | // export a factory fn 49 | module.exports = (values) => { 50 | return new Field(values) 51 | } 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/confirm.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const mixinSetters = require('./mixin-setters') 4 | const setValues = require('./set-values') 5 | 6 | class Confirm { 7 | 8 | constructor (values) { 9 | this.data = {} 10 | 11 | setValues(this, values) 12 | // Adds scope of `this` to each setter fn for chaining `.get()` 13 | this.addSetterScopes() 14 | } 15 | 16 | button (button) { 17 | return this.action(button) 18 | } 19 | 20 | action (action) { 21 | this._action = action 22 | 23 | return this 24 | } 25 | 26 | end () { 27 | return this._action 28 | } 29 | 30 | json () { 31 | return Object.assign({}, this.data) 32 | } 33 | 34 | toJSON () { 35 | return this.json() 36 | } 37 | 38 | } 39 | 40 | // props for Slack API - true gets a generic setter fn 41 | const PROPS = { 42 | title: true, 43 | text: true, 44 | ok_text: true, 45 | // alias for ok_text 46 | ok: 'ok_text', 47 | dismiss_text: true, 48 | // alias for dismiss_text 49 | dismiss: 'dismiss' 50 | } 51 | 52 | mixinSetters(Confirm.prototype, PROPS) 53 | 54 | // export a factory fn 55 | module.exports = (values) => { 56 | return new Confirm(values) 57 | } 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slack-message-builder", 3 | "version": "1.2.1", 4 | "description": "Library for building and manipulating messages for the Slack API", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "npm run lint && npm run coverage", 11 | "lint": "standard", 12 | "unit": "ava --verbose --serial", 13 | "coverage": "nyc --check-coverage --statements 90 ava --verbose --serial", 14 | "lcov": "nyc --reporter lcov ava --serial", 15 | "coveralls": "npm run lcov && cat ./coverage/lcov.info | coveralls" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/missionsai/slack-message-builder.git" 20 | }, 21 | "keywords": [ 22 | "slack", 23 | "bot", 24 | "beepboop" 25 | ], 26 | "author": "Brad Harris", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/missionsai/slack-message-builder/issues" 30 | }, 31 | "devDependencies": { 32 | "ava": "0.25.0", 33 | "coveralls": "3.0.1", 34 | "nyc": "^8.3.0", 35 | "standard": "^8.1.0" 36 | }, 37 | "dependencies": { 38 | "camelcase": "^3.0.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/mixin-setters.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const camelcase = require('camelcase') 4 | 5 | module.exports = (receiver, props) => { 6 | // Add setter fns for each property (camelcased fn names) 7 | Object.keys(props).forEach(prop => { 8 | var setterFn = null 9 | var propVal = props[prop] 10 | var fnName = camelcase(prop) 11 | 12 | // generic setter fn 13 | if (propVal === true) { 14 | setterFn = function (val) { 15 | this.data[prop] = val 16 | 17 | return this 18 | } 19 | } 20 | 21 | // alias setter fn 22 | if (typeof propVal === 'string') { 23 | prop = propVal 24 | setterFn = function () { 25 | return this[propVal].apply(this, arguments) 26 | } 27 | } 28 | 29 | // custom setter fn 30 | if (typeof propVal === 'function') { 31 | setterFn = propVal 32 | } 33 | 34 | if (setterFn) { 35 | receiver[fnName] = setterFn 36 | 37 | // Add a .get(idx) function to setter to return value and optionally index in array 38 | setterFn.get = function (idx) { 39 | var scope = this.scope 40 | if (!scope) { 41 | return null 42 | } 43 | 44 | var val = scope.data[prop] 45 | 46 | // return indexed value 47 | if (idx !== undefined && Array.isArray(val)) { 48 | if (idx < 0) { 49 | idx = val.length + idx 50 | } 51 | return val[idx] 52 | } 53 | 54 | return val 55 | } 56 | } 57 | }) 58 | 59 | // stores a `scope` property on each setter to use for chaining fns like `.get()` 60 | receiver.addSetterScopes = function () { 61 | Object.keys(props).forEach(prop => { 62 | var fnName = camelcase(prop) 63 | 64 | if (typeof this[fnName] === 'function') { 65 | this[fnName].scope = this 66 | } 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/option-group.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('ava').test 4 | const OptionGroup = require('../src/option-group') 5 | 6 | const text = 'option_group_name' 7 | const opt1 = { text: 'text1', value: 'value1' } 8 | const opt2 = { text: 'text2', value: 'value2', description: 'description2' } 9 | const opt3 = { text: 'text3', value: 'value3', description: '' } 10 | const opt4 = { text: 'text3', value: {foo: 'bar'} } 11 | var options = [ opt1, opt2, opt3 ] 12 | 13 | test('OptionGroup()', t => { 14 | const og = OptionGroup() 15 | 16 | t.truthy(og.data) 17 | }) 18 | 19 | test('OptionGroup(text)', t => { 20 | const og = OptionGroup(text) 21 | 22 | t.is(og.data.text, text) 23 | }) 24 | 25 | test('OptionGroup() chaining options', t => { 26 | const og = OptionGroup(text) 27 | .option(opt1.text, opt1.value) 28 | .option(opt2.text, opt2.value, opt2.description) 29 | .option(opt3.text, opt3.value, opt3.description) 30 | 31 | t.is(og.data.text, text) 32 | t.deepEqual(og.data.options, options) 33 | }) 34 | 35 | test('OptionGroup() chaining with value objects', t => { 36 | const og = OptionGroup(text) 37 | .option(opt4.text, opt4.value) 38 | 39 | t.is(og.data.text, text) 40 | const expected = [opt4] 41 | expected[0].value = JSON.stringify(opt4.value) 42 | 43 | t.deepEqual(og.data.options, expected) 44 | }) 45 | 46 | test('OptionGroup({text, options})', t => { 47 | const og = OptionGroup({text, options}) 48 | 49 | t.is(og.data.text, text) 50 | t.deepEqual(og.data.options, options) 51 | }) 52 | 53 | test('OptionGroup().select()', t => { 54 | const og = OptionGroup() 55 | t.truthy(og.select()) 56 | }) 57 | 58 | test('OptionGroup().end()', t => { 59 | const select = { foo: 'bar' } 60 | const og = OptionGroup() 61 | .select(select) 62 | t.is(og.end(), select) 63 | }) 64 | 65 | test('OptionGroup().toJSON()', t => { 66 | const og = OptionGroup({text, options}) 67 | t.truthy(og) 68 | t.deepEqual(og.toJSON(), og.json()) 69 | }) 70 | -------------------------------------------------------------------------------- /src/option-group.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const mixinSetters = require('./mixin-setters') 4 | const setValues = require('./set-values') 5 | 6 | class OptionGroup { 7 | 8 | constructor (text) { 9 | let values = {} 10 | if (typeof text === 'object') { 11 | values = text 12 | } else if (typeof text === 'string') { 13 | values.text = text 14 | } 15 | 16 | this._select = null 17 | this.data = {} 18 | 19 | setValues(this, values) 20 | // Adds scope of `this` to each setter fn for chaining `.get()` 21 | this.addSetterScopes() 22 | } 23 | 24 | select (select) { 25 | this._select = select 26 | 27 | return this 28 | } 29 | 30 | end () { 31 | return this._select 32 | } 33 | 34 | json () { 35 | return Object.assign({}, this.data) 36 | } 37 | 38 | toJSON () { 39 | return this.json() 40 | } 41 | } 42 | 43 | // props for Slack API - true gets a generic setter fn 44 | const PROPS = { 45 | text: true, 46 | options: true, 47 | option: function (text, value, description) { 48 | if (value !== null && value !== undefined && typeof value === 'object') { 49 | value = JSON.stringify(value) 50 | } 51 | let option = { text, value } 52 | if (description !== undefined) option.description = description 53 | if (!Array.isArray(this.data.options)) { 54 | this.data.options = [] 55 | } 56 | this.data.options.push(option) 57 | return this 58 | } 59 | } 60 | 61 | mixinSetters(OptionGroup.prototype, PROPS) 62 | 63 | // export a factory fn 64 | module.exports = (text) => { 65 | return new OptionGroup(text) 66 | } 67 | 68 | // msg.attachment() 69 | // .callbackId('callback_id') 70 | // .text('pic some stuff') 71 | // .select() 72 | // .optionGroup() 73 | // .text('option header') 74 | // .option('Opt 1', 'option 1 value') 75 | // .option('Opt 2', 'option 2 value') 76 | // .option('Opt 3', 'option 3 value', 'option 3 description') 77 | // .option('Opt 4', 'option 4 value', '', true) 78 | // .end() 79 | // .end() 80 | -------------------------------------------------------------------------------- /src/button.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const mixinSetters = require('./mixin-setters') 4 | const setValues = require('./set-values') 5 | const Confirm = require('./confirm') 6 | 7 | class Button { 8 | 9 | constructor (name, text) { 10 | var values = {} 11 | if (typeof name === 'object') { 12 | values = name 13 | } else { 14 | if (typeof name === 'string') { 15 | values.name = name 16 | } 17 | if (typeof text === 'string') { 18 | values.text = text 19 | } 20 | } 21 | 22 | this._attachment = null 23 | this.data = { 24 | type: 'button' 25 | } 26 | 27 | setValues(this, values) 28 | // Adds scope of `this` to each setter fn for chaining `.get()` 29 | this.addSetterScopes() 30 | } 31 | 32 | attachment (attachment) { 33 | this._attachment = attachment 34 | 35 | return this 36 | } 37 | 38 | end () { 39 | return this._attachment 40 | } 41 | 42 | json () { 43 | var button = Object.assign({}, this.data) 44 | 45 | if (this.data.confirm) { 46 | button.confirm = this.data.confirm.json() 47 | } 48 | 49 | return button 50 | } 51 | 52 | toJSON () { 53 | return this.json() 54 | } 55 | } 56 | 57 | // props for Slack API - true gets a generic setter fn 58 | const PROPS = { 59 | name: true, 60 | text: true, 61 | style: true, 62 | type: true, 63 | url: true, 64 | value: function (val) { 65 | if (val !== null && val !== undefined && typeof val === 'object') { 66 | val = JSON.stringify(val) 67 | } 68 | 69 | this.data.value = val 70 | 71 | return this 72 | }, 73 | // alias for value 74 | val: 'value', 75 | confirm: function (confirm) { 76 | if (confirm === null) { 77 | this.data.confirm = null 78 | return this 79 | } 80 | 81 | this.data.confirm = Confirm(confirm).button(this) 82 | 83 | return this.data.confirm 84 | } 85 | } 86 | 87 | mixinSetters(Button.prototype, PROPS) 88 | 89 | // export a factory fn 90 | module.exports = (name, text) => { 91 | return new Button(name, text) 92 | } 93 | -------------------------------------------------------------------------------- /test/button.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('ava').test 4 | const Button = require('../src/button') 5 | 6 | const name = 'button_name' 7 | const text = 'Button Text' 8 | const value = 'button value' 9 | const style = 'primary' 10 | const url = 'http://url' 11 | 12 | test('Button()', t => { 13 | var btn = Button() 14 | 15 | t.is(btn.data.name, undefined) 16 | t.is(btn.data.text, undefined) 17 | t.is(btn.data.value, undefined) 18 | t.is(btn.data.url, undefined) 19 | t.is(btn.data.type, 'button') 20 | }) 21 | 22 | test('Button(name, text)', t => { 23 | var btn = Button(name, text) 24 | 25 | t.is(btn.data.name, name) 26 | t.is(btn.data.text, text) 27 | t.is(btn.data.value, undefined) 28 | t.is(btn.data.type, 'button') 29 | }) 30 | 31 | test('Button(name, text).val(value)', t => { 32 | var btn = Button(name, text).val(value) 33 | 34 | t.is(btn.data.name, name) 35 | t.is(btn.data.text, text) 36 | t.is(btn.data.value, value) 37 | t.is(btn.data.type, 'button') 38 | }) 39 | 40 | test('Button({name, text, value, style})', t => { 41 | var btn = Button({name, text, value, style}) 42 | 43 | t.is(btn.data.name, name) 44 | t.is(btn.data.text, text) 45 | t.is(btn.data.value, value) 46 | t.is(btn.data.style, style) 47 | t.is(btn.data.type, 'button') 48 | }) 49 | 50 | test('Button({text, url})', t => { 51 | var btn = Button({ name, text, url }) 52 | 53 | t.is(btn.data.text, text) 54 | t.is(btn.data.url, url) 55 | t.is(btn.data.type, 'button') 56 | }) 57 | 58 | test('Button() chaining settings', t => { 59 | var btn = Button() 60 | .name(name) 61 | .text(text) 62 | .style(style) 63 | .value(value) 64 | 65 | t.is(btn.data.name, name) 66 | t.is(btn.data.text, text) 67 | t.is(btn.data.value, value) 68 | t.is(btn.data.style, style) 69 | t.is(btn.data.type, 'button') 70 | }) 71 | 72 | test('Button() encoding value', t => { 73 | var value = { foo: 'bar' } 74 | var btn = Button().value(value) 75 | 76 | t.is(btn.data.value, JSON.stringify(value)) 77 | }) 78 | 79 | test('Button().confirm()', t => { 80 | var title = 'Confirm Title' 81 | var text = 'Confirm Text' 82 | 83 | var btn = Button() 84 | .confirm() 85 | .title(title) 86 | .text(text) 87 | .end() 88 | .json() 89 | 90 | t.truthy(btn) 91 | t.truthy(btn.confirm) 92 | t.is(btn.confirm.title, title) 93 | t.is(btn.confirm.text, text) 94 | }) 95 | 96 | test('Button().confirm() w/ null', t => { 97 | var btn = Button() 98 | .confirm(null) 99 | 100 | t.truthy(btn) 101 | t.is(btn.data.confirm, null) 102 | t.falsy(btn.end()) 103 | }) 104 | -------------------------------------------------------------------------------- /src/message.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const mixinSetters = require('./mixin-setters') 4 | const setValues = require('./set-values') 5 | const Attachment = require('./attachment') 6 | 7 | const Message = module.exports = class { 8 | 9 | constructor (text) { 10 | var values = {} 11 | if (typeof text === 'object') { 12 | values = text 13 | } else if (typeof text === 'string') { 14 | values.text = text 15 | } 16 | 17 | this.data = {} 18 | 19 | // populate any properties passed into constructor 20 | setValues(this, values, PROPS) 21 | // Adds scope of `this` to each setter fn for chaining `.get()` 22 | this.addSetterScopes() 23 | } 24 | 25 | // should create an Attachment object and add it to the collection 26 | attachment (values) { 27 | var attachment = Attachment(values).message(this) 28 | 29 | if (!this.data.attachments) { 30 | this.data.attachments = [] 31 | } 32 | this.data.attachments.push(attachment) 33 | 34 | return attachment 35 | } 36 | 37 | // convert Message into an object the slack api can consume 38 | json () { 39 | var message = Object.assign({}, this.data) 40 | 41 | if (this.data.attachments && this.data.attachments.length > 0) { 42 | message.attachments = this.data.attachments.map(attachment => attachment.json()) 43 | } 44 | return message 45 | } 46 | 47 | toJSON () { 48 | return this.json() 49 | } 50 | 51 | } 52 | 53 | // props for Slack API - true gets a generic setter fn 54 | const PROPS = { 55 | text: true, 56 | response_type: true, 57 | replace_original: true, 58 | delete_original: true, 59 | token: true, 60 | channel: true, 61 | user: true, 62 | parse: true, 63 | link_names: true, 64 | unfurl_links: true, 65 | unfurl_media: true, 66 | as_user: true, 67 | icon_url: true, 68 | ts: true, 69 | thread_ts: true, 70 | reply_broadcast: true, 71 | attachments: function (attachments) { 72 | if (attachments === null) { 73 | this.data.attachments = null 74 | return this 75 | } 76 | 77 | // TODO: remove reassignment here, just wipe then set 78 | this.data.attachments = (attachments || []).map(attachment => { 79 | return this.attachment(attachment) 80 | }) 81 | 82 | return this 83 | }, 84 | // bot's username for msg - as_user must be false, or ignored 85 | username: function (val) { 86 | this.data.username = val 87 | this.data.as_user = false 88 | 89 | return this 90 | }, 91 | // as_user must be false, or ignored 92 | icon_emoji: function (val) { 93 | this.data.icon_emoji = val 94 | this.data.as_user = false 95 | 96 | return this 97 | } 98 | } 99 | 100 | mixinSetters(Message.prototype, PROPS) 101 | -------------------------------------------------------------------------------- /src/attachment.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const mixinSetters = require('./mixin-setters') 4 | const setValues = require('./set-values') 5 | const Button = require('./button') 6 | const Select = require('./select') 7 | const Field = require('./field') 8 | 9 | class Attachment { 10 | 11 | constructor (values) { 12 | if (typeof values === 'string') { 13 | values = { text: values } 14 | } 15 | this._message = null 16 | this.data = { 17 | text: '' 18 | } 19 | 20 | setValues(this, values) 21 | // Adds scope of `this` to each setter fn for chaining `.get()` 22 | this.addSetterScopes() 23 | } 24 | 25 | message (message) { 26 | this._message = message 27 | 28 | return this 29 | } 30 | 31 | // convenience fn for adding a single field 32 | field (values) { 33 | var field = Field(values).attachment(this) 34 | 35 | if (!this.data.fields) { 36 | this.data.fields = [] 37 | } 38 | this.data.fields.push(field) 39 | 40 | return field 41 | } 42 | 43 | // creates Button instance, adds it to collection and returns it 44 | button () { 45 | var button = Button.apply(Button, arguments).attachment(this) 46 | 47 | if (!this.data.actions) { 48 | this.data.actions = [] 49 | } 50 | this.data.actions.push(button) 51 | 52 | return button 53 | } 54 | 55 | // creates a Menu instance, adds it to the collection of actions and returns it 56 | select () { 57 | var select = Select.apply(Select, arguments).attachment(this) 58 | 59 | if (!this.data.actions) { 60 | this.data.actions = [] 61 | } 62 | this.data.actions.push(select) 63 | 64 | return select 65 | } 66 | 67 | end () { 68 | return this._message 69 | } 70 | 71 | json () { 72 | var attachment = Object.assign({}, this.data) 73 | 74 | if (this.data.actions && this.data.actions.length > 0) { 75 | attachment.actions = this.data.actions.map(action => action.json()) 76 | } 77 | 78 | if (this.data.fields && this.data.fields.length > 0) { 79 | attachment.fields = this.data.fields.map(field => field.json()) 80 | } 81 | 82 | return attachment 83 | } 84 | 85 | toJSON () { 86 | return this.json() 87 | } 88 | 89 | } 90 | 91 | // props for Slack API - true gets a generic setter fn 92 | const PROPS = { 93 | text: true, 94 | title: true, 95 | title_link: true, 96 | fallback: true, 97 | callback_id: true, 98 | color: true, 99 | pretext: true, 100 | author_name: true, 101 | author_subname: true, 102 | author_link: true, 103 | author_icon: true, 104 | image_url: true, 105 | thumb_url: true, 106 | footer: true, 107 | footer_icon: true, 108 | mrkdwn_in: true, 109 | ts: true, // epoch time 110 | fields (fields) { 111 | if (fields === null) { 112 | this.data.fields = null 113 | return this 114 | } 115 | 116 | this.data.fields = (fields || []).map(field => this.field(field)) 117 | 118 | return this 119 | }, 120 | actions (actions) { 121 | if (actions === null) { 122 | this.data.actions = null 123 | return this 124 | } 125 | 126 | this.data.actions = (actions || []).map(action => { 127 | return action.type === 'select' ? this.select(action) : this.button(action) 128 | }) 129 | 130 | return this 131 | }, 132 | // alias for actions 133 | buttons: 'actions' 134 | } 135 | 136 | mixinSetters(Attachment.prototype, PROPS) 137 | 138 | // export a factory fn 139 | module.exports = (values) => { 140 | return new Attachment(values) 141 | } 142 | -------------------------------------------------------------------------------- /src/select.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const mixinSetters = require('./mixin-setters') 4 | const setValues = require('./set-values') 5 | const Confirm = require('./confirm') 6 | const OptionGroup = require('./option-group') 7 | 8 | class Select { 9 | 10 | constructor (name, text) { 11 | var values = {} 12 | if (typeof name === 'object') { 13 | values = name 14 | } else { 15 | if (typeof name === 'string') { 16 | values.name = name 17 | } 18 | if (typeof text === 'string') { 19 | values.text = text 20 | } 21 | } 22 | 23 | this._attachment = null 24 | this.data = { 25 | type: 'select' 26 | } 27 | 28 | setValues(this, values) 29 | // Adds scope of `this` to each setter fn for chaining `.get()` 30 | this.addSetterScopes() 31 | } 32 | 33 | attachment (attachment) { 34 | this._attachment = attachment 35 | 36 | return this 37 | } 38 | 39 | optionGroup () { 40 | const optionGroup = OptionGroup.apply(OptionGroup, arguments).select(this) 41 | 42 | if (!this.data.option_groups) { 43 | this.data.option_groups = [] 44 | } 45 | this.data.option_groups.push(optionGroup) 46 | 47 | return optionGroup 48 | } 49 | 50 | end () { 51 | return this._attachment 52 | } 53 | 54 | json () { 55 | var select = Object.assign({}, this.data) 56 | 57 | if (this.data.confirm) { 58 | select.confirm = this.data.confirm.json() 59 | } 60 | 61 | if (this.data.option_groups && this.data.option_groups.length > 0) { 62 | select.option_groups = this.data.option_groups.map(optionGroup => optionGroup.json()) 63 | } 64 | 65 | return select 66 | } 67 | 68 | toJSON () { 69 | return this.json() 70 | } 71 | } 72 | 73 | // props for Slack API - true gets a generic setter fn 74 | const PROPS = { 75 | name: true, 76 | text: true, 77 | type: true, 78 | data_source: true, 79 | min_query_length: true, 80 | options: true, 81 | selected_options: true, 82 | value: function (val) { 83 | if (val !== null && val !== undefined && typeof val === 'object') { 84 | val = JSON.stringify(val) 85 | } 86 | 87 | this.data.value = val 88 | 89 | return this 90 | }, 91 | // alias for value 92 | val: 'value', 93 | confirm: function (confirm) { 94 | if (confirm === null) { 95 | this.data.confirm = null 96 | return this 97 | } 98 | 99 | this.data.confirm = Confirm(confirm).action(this) 100 | 101 | return this.data.confirm 102 | }, 103 | selected_option: function (text, value) { 104 | if (value !== null && value !== undefined && typeof value === 'object') { 105 | value = JSON.stringify(value) 106 | } 107 | let option = { text, value } 108 | if (!Array.isArray(this.data.selected_options)) { 109 | this.data.selected_options = [] 110 | } 111 | this.data.selected_options.push(option) 112 | return this 113 | }, 114 | option: function (text, value, description) { 115 | if (value !== null && value !== undefined && typeof value === 'object') { 116 | value = JSON.stringify(value) 117 | } 118 | let option = { text, value } 119 | if (description !== undefined) option.description = description 120 | if (!Array.isArray(this.data.options)) { 121 | this.data.options = [] 122 | } 123 | this.data.options.push(option) 124 | return this 125 | } 126 | } 127 | 128 | mixinSetters(Select.prototype, PROPS) 129 | 130 | // export a factory fn 131 | module.exports = (name, text) => { 132 | return new Select(name, text) 133 | } 134 | 135 | // msg.attachment() 136 | // .callbackId('callback_id') 137 | // .text('pic some stuff') 138 | // .select('name1', 'Select a channel') 139 | // .dataSource('channels') 140 | // .end() 141 | // .select('name2', 'Select a person') 142 | // .dataSource('users') 143 | // .end() 144 | // .select('name3', 'Select from list of static options') 145 | // .option('Opt 1', 'option 1 value') 146 | // .option('Opt 2', 'option 2 value') 147 | // .option('Opt 3', 'option 3 value', 'option 3 description') 148 | // .option('Opt 4', 'option 4 value', '', true) 149 | // .end() 150 | // .select('name 3', 'Select from dynamic') 151 | // .dataSource('external') 152 | // .end() 153 | -------------------------------------------------------------------------------- /test/select.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('ava').test 4 | const Select = require('../src/select') 5 | 6 | const name = 'select_name' 7 | const text = 'Select Text' 8 | const value = 'select value' 9 | const dataSource = 'datasource' 10 | const minQueryLength = 3 11 | const opt1 = { text: 'text1', value: 'value1' } 12 | const opt2 = { text: 'text2', value: 'value2', description: 'description2' } 13 | const opt3 = { text: 'text3', value: 'value3', description: '' } 14 | var options = [ opt1, opt2, opt3 ] 15 | var selectedOptions = [ opt1 ] 16 | 17 | test('Select()', t => { 18 | var sel = Select() 19 | 20 | t.is(sel.data.name, undefined) 21 | t.is(sel.data.text, undefined) 22 | t.is(sel.data.value, undefined) 23 | t.is(sel.data.options, undefined) 24 | t.is(sel.data.type, 'select') 25 | }) 26 | 27 | test('Select(name, text)', t => { 28 | var sel = Select(name, text) 29 | 30 | t.is(sel.data.name, name) 31 | t.is(sel.data.text, text) 32 | t.is(sel.data.value, undefined) 33 | t.is(sel.data.type, 'select') 34 | }) 35 | 36 | test('Select(name, text).val(value)', t => { 37 | var sel = Select(name, text).val(value) 38 | 39 | t.is(sel.data.name, name) 40 | t.is(sel.data.text, text) 41 | t.is(sel.data.value, value) 42 | t.is(sel.data.type, 'select') 43 | }) 44 | 45 | test('Select({name, text, value, options, selectedOptions, dataSource, minQueryLength})', t => { 46 | var sel = Select({name, text, value, options, selectedOptions, dataSource, minQueryLength}) 47 | 48 | t.is(sel.data.name, name) 49 | t.is(sel.data.text, text) 50 | t.is(sel.data.value, value) 51 | t.is(sel.data.options, options) 52 | t.is(sel.data.selected_options, selectedOptions) 53 | t.is(sel.data.data_source, dataSource) 54 | t.is(sel.data.min_query_length, minQueryLength) 55 | t.is(sel.data.type, 'select') 56 | }) 57 | 58 | test('Select() chaining settings', t => { 59 | var sel = Select() 60 | .name(name) 61 | .text(text) 62 | .value(value) 63 | .options(options) 64 | .selectedOptions(selectedOptions) 65 | .dataSource(dataSource) 66 | .minQueryLength(minQueryLength) 67 | 68 | t.is(sel.data.name, name) 69 | t.is(sel.data.text, text) 70 | t.is(sel.data.value, value) 71 | t.is(sel.data.options, options) 72 | t.is(sel.data.selected_options, selectedOptions) 73 | t.is(sel.data.data_source, dataSource) 74 | t.is(sel.data.min_query_length, minQueryLength) 75 | t.is(sel.data.type, 'select') 76 | }) 77 | 78 | test('Select() chaining each option', t => { 79 | var sel = Select(name, text) 80 | .value(value) 81 | .option(opt1.text, opt1.value) 82 | .option(opt2.text, opt2.value, opt2.description) 83 | .option(opt3.text, opt3.value, opt3.description) 84 | 85 | t.is(sel.data.name, name) 86 | t.is(sel.data.text, text) 87 | t.is(sel.data.value, value) 88 | t.deepEqual(sel.data.options, options) 89 | t.is(sel.data.type, 'select') 90 | }) 91 | 92 | test('Select() chaining each selectedOption', t => { 93 | var sel = Select() 94 | selectedOptions.forEach(opt => sel.selectedOption(opt.text, opt.value)) 95 | 96 | t.deepEqual(sel.data.selected_options, selectedOptions) 97 | t.is(sel.data.type, 'select') 98 | }) 99 | 100 | test('Select() auto-stringify values that are objects', t => { 101 | var val = { some: 'object', nested: { down: 'here' } } 102 | var strVal = JSON.stringify(val) 103 | var sel = Select(name, text) 104 | .option(text, val) 105 | 106 | t.is(1, sel.data.options.length) 107 | t.is(text, sel.data.options[0].text) 108 | t.is(strVal, sel.data.options[0].value) 109 | t.is(sel.data.type, 'select') 110 | }) 111 | 112 | test('Select() auto-stringify selected_options values that are objects', t => { 113 | var val = { some: 'object', nested: { down: 'here' } } 114 | var strVal = JSON.stringify(val) 115 | var sel = Select(name, text) 116 | .selectedOption(text, val) 117 | 118 | t.is(1, sel.data.selected_options.length) 119 | t.is(text, sel.data.selected_options[0].text) 120 | t.is(strVal, sel.data.selected_options[0].value) 121 | t.is(sel.data.type, 'select') 122 | }) 123 | 124 | test('Select() encoding value', t => { 125 | var value = { foo: 'bar' } 126 | var sel = Select().value(value) 127 | 128 | t.is(sel.data.value, JSON.stringify(value)) 129 | }) 130 | 131 | test('Select().confirm()', t => { 132 | var title = 'Confirm Title' 133 | var text = 'Confirm Text' 134 | 135 | var sel = Select() 136 | .confirm() 137 | .title(title) 138 | .text(text) 139 | .end() 140 | .json() 141 | 142 | t.truthy(sel) 143 | t.truthy(sel.confirm) 144 | t.is(sel.confirm.title, title) 145 | t.is(sel.confirm.text, text) 146 | }) 147 | 148 | test('Select().confirm() w/ null', t => { 149 | var sel = Select() 150 | .confirm(null) 151 | 152 | t.truthy(sel) 153 | t.is(sel.data.confirm, null) 154 | t.falsy(sel.end()) 155 | }) 156 | 157 | test('Select().optionGrouop()', t => { 158 | const title = 'option title' 159 | 160 | const sel = Select() 161 | .optionGroup() 162 | .text(title) 163 | .option(opt1.text, opt1.value) 164 | .end() 165 | .json() 166 | 167 | t.truthy(sel) 168 | t.truthy(sel.option_groups[0]) 169 | t.is(sel.option_groups[0].text, title) 170 | t.truthy(sel.option_groups[0].options) 171 | t.is(sel.option_groups[0].options[0].text, opt1.text) 172 | t.is(sel.option_groups[0].options[0].value, opt1.value) 173 | }) 174 | 175 | test('Select().attachment()', t => { 176 | var attachment = { foo: 'bar' } 177 | var sel = Select() 178 | .attachment(attachment) 179 | 180 | t.truthy(sel) 181 | t.is(sel.end(), attachment) 182 | }) 183 | 184 | test('Select().toJSON() ', t => { 185 | var sel = Select({name, text, value, options}) 186 | t.truthy(sel) 187 | t.deepEqual(sel.toJSON(), sel.json()) 188 | }) 189 | -------------------------------------------------------------------------------- /test/attachment.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('ava').test 4 | const camelcase = require('camelcase') 5 | const Attachment = require('../src/attachment') 6 | 7 | test('Atachment()', t => { 8 | var a = Attachment().json() 9 | 10 | t.deepEqual(a, { text: '' }) 11 | }) 12 | 13 | test('Atachment() with string', t => { 14 | var text = 'test' 15 | var a = Attachment(text).json() 16 | 17 | t.deepEqual(a, { text }) 18 | }) 19 | 20 | test('Attachment() with chained props', t => { 21 | var a = Attachment() 22 | .text(attachment.text) 23 | .title(attachment.title) 24 | .titleLink(attachment.title_link) 25 | .fallback(attachment.fallback) 26 | .callbackId(attachment.callback_id) 27 | .color(attachment.color) 28 | .pretext(attachment.pretext) 29 | .authorName(attachment.author_name) 30 | .authorSubname(attachment.author_subname) 31 | .authorLink(attachment.author_link) 32 | .authorIcon(attachment.author_icon) 33 | .imageUrl(attachment.image_url) 34 | .thumbUrl(attachment.thumb_url) 35 | .footer(attachment.footer) 36 | .footerIcon(attachment.footer_icon) 37 | .ts(attachment.ts) 38 | .actions(attachment.actions) 39 | .fields(attachment.fields) 40 | .json() 41 | 42 | Object.keys(attachment).forEach(prop => { 43 | t.deepEqual(a[prop], attachment[prop]) 44 | }) 45 | }) 46 | 47 | test('Attachment() with object', t => { 48 | var a = Attachment(attachment).json() 49 | 50 | t.deepEqual(a, attachment) 51 | }) 52 | 53 | test('Attachment().button()', t => { 54 | var name = 'button name' 55 | var text = 'button text' 56 | 57 | var a = Attachment() 58 | .button(name, text).end() 59 | .json() 60 | 61 | t.is(a.actions.length, 1) 62 | t.is(a.actions[0].name, name) 63 | t.is(a.actions[0].text, text) 64 | }) 65 | 66 | test('Attachment().select()', t => { 67 | var name = 'select name' 68 | var text = 'select text' 69 | 70 | var a = Attachment() 71 | .select(name, text).end() 72 | .json() 73 | 74 | t.is(a.actions.length, 1) 75 | t.is(a.actions[0].name, name) 76 | t.is(a.actions[0].text, text) 77 | }) 78 | 79 | test('Attachment().actions()', t => { 80 | var button1 = { 81 | name: 'button1 name', 82 | text: 'button1 text', 83 | value: 'button1 value', 84 | confirm: { 85 | title: 'confirm title', 86 | text: 'confirm text', 87 | ok_text: 'confirm ok text', 88 | dismiss_text: 'confirm dismiss text' 89 | } 90 | } 91 | var button2 = { 92 | name: 'button2 name', 93 | text: 'button2 text', 94 | value: 'button2 value', 95 | style: 'primary' 96 | } 97 | 98 | var a = Attachment() 99 | .actions([button1, button2]) 100 | .json() 101 | 102 | t.is(a.actions.length, 2) 103 | t.is(a.actions[0].name, button1.name) 104 | t.is(a.actions[0].text, button1.text) 105 | t.is(a.actions[0].value, button1.value) 106 | t.is(a.actions[0].confirm.title, button1.confirm.title) 107 | t.is(a.actions[0].confirm.text, button1.confirm.text) 108 | t.is(a.actions[0].confirm.ok_text, button1.confirm.ok_text) 109 | t.is(a.actions[0].confirm.dismiss_text, button1.confirm.dismiss_text) 110 | 111 | t.is(a.actions[1].name, button2.name) 112 | t.is(a.actions[1].text, button2.text) 113 | t.is(a.actions[1].value, button2.value) 114 | t.is(a.actions[1].style, button2.style) 115 | }) 116 | 117 | test('Attachment().field()', t => { 118 | var title = 'field title' 119 | var value = 'field value' 120 | var short = true 121 | 122 | var a = Attachment() 123 | .field() 124 | .title(title) 125 | .value(value) 126 | .short(short) 127 | .end() 128 | .json() 129 | 130 | t.is(a.fields.length, 1) 131 | t.is(a.fields[0].title, title) 132 | t.is(a.fields[0].value, value) 133 | t.is(a.fields[0].short, short) 134 | }) 135 | 136 | test('Attachment() property get()', t => { 137 | var a = Attachment(attachment) 138 | 139 | Object.keys(attachment).forEach(prop => { 140 | var val = attachment[prop] 141 | var getVal = a[camelcase(prop)].get() 142 | 143 | // Flattens object out and normalizes to json structure 144 | var jsonGetVal = JSON.parse(JSON.stringify(getVal)) 145 | t.deepEqual(jsonGetVal, val, `${prop} does not match`) 146 | }) 147 | }) 148 | 149 | test('Attachment().actions.get()', t => { 150 | var a = Attachment() 151 | .actions(attachment.actions) 152 | 153 | t.deepEqual(JSON.parse(JSON.stringify(a.actions.get())), attachment.actions) 154 | }) 155 | 156 | test('Attachment().buttons.get()', t => { 157 | var a = Attachment() 158 | .buttons(attachment.actions) 159 | 160 | t.true(Array.isArray(a.buttons.get())) 161 | t.is(a.buttons.get().length, attachment.actions.length) 162 | t.deepEqual(JSON.parse(JSON.stringify(a.buttons.get())), attachment.actions) 163 | }) 164 | 165 | test('Attachment().actions.get() w/ index', t => { 166 | var a = Attachment() 167 | .actions(attachment.actions) 168 | 169 | t.deepEqual(JSON.parse(JSON.stringify(a.actions.get(0))), attachment.actions[0]) 170 | t.deepEqual(JSON.parse(JSON.stringify(a.actions.get(1))), attachment.actions[1]) 171 | t.deepEqual(JSON.parse(JSON.stringify(a.actions.get(-1))), attachment.actions[1]) 172 | t.deepEqual(JSON.parse(JSON.stringify(a.actions.get(-2))), attachment.actions[0]) 173 | }) 174 | 175 | test('Attachment().fields() w/ null', t => { 176 | var a = Attachment() 177 | .fields(null) 178 | 179 | t.truthy(a) 180 | t.is(a.data.fields, null) 181 | t.falsy(a.end()) 182 | }) 183 | 184 | test('Attachment().actions() w/ null', t => { 185 | var a = Attachment() 186 | .actions(null) 187 | 188 | t.truthy(a) 189 | t.is(a.data.actions, null) 190 | t.falsy(a.end()) 191 | }) 192 | 193 | const attachment = { 194 | text: 'attachment text', 195 | title: 'attachment title', 196 | title_link: 'https://beepboophq.com', 197 | fallback: 'attachment fallback', 198 | callback_id: 'attachment callback_id', 199 | color: '#D9488F', 200 | pretext: 'attachment pretext', 201 | author_name: 'attachment author_name', 202 | author_subname: 'attachment author_subname', 203 | author_link: 'https://beepboophq.com/author', 204 | author_icon: 'https://beepboophq.com/author_icon', 205 | image_url: 'https://beepboophq.com/image', 206 | thumb_url: 'https://beepboophq.com/thumb', 207 | footer: 'attachment footer', 208 | footer_icon: 'https://beepboophq.com/footer_icon', 209 | ts: Date.now(), 210 | actions: [ 211 | { 212 | name: 'button1 name', 213 | text: 'button1 text', 214 | value: 'button1 value', 215 | type: 'button', 216 | confirm: { 217 | title: 'confirm title', 218 | text: 'confirm text', 219 | ok_text: 'confirm ok text', 220 | dismiss_text: 'confirm dismiss text' 221 | } 222 | }, 223 | { 224 | name: 'button2 name', 225 | text: 'button2 text', 226 | type: 'button' 227 | } 228 | ], 229 | fields: [{ 230 | title: 'field title', 231 | value: 12889893, 232 | short: true 233 | }] 234 | } 235 | -------------------------------------------------------------------------------- /test/message.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('ava').test 4 | const camelcase = require('camelcase') 5 | const sm = require('../index') 6 | 7 | const text = 'this is my message' 8 | const iconEmoji = 'icon_emoji' 9 | 10 | test('slackmessage() with just text', t => { 11 | var msg = sm(text).json() 12 | 13 | t.is(msg.text, text) 14 | }) 15 | 16 | test('slackmessage() with an object w/ just text', t => { 17 | var msg = sm({ text }).json() 18 | 19 | t.is(msg.text, text) 20 | }) 21 | 22 | test('slackmessage() with a full message object', t => { 23 | var msg = sm(message).json() 24 | t.deepEqual(msg, message) 25 | }) 26 | 27 | test('slackmessage() with chained setters', t => { 28 | var msg = sm() 29 | .text(message.text) 30 | .responseType(message.response_type) 31 | .replaceOriginal(message.replace_original) 32 | .deleteOriginal(message.delete_original) 33 | .token(message.token) 34 | .channel(message.channel) 35 | .user(message.user) 36 | .parse(message.parse) 37 | .linkNames(message.link_names) 38 | .unfurlLinks(message.unfurl_links) 39 | .unfurlMedia(message.unfurl_media) 40 | .asUser(message.as_user) 41 | .iconUrl(message.icon_url) 42 | .threadTs(message.thread_ts) 43 | .ts(message.ts) 44 | .replyBroadcast(message.reply_broadcast) 45 | .attachments(message.attachments) 46 | .json() 47 | 48 | t.deepEqual(msg, message) 49 | }) 50 | 51 | test('slackmessage() with chained setters and chained attachment', t => { 52 | var msg = sm() 53 | .text(message.text) 54 | .responseType(message.response_type) 55 | .replaceOriginal(message.replace_original) 56 | .deleteOriginal(message.delete_original) 57 | .token(message.token) 58 | .channel(message.channel) 59 | .user(message.user) 60 | .parse(message.parse) 61 | .linkNames(message.link_names) 62 | .unfurlLinks(message.unfurl_links) 63 | .unfurlMedia(message.unfurl_media) 64 | .asUser(message.as_user) 65 | .iconUrl(message.icon_url) 66 | .threadTs(message.thread_ts) 67 | .ts(message.ts) 68 | .replyBroadcast(message.reply_broadcast) 69 | .attachment() 70 | .text(message.attachments[0].text) 71 | .title(message.attachments[0].title) 72 | .titleLink(message.attachments[0].title_link) 73 | .fallback(message.attachments[0].fallback) 74 | .callbackId(message.attachments[0].callback_id) 75 | .color(message.attachments[0].color) 76 | .pretext(message.attachments[0].pretext) 77 | .authorName(message.attachments[0].author_name) 78 | .authorSubname(message.attachments[0].author_subname) 79 | .authorLink(message.attachments[0].author_link) 80 | .authorIcon(message.attachments[0].author_icon) 81 | .imageUrl(message.attachments[0].image_url) 82 | .thumbUrl(message.attachments[0].thumb_url) 83 | .footer(message.attachments[0].footer) 84 | .footerIcon(message.attachments[0].footer_icon) 85 | .ts(message.attachments[0].ts) 86 | .button() 87 | .name(message.attachments[0].actions[0].name) 88 | .text(message.attachments[0].actions[0].text) 89 | .value(message.attachments[0].actions[0].value) 90 | .type(message.attachments[0].actions[0].type) 91 | .confirm() 92 | .title(message.attachments[0].actions[0].confirm.title) 93 | .text(message.attachments[0].actions[0].confirm.text) 94 | .okText(message.attachments[0].actions[0].confirm.ok_text) 95 | .dismissText(message.attachments[0].actions[0].confirm.dismiss_text) 96 | .end() 97 | .end() 98 | .button() 99 | .name(message.attachments[0].actions[1].name) 100 | .text(message.attachments[0].actions[1].text) 101 | .type(message.attachments[0].actions[1].type) 102 | .end() 103 | .field() 104 | .title(message.attachments[0].fields[0].title) 105 | .value(message.attachments[0].fields[0].value) 106 | .short(message.attachments[0].fields[0].short) 107 | .end() 108 | .select() 109 | .name(message.attachments[0].actions[2].name) 110 | .text(message.attachments[0].actions[2].text) 111 | .dataSource(message.attachments[0].actions[2].data_source) 112 | .end() 113 | .end() 114 | .attachment() 115 | .text(message.attachments[1].text) 116 | .title(message.attachments[1].title) 117 | .end() 118 | .json() 119 | 120 | t.deepEqual(msg, message) 121 | }) 122 | 123 | test('slackmessage() property get()', t => { 124 | var m = sm(message) 125 | 126 | Object.keys(message).forEach(prop => { 127 | var val = message[prop] 128 | var getVal = m[camelcase(prop)].get() 129 | 130 | // Flattens object out and normalizes to json structure 131 | var jsonGetVal = JSON.parse(JSON.stringify(getVal)) 132 | t.deepEqual(jsonGetVal, val, `${prop} does not match`) 133 | }) 134 | }) 135 | 136 | test('slackmessage().attachments.get()', t => { 137 | var m = sm() 138 | .attachments(message.attachments) 139 | 140 | t.deepEqual(JSON.parse(JSON.stringify(m.attachments.get())), message.attachments) 141 | }) 142 | 143 | test('slackmessage().attachments.get() w/ index', t => { 144 | var m = sm() 145 | .attachments(message.attachments) 146 | 147 | t.deepEqual(JSON.parse(JSON.stringify(m.attachments.get(0))), message.attachments[0]) 148 | t.deepEqual(JSON.parse(JSON.stringify(m.attachments.get(1))), message.attachments[1]) 149 | t.deepEqual(JSON.parse(JSON.stringify(m.attachments.get(-1))), message.attachments[1]) 150 | t.deepEqual(JSON.parse(JSON.stringify(m.attachments.get(-2))), message.attachments[0]) 151 | }) 152 | 153 | test('slackmessage().username()', t => { 154 | var m = sm() 155 | .username('test') 156 | .json() 157 | 158 | t.is(m.username, 'test') 159 | t.false(m.as_user) 160 | }) 161 | 162 | test('slackmessage().attachments() w/ null', t => { 163 | var m = sm() 164 | .attachments(null) 165 | 166 | t.truthy(m) 167 | t.is(m.data.attachments, null) 168 | }) 169 | 170 | test('slackmessage().iconEmoji()', t => { 171 | var m = sm().asUser(true) 172 | 173 | t.true(m.data.as_user) 174 | 175 | m = m.iconEmoji(iconEmoji) 176 | 177 | t.truthy(m) 178 | t.is(m.data.icon_emoji, iconEmoji) 179 | t.false(m.data.as_user) 180 | }) 181 | 182 | test('slackmessage().toJSON() ', t => { 183 | var m = sm(message) 184 | t.truthy(m) 185 | t.deepEqual(m.toJSON(), m.json()) 186 | }) 187 | 188 | const message = { 189 | text: 'message text', 190 | response_type: 'ephemeral', 191 | replace_original: false, 192 | delete_original: false, 193 | token: 'XIUUS9009', 194 | channel: 'C1188HKK', 195 | user: 'U12345678', 196 | parse: true, 197 | link_names: true, 198 | unfurl_links: false, 199 | unfurl_media: false, 200 | as_user: false, 201 | icon_url: 'https://beepboophq.com/icon', 202 | thread_ts: '1231231231312312', 203 | ts: '11111111.1111111', 204 | reply_broadcast: true, 205 | attachments: [ 206 | { 207 | text: 'attachment text', 208 | title: 'attachment title', 209 | title_link: 'https://beepboophq.com', 210 | fallback: 'attachment fallback', 211 | callback_id: 'attachment callback_id', 212 | color: '#D9488F', 213 | pretext: 'attachment pretext', 214 | author_name: 'attachment author_name', 215 | author_subname: 'attachment author_subname', 216 | author_link: 'https://beepboophq.com/author', 217 | author_icon: 'https://beepboophq.com/author_icon', 218 | image_url: 'https://beepboophq.com/image', 219 | thumb_url: 'https://beepboophq.com/thumb', 220 | footer: 'attachment footer', 221 | footer_icon: 'https://beepboophq.com/footer_icon', 222 | ts: Date.now(), 223 | actions: [ 224 | { 225 | name: 'button1 name', 226 | text: 'button1 text', 227 | value: 'button1 value', 228 | type: 'button', 229 | confirm: { 230 | title: 'confirm title', 231 | text: 'confirm text', 232 | ok_text: 'confirm ok text', 233 | dismiss_text: 'confirm dismiss text' 234 | } 235 | }, 236 | { 237 | name: 'button2 name', 238 | text: 'button2 text', 239 | type: 'button' 240 | }, 241 | { 242 | type: 'select', 243 | name: 'menu1 name', 244 | text: 'menu2 text', 245 | data_source: 'channels' 246 | } 247 | ], 248 | fields: [{ 249 | title: 'field title', 250 | value: 12889893, 251 | short: true 252 | }] 253 | }, 254 | { 255 | text: 'second attachment', 256 | title: 'second attachment title' 257 | } 258 | ] 259 | } 260 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Sponsored by Beep Boop](https://img.shields.io/badge/%E2%9D%A4%EF%B8%8F_sponsored_by-%E2%9C%A8_Robots%20%26%20Pencils_%E2%9C%A8-FB6CBE.svg)](https://missions.ai) 2 | [![Build Status](https://travis-ci.org/missionsai/slack-message-builder.svg)](https://travis-ci.org/MissionsAI/slack-message-builder) 3 | [![Coverage Status](https://coveralls.io/repos/github/MissionsAI/slack-message-builder/badge.svg)](https://coveralls.io/github/missionsai/slack-message-builder) 4 | 5 | # Slack Message Builder 6 | Slack Message Builder is a node.js module that builds JSON documents that can be used to post messages to slack's chat.postMessage API. Can be used where ever you need to generate Slack message JSON especially in [Slapp](https://github.com/missionsai/slapp). 7 | 8 | ## Install 9 | 10 | ``` 11 | npm install --save slack-message-builder 12 | ``` 13 | 14 | ## Usage 15 | ### Basic Formatting 16 | ```javascript 17 | const smb = require('slack-message-builder') 18 | smb().text('I am a test message http://slack.com') 19 | .attachment() 20 | .text("And here's an attachment!") 21 | .end() 22 | .json() 23 | ``` 24 | [Produces](https://api.slack.com/docs/messages/builder?msg=%7B%22text%22%3A%22I%20am%20a%20test%20message%20http%3A%2F%2Fslack.com%22%2C%22attachments%22%3A%5B%7B%22text%22%3A%22And%20here%27s%20an%20attachment!%22%7D%5D%7D) 25 | ```javascript 26 | { 27 | "text": "I am a test message http://slack.com", 28 | "attachments": [ 29 | { 30 | "text": "And here's an attachment!" 31 | } 32 | ] 33 | } 34 | ``` 35 | 36 | ### Attachments 37 | ```javascript 38 | const smb = require('slack-message-builder') 39 | smb() 40 | .attachment() 41 | .fallback("Required plain-text summary of the attachment.") 42 | .color("#36a64f") 43 | .pretext("Optional text that appears above the attachment block") 44 | .authorName("Bobby Tables") 45 | .authorLink("http://flickr.com/bobby/") 46 | .authorIcon("http://flickr.com/icons/bobby.jpg") 47 | .title("Slack API Documentation") 48 | .titleLink("https://api.slack.com/") 49 | .text("Optional text that appears within the attachment") 50 | .field() 51 | .title("Priority") 52 | .value("High") 53 | .short(false) 54 | .end() 55 | .imageUrl("http://my-website.com/path/to/image.jpg") 56 | .thumbUrl("http://example.com/path/to/thumb.png") 57 | .footer("Slack API") 58 | .footerIcon("https://platform.slack-edge.com/img/default_application_icon.png") 59 | .ts(12345678) 60 | .end() 61 | .json() 62 | ``` 63 | [Produces](https://api.slack.com/docs/messages/builder?msg=%7B%22attachments%22%3A%5B%7B%22fallback%22%3A%22Required%20plain-text%20summary%20of%20the%20attachment.%22%2C%22color%22%3A%22%2336a64f%22%2C%22pretext%22%3A%22Optional%20text%20that%20appears%20above%20the%20attachment%20block%22%2C%22author_name%22%3A%22Bobby%20Tables%22%2C%22author_link%22%3A%22http%3A%2F%2Fflickr.com%2Fbobby%2F%22%2C%22author_icon%22%3A%22http%3A%2F%2Fflickr.com%2Ficons%2Fbobby.jpg%22%2C%22title%22%3A%22Slack%20API%20Documentation%22%2C%22title_link%22%3A%22https%3A%2F%2Fapi.slack.com%2F%22%2C%22text%22%3A%22Optional%20text%20that%20appears%20within%20the%20attachment%22%2C%22fields%22%3A%5B%7B%22title%22%3A%22Priority%22%2C%22value%22%3A%22High%22%2C%22short%22%3Afalse%7D%5D%2C%22image_url%22%3A%22http%3A%2F%2Fmy-website.com%2Fpath%2Fto%2Fimage.jpg%22%2C%22thumb_url%22%3A%22http%3A%2F%2Fexample.com%2Fpath%2Fto%2Fthumb.png%22%2C%22footer%22%3A%22Slack%20API%22%2C%22footer_icon%22%3A%22https%3A%2F%2Fplatform.slack-edge.com%2Fimg%2Fdefault_application_icon.png%22%2C%22ts%22%3A123456789%7D%5D%7D) 64 | ```javascript 65 | { 66 | "attachments": [ 67 | { 68 | "fallback": "Required plain-text summary of the attachment.", 69 | "color": "#36a64f", 70 | "pretext": "Optional text that appears above the attachment block", 71 | "author_name": "Bobby Tables", 72 | "author_link": "http://flickr.com/bobby/", 73 | "author_icon": "http://flickr.com/icons/bobby.jpg", 74 | "title": "Slack API Documentation", 75 | "title_link": "https://api.slack.com/", 76 | "text": "Optional text that appears within the attachment", 77 | "fields": [ 78 | { 79 | "title": "Priority", 80 | "value": "High", 81 | "short": false 82 | } 83 | ], 84 | "image_url": "http://my-website.com/path/to/image.jpg", 85 | "thumb_url": "http://example.com/path/to/thumb.png", 86 | "footer": "Slack API", 87 | "footer_icon": "https://platform.slack-edge.com/img/default_application_icon.png", 88 | "ts": 123456789 89 | } 90 | ] 91 | } 92 | ``` 93 | 94 | ### Buttons 95 | ```javascript 96 | const smb = require('slack-message-builder') 97 | smb() 98 | .text("Would you like to play a game?") 99 | .attachment() 100 | .text("Choose a game to play") 101 | .fallback("You are unable to choose a game") 102 | .callbackId("wopr_game") 103 | .color("#3AA3E3") 104 | .button() 105 | .name("chess") 106 | .text("Chess") 107 | .type("button") 108 | .value("chess") 109 | .end() 110 | .button() 111 | .name("maze") 112 | .text("Falken's Maze") 113 | .type("button") 114 | .value("maze") 115 | .end() 116 | .button() 117 | .name("war") 118 | .text("Thermonuclear War") 119 | .style("danger") 120 | .type("button") 121 | .value("war") 122 | .confirm() 123 | .title("Are you sure?") 124 | .text("Wouldn't you prefer a good game of chess?") 125 | .okText("Yes") 126 | .dismissText("No") 127 | .end() 128 | .end() 129 | .end() 130 | .json() 131 | ``` 132 | [Produces](https://api.slack.com/docs/messages/builder?msg=%7B%22text%22%3A%22Would%20you%20like%20to%20play%20a%20game%3F%22%2C%22attachments%22%3A%5B%7B%22text%22%3A%22Choose%20a%20game%20to%20play%22%2C%22fallback%22%3A%22You%20are%20unable%20to%20choose%20a%20game%22%2C%22callback_id%22%3A%22wopr_game%22%2C%22color%22%3A%22%233AA3E3%22%2C%22attachment_type%22%3A%22default%22%2C%22actions%22%3A%5B%7B%22name%22%3A%22chess%22%2C%22text%22%3A%22Chess%22%2C%22type%22%3A%22button%22%2C%22value%22%3A%22chess%22%7D%2C%7B%22name%22%3A%22maze%22%2C%22text%22%3A%22Falken%27s%20Maze%22%2C%22type%22%3A%22button%22%2C%22value%22%3A%22maze%22%7D%2C%7B%22name%22%3A%22war%22%2C%22text%22%3A%22Thermonuclear%20War%22%2C%22style%22%3A%22danger%22%2C%22type%22%3A%22button%22%2C%22value%22%3A%22war%22%2C%22confirm%22%3A%7B%22title%22%3A%22Are%20you%20sure%3F%22%2C%22text%22%3A%22Wouldn%27t%20you%20prefer%20a%20good%20game%20of%20chess%3F%22%2C%22ok_text%22%3A%22Yes%22%2C%22dismiss_text%22%3A%22No%22%7D%7D%5D%7D%5D%7D) 133 | ```javascript 134 | { 135 | "text": "Would you like to play a game?", 136 | "attachments": [ 137 | { 138 | "text": "Choose a game to play", 139 | "fallback": "You are unable to choose a game", 140 | "callback_id": "wopr_game", 141 | "color": "#3AA3E3", 142 | "actions": [ 143 | { 144 | "name": "chess", 145 | "text": "Chess", 146 | "type": "button", 147 | "value": "chess" 148 | }, 149 | { 150 | "name": "maze", 151 | "text": "Falken's Maze", 152 | "type": "button", 153 | "value": "maze" 154 | }, 155 | { 156 | "name": "war", 157 | "text": "Thermonuclear War", 158 | "style": "danger", 159 | "type": "button", 160 | "value": "war", 161 | "confirm": { 162 | "title": "Are you sure?", 163 | "text": "Wouldn't you prefer a good game of chess?", 164 | "ok_text": "Yes", 165 | "dismiss_text": "No" 166 | } 167 | } 168 | ] 169 | } 170 | ] 171 | } 172 | ``` 173 | 174 | ### Message Menus (type=select) 175 | 176 | Message menus: 177 | 178 | ```javascript 179 | const smb = require('slack-message-builder') 180 | smb() 181 | .text("Pick a user") 182 | .attachment() 183 | .text("") 184 | .fallback("Pick a user") 185 | .callbackId("user_callback") 186 | .select() 187 | .name("pick_user") 188 | .text("Users") 189 | .dataSource("users") 190 | .end() 191 | .select() 192 | .name("pick_channel") 193 | .text("Channels") 194 | .dataSource("channels") 195 | .end() 196 | .select() 197 | .name("pick_value") 198 | .text("Static") 199 | .option("some text", "a value") 200 | .option("some more text", "moar value") 201 | .option("an object value", { foo: 'bar' }) 202 | .option("even more text", "even moar value", "a description", isSelected) // isSelected = true 203 | .end() 204 | .select() 205 | .name("pick_dynamic") 206 | .text("Choose something dynamic!") 207 | .dataSource("external") 208 | .end() 209 | .end() 210 | .json() 211 | ``` 212 | 213 | Produces: 214 | 215 | ```javascript 216 | { 217 | text: 'Pickauser', 218 | attachments: [ 219 | { 220 | text: '', 221 | fallback: 'Pickauser', 222 | callback_id: 'user_callback', 223 | actions: [ 224 | { 225 | type: 'select', 226 | name: 'pick_user', 227 | text: 'Users', 228 | data_source: 'users' 229 | }, 230 | { 231 | type: 'select', 232 | name: 'pick_channel', 233 | text: 'Channels', 234 | data_source: 'channels' 235 | }, 236 | { 237 | type: 'select', 238 | name: 'pick_value', 239 | text: 'Static', 240 | options: [ 241 | { 242 | text: 'some text', 243 | value: ' avalue' 244 | }, 245 | { 246 | text: 'some more text', 247 | value: 'moar value' 248 | }, 249 | { 250 | text: 'an object value', 251 | value: '{"foo":"bar"}' 252 | }, 253 | { 254 | text: 'even more text', 255 | value: 'even moar value', 256 | description: "a description", 257 | selected: true 258 | } 259 | ] 260 | }, 261 | { 262 | type: 'select', 263 | name: 'pick_dynamic', 264 | text: 'Choosesomethingdynamic!', 265 | data_source: 'external' 266 | } 267 | ] 268 | } 269 | ] 270 | } 271 | ``` 272 | 273 | ### Message Menus with Option Groups (type=select) 274 | ```javascript 275 | const smb = require('slack-message-builder') 276 | smb() 277 | .text('Pick a user') 278 | .attachment() 279 | .text('Pick a user') 280 | .fallback('Pick a user') 281 | .callbackId('user_callback') 282 | .select() 283 | .name('option_group') 284 | .text('Static Option Groups') 285 | .optionGroup() 286 | .text('Option header') 287 | .option('some text', 'a value') 288 | .option('some more text', 'moar value') 289 | .end() 290 | .optionGroup() 291 | .text('Second Option header') 292 | .option('some text', 'a value') 293 | .option('some more text', 'moar value') 294 | .end() 295 | .end() 296 | .end() 297 | .json() 298 | ``` 299 | 300 | Produces: 301 | 302 | ```javascript 303 | { 304 | "text": "Pick a user", 305 | "attachments": [ 306 | { 307 | "text": "Pick a user", 308 | "fallback": "Pick a user", 309 | "callback_id": "user_callback", 310 | "actions": [ 311 | { 312 | "type": "select", 313 | "name": "option_group", 314 | "text": "Static Option Groups", 315 | "option_groups": [ 316 | { 317 | "text": "Option header", 318 | "options": [ 319 | { 320 | "text": "some text", 321 | "value": "a value" 322 | }, 323 | { 324 | "text": "some more text", 325 | "value": "moar value" 326 | } 327 | ] 328 | }, 329 | { 330 | "text": "Second Option header", 331 | "options": [ 332 | { 333 | "text": "some text", 334 | "value": "a value" 335 | }, 336 | { 337 | "text": "some more text", 338 | "value": "moar value" 339 | } 340 | ] 341 | } 342 | ] 343 | } 344 | ] 345 | } 346 | ] 347 | } 348 | 349 | ``` 350 | ### Modifying Original Messages 351 | 352 | Slack message builder can also be used to modify existing messages, such as the `original_message` that comes with an interactive message action. Consider the following example that uses the [Slapp](https://github.com/missionsai/slapp) framework. 353 | 354 | ```javascript 355 | const slapp = require('slapp') 356 | 357 | slapp.action('buttonCallbackId', 'action', (msg) => { 358 | msg.respond(smb(msg.body.original_message) 359 | .attachments.get(-1) // get the last attachment 360 | .buttons(null) // remove the buttons 361 | .text(`:white_check_mark: got it`) // add some confirmation text 362 | .end() 363 | .json()) 364 | }) 365 | ``` 366 | 367 | ### Using JSON 368 | Mix and match JSON documents with slack-message-builder's functions 369 | 370 | ```javascript 371 | smb() 372 | .text("I am a test message") 373 | .attachments([{"text": "And Here's an attachment!", "color":"#3AA3E3"}]) 374 | .json() 375 | ``` 376 | 377 | ```javascript 378 | smb() 379 | .attachment() 380 | .fields([{"title": "title", "value":"value"}]) 381 | .end() 382 | .json() 383 | ``` 384 | --------------------------------------------------------------------------------