├── .gitignore ├── .travis.yml ├── README.md ├── client ├── app.js ├── components │ └── form.js └── route.js ├── cloud ├── app.js ├── main.js └── views │ ├── index.ejs │ └── new.ejs ├── package.json └── public ├── favicon.png ├── index.html └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### Node ### 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 30 | node_modules 31 | 32 | config 33 | 34 | public/bundle.js 35 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ssslack 2 | [](https://travis-ci.org/uiureo/ssslack) 3 | 4 | Currently in **beta**. 5 | 6 | ssslack is a service to share your slack logs. Like 'togetter for slack'. 7 | 8 | It can be useful to share ideas with people out of your team. 9 | 10 | 11 | The server is hosted on [parse.com](https://parse.com/). 12 | 13 | This project is inspired by [iiirc](https://github.com/iiirc/iiirc). 14 | 15 | ## Example 16 | https://ssslack.parseapp.com/g5BWxwAB6y 17 | 18 | ## Try 19 | Copy like below and paste here. 20 | https://ssslack.parseapp.com/new 21 | [](https://gyazo.com/a1451fdb829fcdb8d580fee7e970e1b2) 22 | 23 | 24 | There is also [chrome-extension](https://github.com/uiureo/ssslack-chrome-extension). 25 | 26 | ## License 27 | MIT 28 | -------------------------------------------------------------------------------- /client/app.js: -------------------------------------------------------------------------------- 1 | /* global Parse */ 2 | require('es5-shim') 3 | 4 | Parse.initialize('d0AdLsVEqFJsTX9XTuoz3YXluUVZ6mbRdOWM7ea6', 'ywwkjYyVSODKbZkH0G5Y4Ly7IqwWfahsWOPYfrHI') 5 | 6 | require('./route.js') 7 | -------------------------------------------------------------------------------- /client/components/form.js: -------------------------------------------------------------------------------- 1 | /* global Parse */ 2 | var hg = require('mercury') 3 | var h = require('mercury').h 4 | 5 | function Form () { 6 | return hg.state({ 7 | title: hg.value(''), 8 | body: hg.value(''), 9 | channels: { 10 | body: setBody, 11 | title: setTitle 12 | } 13 | }) 14 | } 15 | 16 | function setBody (state, data) { 17 | state.body.set(data.body) 18 | } 19 | 20 | function setTitle (state, data) { 21 | state.title.set(data.title) 22 | } 23 | 24 | function parse (str) { 25 | if (str.trim().length === 0) return [] 26 | 27 | return str.trim().split('\n\n').map(function (message) { 28 | var header = message.split('\n')[0] 29 | var match = (/^(.+)\s*\[(.+)\]\s*$/).exec(header) 30 | 31 | if (!match) return 32 | 33 | var content = message.split('\n').slice(1).join('\n') 34 | 35 | return { 36 | sender: match[1], 37 | timestamp: match[2], 38 | content: content 39 | } 40 | }).filter(function (message) { return message }) 41 | } 42 | 43 | Form.render = function (state) { 44 | var messages = parse(state.body) 45 | 46 | return h('form.post-form.container', [ 47 | h('input.form-control.input-title', { 48 | type: 'text', 49 | name: 'title', 50 | value: state.title, 51 | placeholder: 'Title (optional)', 52 | 'ev-event': hg.sendChange(state.channels.title) 53 | }), 54 | h('.row', [ 55 | h('.col-md-6', 56 | h('textarea.form-control.input-body', { 57 | name: 'body', 58 | placeholder: 'slackbot [8:00 PM]\npaste here\n\nslackbot [8:01 PM]\nlike this\n', 59 | value: state.body, 60 | 'ev-event': hg.sendValue(state.channels.body) 61 | }) 62 | ), 63 | h('.col-md-6', 64 | h('.input-preview', renderMessages(messages)) 65 | ) 66 | ]), 67 | h('input.btn.btn-primary.btn-lg.post-form-submit', { 68 | type: 'submit', 69 | value: 'Private Post', 70 | 'ev-click': function (e) { 71 | e.preventDefault() 72 | 73 | var Snippet = Parse.Object.extend('Snippet') 74 | var snippet = new Snippet() 75 | 76 | snippet.save({ 77 | title: state.title, 78 | messages: messages 79 | }, { 80 | success: function (newSnippet) { 81 | window.location.href = '/' + newSnippet.id 82 | } 83 | }) 84 | } 85 | }) 86 | ]) 87 | } 88 | 89 | function renderMessages (messages) { 90 | if (messages.length === 0) return 91 | 92 | return messages.map(function (message) { 93 | return h('.message.message-without-image', [ 94 | h('span.sender', message.sender), 95 | h('span.timestamp', message.timestamp), 96 | h('span.content', message.content) 97 | ]) 98 | }) 99 | } 100 | 101 | module.exports = Form 102 | -------------------------------------------------------------------------------- /client/route.js: -------------------------------------------------------------------------------- 1 | /* global Parse */ 2 | var domready = require('domready') 3 | var purify = require('dompurify') 4 | 5 | var h = require('virtual-dom/h') 6 | var createElement = require('virtual-dom/create-element') 7 | 8 | var hg = require('mercury') 9 | 10 | var autolinker = require('autolinker') 11 | 12 | var VNode = require('virtual-dom/vnode/vnode') 13 | var VText = require('virtual-dom/vnode/vtext') 14 | 15 | var page = require('page') 16 | 17 | var convertHTML = require('html-to-vdom')({ 18 | VNode: VNode, 19 | VText: VText 20 | }) 21 | 22 | var Form = require('./components/form.js') 23 | 24 | page('/new', function newSnippet () { 25 | hg.app(document.querySelector('#root'), Form(), Form.render) 26 | }) 27 | 28 | page('/:id', function show (ctx) { 29 | var id = ctx.params.id 30 | 31 | fetchSnippet(id, function (err, snippet) { 32 | if (err) { throw err } 33 | 34 | domready(function () { 35 | var tree = renderSnippet(snippet.attributes) 36 | var rootNode = createElement(tree) 37 | 38 | var root = document.querySelector('#root') 39 | root.innerHTML = '' 40 | root.appendChild(rootNode) 41 | }) 42 | }) 43 | }) 44 | 45 | page() 46 | 47 | function renderSnippet (snippet) { 48 | var messages = snippet.messages.map(function (message) { 49 | if (message.imageUrl) { 50 | // from chrome extension 51 | var image = h('img.image', { src: message.imageUrl }) 52 | 53 | var pureContent = purify.sanitize(message.content, { 54 | ALLOWED_TAGS: ['a'], 55 | ALLOWED_ATTR: ['href'], 56 | ALLOW_DATA_ATTR: false 57 | }) 58 | 59 | return h('.message', [image].concat([ 60 | h('.right', [ 61 | h('span.sender', message.sender), 62 | h('span.timestamp', message.timestamp), 63 | h('span.content', convertHTML('' + pureContent + '')) 64 | ]) 65 | ])) 66 | } else { 67 | return h('.message.message-without-image', [ 68 | h('span.sender', message.sender), 69 | h('span.timestamp', message.timestamp), 70 | h('span.content', convertHTML('' + autolinker.link(message.content, { stripPrefix: false }) + '')) 71 | ]) 72 | } 73 | }) 74 | 75 | var children = [] 76 | if (snippet.title) { 77 | children.push(h('h1.title', snippet.title)) 78 | } 79 | children.push(h('.messages', messages)) 80 | 81 | return h('article.snippet', children) 82 | } 83 | 84 | function fetchSnippet (id, callback) { 85 | var Snippet = Parse.Object.extend('Snippet') 86 | var query = new Parse.Query(Snippet) 87 | 88 | query.get(id, { 89 | success: function (snippet) { 90 | callback(null, snippet) 91 | }, 92 | error: function (object, error) { 93 | callback(error) 94 | } 95 | }) 96 | } 97 | -------------------------------------------------------------------------------- /cloud/app.js: -------------------------------------------------------------------------------- 1 | /* global Parse */ 2 | 3 | var express = require('express') 4 | var app = express() 5 | 6 | app.set('views', 'cloud/views') 7 | app.set('view engine', 'ejs') 8 | app.use(express.bodyParser()) 9 | 10 | app.get('/new', function (req, res) { 11 | res.render('new') 12 | }) 13 | 14 | app.get('/:id', function (req, res) { 15 | var id = req.params.id 16 | var Snippet = Parse.Object.extend('Snippet') 17 | var query = new Parse.Query(Snippet) 18 | query.get(id, { 19 | success: function (snippet) { 20 | res.render('index', { snippet: snippet }) 21 | }, 22 | error: function (object, error) { 23 | res.status(404).send('Not Found') 24 | } 25 | }) 26 | }) 27 | 28 | app.listen() 29 | -------------------------------------------------------------------------------- /cloud/main.js: -------------------------------------------------------------------------------- 1 | require('cloud/app.js') 2 | -------------------------------------------------------------------------------- /cloud/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 |