├── .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 | [![Build Status](https://travis-ci.org/uiureo/ssslack.svg?branch=master)](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 | [![Gyazo](https://i.gyazo.com/a1451fdb829fcdb8d580fee7e970e1b2.png)](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 | ssslack 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 | <% snippet.get('messages').forEach(function(message){ %> 20 |
21 | <%= message.sender %> 22 | <%= message.timestamp %> 23 | <%= message.content %> 24 |
25 | <% }) %> 26 |
27 |
28 | 29 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /cloud/views/new.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ssslack 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssslack", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "", 6 | "devDependencies": { 7 | "browserify": "^10.2.0", 8 | "standard": "^3.8.0", 9 | "watchify": "^3.2.1" 10 | }, 11 | "scripts": { 12 | "test": "standard" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/uiureo/ssslack.git" 17 | }, 18 | "author": "Kazato Sugimoto", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/uiureo/ssslack/issues" 22 | }, 23 | "homepage": "https://github.com/uiureo/ssslack", 24 | "dependencies": { 25 | "autolinker": "^0.17.1", 26 | "dompurify": "^0.6.3", 27 | "domready": "^1.0.8", 28 | "es5-shim": "^4.1.1", 29 | "html-to-vdom": "^0.5.5", 30 | "mercury": "^14.0.0", 31 | "page": "^1.6.3", 32 | "parse": "^1.4.2", 33 | "virtual-dom": "^2.0.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uiur/ssslack/8b76e9a7ce19dd5d28fae88daefc83906777d5c7/public/favicon.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ssslack 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |

ssslack

18 | new 19 | 20 | 21 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | font-family: 'Lato', sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | width: 100%; 9 | background: #f8f8f8; 10 | margin: 0; 11 | } 12 | 13 | .message { 14 | position: relative; 15 | 16 | padding-left: 48px; 17 | margin-bottom: 8px; 18 | 19 | font-size: 15px; 20 | line-height: 22px; 21 | color: #3d3c40; 22 | word-wrap: break-word; 23 | } 24 | 25 | .message-without-image { 26 | padding-left: 0; 27 | } 28 | 29 | .sender { 30 | font-weight: 900; 31 | } 32 | 33 | .content { 34 | display: block; 35 | white-space: pre-wrap; 36 | margin-top: -2px; 37 | } 38 | 39 | .timestamp { 40 | color: #babbbf; 41 | font-size: 12px; 42 | margin-left: 6px; 43 | } 44 | 45 | .message .image { 46 | position: absolute; 47 | left: 0; 48 | top: 4px; 49 | 50 | width: 36px; 51 | height: 36px; 52 | 53 | border-radius: 2px; 54 | } 55 | 56 | .snippet { 57 | margin-left: auto; 58 | margin-right: auto; 59 | margin-top: 40px; 60 | 61 | padding: 12px 18px; 62 | width: 60%; 63 | background: #fff; 64 | border: 1px solid #e1e1e1; 65 | border-radius: 6px; 66 | } 67 | 68 | .title { 69 | margin: 12px 0; 70 | font-size: 24px; 71 | color: #555; 72 | } 73 | 74 | footer { 75 | margin-top: 20px; 76 | text-align: center; 77 | font-size: 12px; 78 | } 79 | 80 | .post-form { 81 | margin-top: 40px; 82 | } 83 | 84 | .post-form-submit { 85 | float: right; 86 | } 87 | 88 | .input-title { 89 | margin-bottom: 8px; 90 | } 91 | 92 | .input-body.input-body { 93 | height: 400px; 94 | } 95 | 96 | .input-preview { 97 | padding: 6px 12px; 98 | height: 400px; 99 | border: 1px solid #ccc; 100 | overflow: scroll; 101 | background: #fff; 102 | border-radius: 4px; 103 | margin-bottom: 8px; 104 | } 105 | 106 | @media (max-width: 768px) { 107 | .snippet { 108 | width: 98%; 109 | } 110 | } 111 | --------------------------------------------------------------------------------