git ssb

1+

punkmonk.termux / mvd



forked from ev / mvd

Commit d2b5693b01842e3376dae3c3cf5c579e03e490c4

added initial localhost landing page

austinfrey committed on 4/9/2019, 2:09:38 AM
Parent: bea0d6f86662e5db7348deb37bef697f1226edae

Files changed

bin.jschanged
package-lock.jsonchanged
package.jsonchanged
config.jsdeleted
caps.jsadded
index.jsadded
localhost/engine.jsadded
localhost/index.jsadded
mvd/avatar.jsadded
mvd/compose.jsadded
mvd/index.jsadded
mvd/keys.jsadded
mvd/mvd-indexes.jsadded
mvd/render.jsadded
mvd/style.cssadded
mvd/style.css.jsonadded
mvd/style.jsadded
mvd/tools.jsadded
mvd/views.jsadded
ui/avatar.jsdeleted
ui/compose.jsdeleted
ui/index.jsdeleted
ui/keys.jsdeleted
ui/mvd-indexes.jsdeleted
ui/render.jsdeleted
ui/style.cssdeleted
ui/style.css.jsondeleted
ui/style.jsdeleted
ui/tools.jsdeleted
ui/views.jsdeleted
plugins/friends.mdadded
plugins/gossip.mdadded
plugins/gossip/index.jsadded
plugins/gossip/init.jsadded
plugins/gossip/schedule.jsadded
plugins/invite.jsadded
plugins/invite.mdadded
plugins/local.jsadded
plugins/logging.jsadded
plugins/master.jsadded
plugins/no-auth.jsadded
plugins/onion.jsadded
plugins/plugins.jsadded
plugins/plugins.mdadded
plugins/replicate.mdadded
plugins/replicate/index.jsadded
plugins/replicate/legacy.jsadded
plugins/unix-socket.jsadded
bin.jsView
@@ -1,5 +1,6 @@
11 var fs = require('fs')
2 +var file = require('pull-file')
23 var path = require('path')
34 var ssbKeys = require('ssb-keys')
45 var stringify = require('pull-stringify')
56 var open = require('open')
@@ -7,8 +8,9 @@
78 var nonPrivate = require('non-private-ip')
89 var muxrpcli = require('muxrpcli')
910 var {pull, values, once} = require('pull-stream')
1011 var toPull = require('stream-to-pull-stream')
12 +var through = require('pull-through')
1113 const webresolve = require('ssb-web-resolver')
1214
1315 var SEC = 1e3
1416 var MIN = 60*SEC
@@ -19,9 +21,8 @@
1921 var urlIdRegex = /^(?:\/(([%&@]|%25|%26|%40)(?:[A-Za-z0-9\/+]|%2[Ff]|%2[Bb]){43}(?:=|%3[Dd])\.(?:sha256|ed25519))(?:\.([^?]*))?|(\/.*?))(?:\?(.*))?$/
2022
2123 config.keys = ssbKeys.loadOrCreateSync(path.join(config.path, 'secret'))
2224
23-var mvdClient = fs.readFileSync(path.join('./build/index.html'))
2425 var favicon = fs.readFileSync(path.join('./public/favicon.ico'))
2526
2627 var manifestFile = path.join(config.path, 'manifest.json')
2728
@@ -31,54 +32,61 @@
3132 argv = ~i ? argv.slice(0, i) : argv
3233
3334 if (argv[0] == 'start') {
3435
35- var createSbot = require('ssb-server')
36 + var createSbot = require('./')
3637
3738 createSbot
38- .use(require('ssb-server/plugins/master'))
39 + .use(require('./plugins/master'))
40 + .use(require('./plugins/local'))
3941 .use(require('ssb-replicate'))
42 + .use(require('ssb-invite'))
4043 .use(require('ssb-friends'))
4144 .use(require('ssb-gossip'))
4245 .use(require('ssb-blobs'))
4346 .use(require('ssb-backlinks'))
4447 .use(require('ssb-query'))
4548 .use(require('ssb-links'))
4649 .use(require('ssb-ebt'))
4750 .use(require('ssb-search'))
48- .use(require('ssb-server/plugins/local'))
4951 .use(require('ssb-ws'))
5052 .use({
5153 name: 'serve',
5254 version: '1.0.0',
5355 init: function (sbot) {
54- console.log(sbot.getAddress())
5556 sbot.ws.use(function (req, res, next) {
5657 var send = config
5758
5859 delete send.keys // very important to keep this, as it removes the server keys from the config before broadcast
5960
6061 send.address = 'ws://100.115.92.2:8989~shs:VelntasZy86CuIihzSpkzPvIOYgyu3FO3NZww/UOirk='
6162
62- //sbot.invite.create({modern: true}, function (err, cb) {
63- // send.invite = cb
64- //})
65-
6663 var m = urlIdRegex.exec(req.url)
6764
65 + function onError(err) {
66 + if (err) console.error('[viewer]', err)
67 + }
68 +
6869 if(req.url == '/') {
69- console.log('/')
70- return res.end('<h1>/</h1>')
70 + var filePath = path.join(__dirname, 'localhost/build/index.html')
71 + console.log(filePath)
72 +
73 + return pull(
74 + file(filePath),
75 + through(function (data) {
76 + console.log(data.toString())
77 + this.queue(data)
78 + }),
79 + toPull(res, onError)
80 + )
7181 }
7282 if(req.url.startsWith('/web/')) {
7383 return serveWeb(req, res, m[4])
7484 }
7585 if(req.url == '/get-config') {
76- console.log('/get-config')
7786 return res.end(JSON.stringify(send))
7887 }
7988 if(req.url == '/favicon.ico') {
80- console.log('/favicon')
8189 return res.end(favicon)
8290 } else next()
8391
8492 function respond(res, status, message) {
@@ -95,23 +103,19 @@
95103 if (components[0] === 'web') components.shift()
96104 components[0] = decodeURIComponent(components[0])
97105
98106 webresolve(sbot, components, function (err, data) {
99- console.log(err)
100107 if (err) return respond(res, 404, 'ERROR: ' + err)
101108
102- function onError(err) {
103- if (err) console.error('[viewer]', err)
104- }
105109
106110 return pull(once(data), toPull(res))
107111 })
108112 }
109113 })
110114 }
111115 })
112116
113- // open('http://localhost:' + config.ws.port, {wait: false})
117 + open('http://100.115.92.2:' + config.ws.port, {wait: false})
114118
115119 // start server
116120 var server = createSbot(config)
117121
package-lock.jsonView
The diff is too large to show. Use a local git client to view these changes.
Old file size: 276075 bytes
New file size: 279318 bytes
package.jsonView
@@ -7,17 +7,19 @@
77 "start": "node bin server",
88 "decent": "node bin server --appname=decent",
99 "ssb": "node bin server --appname=ssb",
1010 "testnet": "node bin server --appname=testnet",
11- "build": "node ui/style.js && mkdir -p build && browserify ui/index.js | indexhtmlify > build/index.html"
11 + "build:mvd": "node mvd/style.js && mkdir -p build && browserify mvd/index.js | indexhtmlify > build/index.html",
12 + "build:localhost": "browserify localhost/index.js -t sheetify | indexhtmlify > localhost/build/index.html"
1213 },
1314 "devDependencies": {
1415 "browserify": "^16.2.2",
1516 "indexhtmlify": "^1.3.1"
1617 },
1718 "author": "Ev Bogue <ev@evbogue.com>",
1819 "license": "MIT",
1920 "dependencies": {
21 + "broadcast-stream": "^0.2.2",
2022 "chloride": "^2.2.14",
2123 "dataurl-": "^0.1.0",
2224 "deep-extend": "^0.6.0",
2325 "diff": "^3.5.0",
@@ -26,26 +28,33 @@
2628 "hyperfile": "^2.0.0",
2729 "hyperloadmore": "^1.1.0",
2830 "hyperscript": "^2.0.2",
2931 "hyperscroll": "^1.0.0",
32 + "inu-engine": "^1.0.0-pre.0",
3033 "multiblob-http": "^0.4.2",
3134 "muxrpcli": "^1.1.0",
35 + "nanomorph": "^5.4.0",
3236 "non-private-ip": "^1.4.3",
3337 "open": "^6.1.0",
3438 "os-homedir": "^1.0.2",
3539 "patch-package": "^6.1.0",
40 + "pull-file": "^1.1.0",
3641 "pull-more": "^1.1.0",
3742 "pull-next-query": "^1.0.0",
3843 "pull-reconnect": "0.0.3",
3944 "pull-stream": "^3.6.9",
4045 "pull-stringify": "^2.0.0",
46 + "pull-through": "^1.0.18",
4147 "rc": "^1.2.7",
48 + "secret-stack": "^6.0.3",
49 + "sheetify": "^7.3.3",
4250 "simple-mime": "^0.1.0",
4351 "split-buffer": "^1.0.0",
4452 "ssb-avatar": "^0.2.0",
4553 "ssb-backlinks": "^0.7.1",
4654 "ssb-blobs": "^1.1.5",
4755 "ssb-client": "^4.5.7",
56 + "ssb-db": "^19.1.1",
4857 "ssb-ebt": "^5.1.5",
4958 "ssb-feed": "^2.3.0",
5059 "ssb-friends": "^3.1.6",
5160 "ssb-gossip": "^1.0.6",
@@ -57,13 +66,12 @@
5766 "ssb-query": "^2.3.0",
5867 "ssb-ref": "^2.11.1",
5968 "ssb-replicate": "^1.2.3",
6069 "ssb-search": "^1.0.1",
61- "ssb-server": "^13.6.3",
62- "ssb-viewer": "^1.0.0",
6370 "ssb-web-resolver": "^1.1.2",
6471 "ssb-ws": "^6.0.0",
6572 "stack": "^0.1.0",
6673 "stream-to-pull-stream": "^1.7.3",
74 + "tachyons": "^4.11.1",
6775 "visualize-buffer": "0.0.1"
6876 }
6977 }
config.jsView
@@ -1,36 +1,0 @@
1-var http = require('http')
2-
3-module.exports = function () {
4- //var host = window.location.origin
5-
6- var host = 'http://100.115.92.2:9191'
7-
8- function getConfig () {
9- http.get(host + '/get-config', function (res) {
10- res.on('data', function (data, remote) {
11- var config = data
12- localStorage[host] = config
13- })
14- })
15- }
16-
17- if (localStorage[host]) {
18- var config = JSON.parse(localStorage[host])
19- getConfig()
20- } else {
21- getConfig()
22- setTimeout(function () {
23- location.reload()
24- }, 1000)
25- }
26-
27- config.blobsUrl = host + '/blobs/get/'
28- config.emojiUrl = host + '/img/emoji/'
29- console.log(config)
30- if (config.ws.remote)
31- config.remote = config.ws.remote
32- else
33- config.remote = config.address
34-
35- return config
36-}
caps.jsView
@@ -1,0 +1,14 @@
1 +module.exports = {
2 + shs: Buffer.from('1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=', 'base64')
3 + //this is the key for accessing the ssb protocol.
4 + //this will be updated whenever breaking changes are made.
5 + //(see secret-handshake paper for a full explaination)
6 +
7 + //there is nothing special about this value.
8 + //I generated it in the node repl with:
9 + //
10 + // > crypto.randomBytes(32).toString('base64')
11 + //
12 + //and copied it here.
13 +
14 +}
index.jsView
@@ -1,0 +1,13 @@
1 +var SecretStack = require('secret-stack')
2 +
3 +var SSB = require('ssb-db')
4 +
5 +//create a sbot with default caps. these can be overridden again when you call create.
6 +function createSsbServer () {
7 + return SecretStack({ caps: require('./caps') }).use(SSB)
8 +}
9 +module.exports = createSsbServer()
10 +
11 +//this isn't really needed anymore.
12 +module.exports.createSsbServer = createSsbServer
13 +
localhost/engine.jsView
@@ -1,0 +1,11 @@
1 +const {pull} = require('pull-stream')
2 +const h = require('hyperscript')
3 +
4 +const engine = {
5 + init: () => ({model: {}}),
6 + update: (model, action) => {},
7 + view: (model, dispatch) => h('body', h('h1', 'localhost')),
8 + run: (lastModel, effect, sources) => {}
9 +}
10 +
11 +module.exports = engine
localhost/index.jsView
@@ -1,0 +1,12 @@
1 +const start = require('inu-engine')
2 +const morph = require('nanomorph')
3 +const {pull, drain} = require('pull-stream')
4 +const css = require('sheetify')
5 +
6 +css('tachyons')
7 +
8 +const {views} = start(require('./engine'))
9 +
10 +const main = document.body
11 +
12 +pull(views(), drain(view => morph(main, view)))
mvd/avatar.jsView
@@ -1,0 +1,92 @@
1 +var pull = require('pull-stream')
2 +var query = require('./scuttlebot').query
3 +var h = require('hyperscript')
4 +var visualize = require('visualize-buffer')
5 +
6 +var avatar = require('ssb-avatar')
7 +
8 +var sbot = require('./scuttlebot')
9 +
10 +var config = require('./config')()
11 +
12 +var id = require('./keys').id
13 +
14 +var ref = require('ssb-ref')
15 +
16 +module.exports.name = function (key) {
17 +
18 + var avatarname = h('span', key.substring(0, 10))
19 + if (ref.isFeedId(key)) {
20 + avatar(sbot, id, key, function (err, data) {
21 + if (err) throw err
22 + if (data.name) {
23 + if (data.name[0] != '@') {
24 + var name = '@' + data.name
25 + } else {
26 + var name = data.name
27 + }
28 + localStorage[key + 'name'] = name
29 + avatarname.textContent = name
30 + }
31 + })
32 + }
33 + return avatarname
34 +}
35 +
36 +module.exports.image = function (key) {
37 + var img = visualize(new Buffer(key.substring(1), 'base64'), 256)
38 +
39 + if (ref.isFeedId(key)) {
40 + avatar(sbot, id, key, function (err, data) {
41 + if (err) throw err
42 + if (data.image) {
43 + localStorage[key + 'image'] = data.image
44 + img.src = config.blobsUrl + data.image
45 + }
46 + })
47 + }
48 + return img
49 +}
50 +
51 +module.exports.cachedName = function (key) {
52 + var avatarname = h('span', key.substring(0, 10))
53 +
54 + if (localStorage[key + 'name']) {
55 + avatarname.textContent = localStorage[key + 'name']
56 + } else {
57 + if (ref.isFeedId(key)) {
58 + avatar(sbot, id, key, function (err, data) {
59 + if (data.name) {
60 + if (data.name[0] != '@') {
61 + var name = '@' + data.name
62 + } else {
63 + var name = data.name
64 + }
65 + localStorage[key + 'name'] = name
66 + avatarname.textContent = name
67 + }
68 + })
69 + }
70 + }
71 +
72 + return avatarname
73 +}
74 +
75 +module.exports.cachedImage = function (key) {
76 + var img = visualize(new Buffer(key.substring(1), 'base64'), 256)
77 +
78 + if (localStorage[key + 'image']) {
79 + img.src = config.blobsUrl + localStorage[key + 'image']
80 + } else {
81 + if (ref.isFeedId(key)) {
82 + avatar(sbot, id, key, function (err, data) {
83 + if (data.image) {
84 + localStorage[key + 'image'] = data.image
85 + img.src = config.blobsUrl + data.image
86 + }
87 + })
88 + }
89 + }
90 +
91 + return img
92 +}
mvd/compose.jsView
@@ -1,0 +1,187 @@
1 +var h = require('hyperscript')
2 +var pull = require('pull-stream')
3 +var sbot = require('./scuttlebot')
4 +var human = require('human-time')
5 +var id = require('./keys').id
6 +var mentions = require('ssb-mentions')
7 +
8 +var avatar = require('./avatar')
9 +var tools = require('./tools')
10 +
11 +var mime = require('simple-mime')('application/octect-stream')
12 +var split = require('split-buffer')
13 +
14 +var route = require('./views')
15 +
16 +function file_input (onAdded) {
17 + return h('label.btn', 'Upload file',
18 + h('input', { type: 'file', hidden: true,
19 + onchange: function (ev) {
20 + var file = ev.target.files[0]
21 + if (!file) return
22 + var reader = new FileReader()
23 + reader.onload = function () {
24 + pull(
25 + pull.values(split(new Buffer(reader.result), 64*1024)),
26 + sbot.addblob(function (err, blob) {
27 + if(err) return console.error(err)
28 + onAdded({
29 + link: blob,
30 + name: file.name,
31 + size: reader.result.length || reader.result.byteLength,
32 + type: mime(file.name)
33 + })
34 + })
35 + )
36 + }
37 + reader.readAsArrayBuffer(file)
38 + }
39 + }))
40 +}
41 +
42 +module.exports = function (opts, fallback) {
43 + var files = []
44 + var filesById = {}
45 +
46 + var composer = h('div.composer')
47 + var container = h('div.container')
48 + if (opts.boostAuthor) {
49 + var boostName = avatar.cachedName(opts.boostAuthor)
50 + }
51 + if (opts.boostContent) {
52 + var textarea = h('textarea.compose')
53 + var str = opts.boostContent
54 + var lines = str.split("\n")
55 + for(var i=0; i<lines.length; i++) {
56 + lines[i] = "> " + lines[i]
57 + }
58 + var newContent = lines.join("\n")
59 + var content = 'Boosting: ' + opts.boostKey + '\n\n' + newContent + ' - [' + boostName.textContent + ']('+ opts.boostAuthor + ')'
60 + textarea.value = content
61 + }
62 +
63 + else if (opts.mentions) {
64 + var textarea = h('textarea.compose', opts.mentions)
65 + }
66 +
67 + else if (opts.type == 'wiki')
68 + var textarea = h('textarea.compose', {placeholder: opts.placeholder || 'Write a wiki (anyone can edit)'})
69 + else if (opts.type == 'post')
70 + var textarea = h('textarea.compose', {placeholder: opts.placeholder || 'Write a message (only you can edit)'})
71 + else
72 + var textarea = h('textarea.compose', {placeholder: opts.placeholder || 'Write a message (only you can edit)'}, fallback.messageText)
73 +
74 + var cancelBtn = h('button.btn', 'Cancel', {
75 + onclick: function () {
76 + var cancel
77 + console.log(opts)
78 +
79 + if (opts.type == 'edit') {
80 + cancel = document.getElementById('edit:' + opts.branch.substring(0,44))
81 + var oldMessage = h('div.message__body', tools.markdown(fallback.messageText))
82 + cancel.parentNode.replaceChild(oldMessage, cancel)
83 + oldMessage.parentNode.appendChild(fallback.buttons)
84 + } else if (opts.branch) {
85 + //cancel reply composer
86 + cancel = document.getElementById('re:' + opts.branch.substring(0,44))
87 + cancel.parentNode.removeChild(cancel)
88 + message = document.getElementById(opts.branch.substring(0,44))
89 + message.appendChild(fallback.buttons)
90 + } else {
91 + // cancel generic composer
92 + cancel = document.getElementById('composer')
93 + cancel.parentNode.removeChild(cancel)
94 + }
95 + }
96 +
97 + })
98 +
99 + var initialButtons = h('span',
100 + h('button.btn', 'Preview', {
101 + onclick: function () {
102 + if (textarea.value) {
103 + var msg = {}
104 +
105 + msg.value = {
106 + "author": id,
107 + "content": opts
108 + }
109 +
110 + msg.value.content.text = textarea.value
111 + msg.value.content.mentions = mentions(textarea.value).map(
112 + function (mention) {
113 + var file = filesById[mention.link]
114 + if (file) {
115 + if (file.type) mention.type = file.type
116 + if (file.size) mention.size = file.size
117 + }
118 + return mention
119 + }
120 + )
121 +
122 + if (opts.recps)
123 + msg.value.private = true
124 +
125 + console.log(msg)
126 + if (opts.type == 'post' || opts.type == 'wiki')
127 + var header = tools.header(msg)
128 + if (opts.type == 'update')
129 + var header = tools.timestamp(msg, {edited: true})
130 + var preview = h('div',
131 + header,
132 + h('div.message__content', tools.markdown(msg.value.content.text)),
133 + h('button.btn', 'Publish', {
134 + onclick: function () {
135 + if (msg.value.content) {
136 + sbot.publish(msg.value.content, function (err, msg) {
137 + if(err) throw err
138 + console.log('Published!', msg)
139 + if (opts.type == 'edit') {
140 + var message = document.getElementById(opts.branch.substring(0,44))
141 + fallback.messageText = msg.value.content.text
142 + var editBody = h('div.message__body',
143 + tools.timestamp(msg, {edited: true}),
144 + h('div', tools.markdown(msg.value.content.text))
145 + )
146 +
147 + message.replaceChild(editBody, message.childNodes[message.childNodes.length - 1])
148 + editBody.parentNode.appendChild(fallback.buttons)
149 + } else {
150 + if (opts.branch)
151 + cancel = document.getElementById('re:' + opts.branch.substring(0,44))
152 + else
153 + cancel = document.getElementById('composer')
154 + cancel.parentNode.removeChild(cancel)
155 + }
156 + })
157 + }
158 + }
159 + }),
160 + h('button.btn', 'Cancel', {
161 + onclick: function () {
162 + composer.replaceChild(container, composer.firstChild)
163 + container.appendChild(textarea)
164 + container.appendChild(initialButtons)
165 + }
166 + })
167 + )
168 + composer.replaceChild(preview, composer.firstChild)
169 + }
170 + }
171 + }),
172 + file_input(function (file) {
173 + files.push(file)
174 + filesById[file.link] = file
175 + var embed = file.type.indexOf('image/') === 0 ? '!' : ''
176 + textarea.value += embed + '['+file.name+']('+file.link+')'
177 + }),
178 + cancelBtn
179 + )
180 +
181 + composer.appendChild(container)
182 + container.appendChild(textarea)
183 + container.appendChild(initialButtons)
184 +
185 + return composer
186 +}
187 +
mvd/index.jsView
@@ -1,0 +1,81 @@
1 +var h = require('hyperscript')
2 +var route = require('./views')
3 +var avatar = require('./avatar')
4 +
5 +var compose = require('./compose')
6 +
7 +var id = require('./keys').id
8 +
9 +document.head.appendChild(h('style', require('./style.css.json')))
10 +
11 +var screen = h('div#screen')
12 +
13 +var search = h('input.search', {placeholder: 'Search'})
14 +
15 +var nav = h('div.navbar',
16 + h('div.internal',
17 + h('li', h('a', {href: '#' + id}, h('span.avatar--small', avatar.image(id)))),
18 + h('li', h('a', {href: '#' + id}, avatar.name(id))),
19 + h('li', h('a', 'New Post', {
20 + onclick: function () {
21 + if (document.getElementById('composer')) { return }
22 + else {
23 + var currentScreen = document.getElementById('screen')
24 + var opts = {}
25 + opts.type = 'post'
26 + var composer = h('div.content#composer', h('div.message', compose(opts)))
27 + if (currentScreen.firstChild.firstChild) {
28 + currentScreen.firstChild.insertBefore(composer, currentScreen.firstChild.firstChild)
29 + } else {
30 + currentScreen.firstChild.appendChild(composer)
31 + }
32 + }
33 + }
34 + })),
35 + h('li', h('a', 'New Wiki', {
36 + onclick: function () {
37 + if (document.getElementById('composer')) { return }
38 + else {
39 + var currentScreen = document.getElementById('screen')
40 + var opts = {}
41 + opts.type = 'wiki'
42 + var composer = h('div.content#composer', h('div.message', compose(opts)))
43 + if (currentScreen.firstChild.firstChild) {
44 + currentScreen.firstChild.insertBefore(composer, currentScreen.firstChild.firstChild)
45 + } else {
46 + currentScreen.firstChild.appendChild(composer)
47 + }
48 + }
49 + }
50 + })),
51 + h('li', h('a', {href: '#' }, 'All')),
52 + h('li', h('a', {href: '#private' }, 'Private')),
53 + h('li', h('a', {href: '#friends/' + id }, 'Friends')),
54 + h('li', h('a', {href: '#wall/' + id }, 'Wall')),
55 + h('li', h('a', {href: '#queue'}, 'Queue')),
56 + h('li', h('a', {href: '#key' }, 'Key')),
57 + h('li.right', h('a', {href: 'http://gitmx.com/#%NPNNvcnTMZUFZSWl/2Z4XX+YSdqsqOhyPacp+lgpQUw=.sha256'}, '?')),
58 + h('form.search', {
59 + onsubmit: function (e) {
60 + if (search.value[0] == '#')
61 + window.location.hash = '#' + search.value
62 + else
63 + window.location.hash = '?' + search.value
64 + e.preventDefault()
65 + }},
66 + search
67 + )
68 + )
69 +)
70 +
71 +document.body.appendChild(nav)
72 +document.body.appendChild(screen)
73 +route()
74 +
75 +window.onhashchange = function () {
76 + var oldscreen = document.getElementById('screen')
77 + var newscreen = h('div#screen')
78 + oldscreen.parentNode.replaceChild(newscreen, oldscreen)
79 + route()
80 +}
81 +
mvd/keys.jsView
@@ -1,0 +1,6 @@
1 +
2 +var config = require('./config')()
3 +var ssbKeys = require('ssb-keys')
4 +var path = require('path')
5 +
6 +module.exports = ssbKeys.loadOrCreateSync(path.join(config.caps.shs + '/secret'))
mvd/mvd-indexes.jsView
@@ -1,0 +1,20 @@
1 +var Indexes = require('flumeview-query/indexes')
2 +var pkg = require('./package.json')
3 +exports.name = 'mvd-indexes'
4 +exports.version = pkg.version
5 +exports.manifest = {}
6 +
7 +exports.init = function (sbot, config) {
8 +
9 + var view =
10 + sbot._flumeUse('query/mvd', Indexes(1, {
11 + indexes: [
12 + {key: 'chr', value: [['value', 'timestamp' ]]}
13 + ]
14 + }))
15 +
16 + var indexes = view.indexes()
17 + sbot.query.add(indexes[0])
18 +
19 + return {}
20 +}
mvd/render.jsView
@@ -1,0 +1,430 @@
1 +var h = require('hyperscript')
2 +var pull = require('pull-stream')
3 +var human = require('human-time')
4 +
5 +var sbot = require('./scuttlebot')
6 +var composer = require('./compose')
7 +var tools = require('./tools')
8 +
9 +var config = require('./config')()
10 +var id = require('./keys').id
11 +var avatar = require('./avatar')
12 +var ssbAvatar = require('ssb-avatar')
13 +
14 +var ssbKeys = require('ssb-keys')
15 +var keys = require('./keys')
16 +
17 +var diff = require('diff')
18 +
19 +function hash () {
20 + return window.location.hash.substring(1)
21 +}
22 +
23 +module.exports = function (msg) {
24 + var message = h('div.message#' + msg.key.substring(0, 44))
25 +
26 + if (!localStorage[msg.value.author])
27 + var cache = {mute: false}
28 + else
29 + var cache = JSON.parse(localStorage[msg.value.author])
30 +
31 + if (cache.mute == true) {
32 + var muted = h('span', ' muted')
33 + message.appendChild(tools.mini(msg, muted))
34 + return message
35 + }
36 +
37 + else if (msg.value.content.type == 'about') {
38 + if (msg.value.content.image) {
39 + var image = h('span.avatar--small',
40 + ' identified ',
41 + h('a', {href: '#' + msg.value.content.about}, avatar.cachedName(msg.value.content.about)),
42 + ' as ',
43 + h('img', {src: config.blobsUrl + msg.value.content.image.link})
44 + )
45 + message.appendChild(tools.mini(msg, image))
46 + }
47 + if (msg.value.content.name) {
48 + var name = h('span',
49 + ' identified ',
50 + h('a', {href: '#' + msg.value.content.about}, avatar.cachedName(msg.value.content.about)),
51 + ' as ', msg.value.content.name
52 + )
53 + message.appendChild(tools.mini(msg, name))
54 + }
55 +
56 + return message
57 + }
58 +
59 + else if (msg.value.content.type == 'label'){
60 + var content = h('span', ' labeled ', tools.messageLink(msg.value.content.link), ' as ', h('mark', h('a', {href: '#label/' + msg.value.content.label}, msg.value.content.label)))
61 + message.appendChild(tools.mini(msg, content))
62 + return message
63 + }
64 +
65 + else if (msg.value.content.type == 'queue') {
66 + if (msg.value.content.queue == true) {
67 + var content = h('span', ' added ', tools.messageLink(msg.value.content.message), ' to their ', h('a', {href: '#queue'}, 'queue'))
68 + message.appendChild(tools.mini(msg, content))
69 + }
70 + if (msg.value.content.queue == false) {
71 + var content = h('span', ' removed ', tools.messageLink(msg.value.content.message), ' from their ', h('a', {href: '#queue'}, 'queue'))
72 + message.appendChild(tools.mini(msg, content))
73 +
74 + }
75 + return message
76 + }
77 +
78 + else if (msg.value.content.type == 'edit') {
79 + message.appendChild(tools.header(msg))
80 + if (msg.value.content.text) {
81 + var current = msg.value.content.text
82 + sbot.get(msg.value.content.updated, function (err, updated) {
83 + if (updated) {
84 + // quick fix, need to decrypt messages if they're private
85 + if (updated.content.text) {
86 + fragment = document.createDocumentFragment()
87 + var previous = updated.content.text
88 + var ready = diff.diffWords(previous, current)
89 + ready.forEach(function (part) {
90 + if (part.added === true) {
91 + color = 'cyan'
92 + } else if (part.removed === true) {
93 + color = 'gray'
94 + } else {color = '#333'}
95 + var span = h('span')
96 + span.style.color = color
97 + if (part.removed === true) {
98 + span.appendChild(h('del', document.createTextNode(part.value)))
99 + } else {
100 + span.appendChild(document.createTextNode(part.value))
101 + }
102 + fragment.appendChild(span)
103 + })
104 + message.appendChild(h('code', fragment))
105 + }
106 + }
107 + })
108 + }
109 + return message
110 + }
111 +
112 + else if (msg.value.content.type == 'scat_message') {
113 + var src = hash()
114 + if (src != 'backchannel') {
115 + message.appendChild(h('button.btn.right', h('a', {href: '#backchannel'}, 'Chat')))
116 + }
117 + message.appendChild(tools.mini(msg, ' ' + msg.value.content.text))
118 + return message
119 + }
120 + else if (msg.value.content.type == 'contact') {
121 + if (msg.value.content.contact) {
122 + var contact = h('a', {href: '#' + msg.value.content.contact}, avatar.name(msg.value.content.contact))
123 + } else { var contact = h('p', 'no contact named')}
124 +
125 + if (msg.value.content.following == true) {
126 + var following = h('span', ' follows ', contact)
127 + message.appendChild(tools.mini(msg, following))
128 + }
129 + if (msg.value.content.following == false) {
130 + var unfollowing = h('span', ' unfollows ', contact)
131 + message.appendChild(tools.mini(msg, unfollowing))
132 + }
133 + if (msg.value.content.blocking == true) {
134 + var blocking = h('span', ' blocks ', contact)
135 + message.appendChild(tools.mini(msg, blocking))
136 + }
137 + if (msg.value.content.blocking == false) {
138 + var unblocking = h('span', ' unblocks ', contact)
139 + message.appendChild(tools.mini(msg, unblocking))
140 + }
141 + return message
142 +
143 + }
144 +
145 + else if (msg.value.content.type == 'git-update') {
146 +
147 + message.appendChild(tools.header(msg))
148 +
149 + var reponame = h('p', 'pushed to ', h('a', {href: '#' + msg.value.content.repo}, msg.value.content.repo))
150 +
151 + var cloneurl = h('pre', 'git clone ssb://' + msg.value.content.repo)
152 +
153 + message.appendChild(reponame)
154 +
155 +
156 + ssbAvatar(sbot, id, msg.value.content.repo, function (err, data) {
157 + if (data) {
158 + var actualname = h('p', 'pushed to ', h('a', {href: '#' + msg.value.content.repo}, '%' + data.name))
159 + reponame.parentNode.replaceChild(actualname, reponame)
160 + }
161 + })
162 +
163 + message.appendChild(cloneurl)
164 +
165 + var commits = h('ul')
166 + //if (msg.value.content.commits[0]) {
167 + if (msg.value.content.commits) {
168 + msg.value.content.commits.map(function (commit) {
169 + commits.appendChild(h('li', h('code', commit.sha1), ' - ', commit.title))
170 + })
171 +
172 + }
173 +
174 + message.appendChild(commits)
175 +
176 + return message
177 +
178 + }
179 + else if (msg.value.content.type == 'git-repo') {
180 + message.appendChild(tools.header(msg))
181 +
182 + var reponame = h('p', 'git-ssb repo ', h('a', {href: '#' + msg.key}, msg.key))
183 +
184 + message.appendChild(reponame)
185 +
186 + ssbAvatar(sbot, id, msg.key, function (err, data) {
187 + if (data)
188 + var actualname = h('p', 'git-ssb repo ', h('a', {href: '#' + msg.key}, '%' + data.name))
189 + reponame.parentNode.replaceChild(actualname, reponame)
190 + })
191 +
192 + var cloneurl = h('pre', 'git clone ssb://' + msg.key)
193 + message.appendChild(cloneurl)
194 + return message
195 + }
196 +
197 + else if (msg.value.content.type == 'wiki') {
198 + var fallback = {}
199 +
200 + var opts = {
201 + type: 'wiki',
202 + branch: msg.key
203 + }
204 +
205 + if (msg.value.content.root)
206 + opts.root = msg.value.content.root
207 + else
208 + opts.root = msg.key
209 +
210 + message.appendChild(tools.header(msg))
211 +
212 + message.appendChild(h('div.message__body', tools.markdown(msg.value.content.text)))
213 +
214 + pull(
215 + sbot.query({query: [{$filter: {value: {content: {type: 'edit', original: msg.key}}}}], limit: 100}),
216 + pull.drain(function (update) {
217 + if (update.sync) {
218 + } else {
219 + var newMessage = h('div', tools.markdown(update.value.content.text))
220 + var latest = h('div.message__body',
221 + tools.timestamp(update, {edited: true}),
222 + newMessage
223 + )
224 + message.replaceChild(latest, message.childNodes[message.childNodes.length - 2])
225 + fallback.messageText = update.value.content.text
226 + opts.updated = update.key
227 + opts.original = msg.key
228 + }
229 + })
230 + )
231 +
232 + var buttons = h('div.buttons')
233 +
234 + buttons.appendChild(h('button.btn', 'Edit wiki', {
235 + onclick: function () {
236 + opts.type = 'edit'
237 + if (!fallback.messageText)
238 + fallback.messageText = msg.value.content.text
239 +
240 + if (!opts.updated)
241 + opts.updated = msg.key
242 + opts.original = msg.key
243 +
244 + var r = message.childNodes.length - 1
245 + fallback.buttons = message.childNodes[r]
246 + message.removeChild(message.childNodes[r])
247 + var compose = h('div#edit:' + msg.key.substring(0, 44), composer(opts, fallback))
248 + message.replaceChild(compose, message.lastElementChild)
249 + }
250 + }))
251 +
252 + buttons.appendChild(tools.star(msg))
253 + message.appendChild(buttons)
254 + return message
255 +
256 + } else if (msg.value.content.type == 'post') {
257 + var opts = {
258 + type: 'post',
259 + branch: msg.key
260 + }
261 + var fallback = {}
262 +
263 +
264 + if (msg.value.content.root)
265 + opts.root = msg.value.content.root
266 + else
267 + opts.root = msg.key
268 +
269 + message.appendChild(tools.header(msg))
270 +
271 + if (msg.value.content.root)
272 + message.appendChild(h('span', 're: ', tools.messageLink(msg.value.content.root)))
273 +
274 + message.appendChild(h('div.message__body', tools.markdown(msg.value.content.text)))
275 +
276 + pull(
277 + sbot.query({query: [{$filter: {value: {content: {type: 'edit', original: msg.key}}}}], limit: 100}),
278 + pull.drain(function (update) {
279 + if (update.sync) {
280 + } else {
281 + var newMessage = h('div', tools.markdown(update.value.content.text))
282 + var latest = h('div.message__body',
283 + tools.timestamp(update, {edited: true}),
284 + newMessage
285 + )
286 + message.replaceChild(latest, message.childNodes[message.childNodes.length - 2])
287 + fallback.messageText = update.value.content.text
288 + opts.updated = update.key
289 + opts.original = msg.key
290 + }
291 + })
292 + )
293 +
294 + pull(
295 + sbot.query({query: [{$filter: {value: { content: {type: 'label', link: msg.key}}}}], limit: 100, live: true}),
296 + pull.drain(function (labels){
297 + console.log(labels)
298 + if (labels.value){
299 + message.appendChild(h('span', ' ', h('mark', h('a', {href: '#label/' + labels.value.content.label}, labels.value.content.label))))
300 +
301 + }
302 + })
303 + )
304 +
305 + var name = avatar.name(msg.value.author)
306 +
307 + var buttons = h('div.buttons')
308 +
309 + buttons.appendChild(h('button.btn', 'Reply', {
310 + onclick: function () {
311 + opts.type = 'post'
312 + opts.mentions = '[' + name.textContent + '](' + msg.value.author + ')'
313 + if (msg.value.content.recps) {
314 + opts.recps = msg.value.content.recps
315 + }
316 + var r = message.childNodes.length - 1
317 + delete opts.updated
318 + delete opts.original
319 + delete fallback.messageText
320 + fallback.buttons = message.childNodes[r]
321 + var compose = h('div.message#re:' + msg.key.substring(0, 44), composer(opts, fallback))
322 + message.removeChild(message.childNodes[r])
323 + message.parentNode.insertBefore(compose, message.nextSibling)
324 + }
325 + }))
326 +
327 + buttons.appendChild(h('button.btn', 'Boost', {
328 + onclick: function () {
329 + opts.type = 'post'
330 + opts.mentions = '[' + name.textContent + '](' + msg.value.author + ')'
331 + if (msg.value.content.recps) {
332 + opts.recps = msg.value.content.recps
333 + }
334 + var r = message.childNodes.length - 1
335 + delete opts.updated
336 + delete opts.original
337 + delete fallback.messageText
338 + opts.boostContent = msg.value.content.text
339 + opts.boostKey = msg.key
340 + opts.boostAuthor = msg.value.author
341 + fallback.buttons = message.childNodes[r]
342 + var compose = h('div.message#re:' + msg.key.substring(0, 44), composer(opts, fallback))
343 + message.removeChild(message.childNodes[r])
344 + message.parentNode.insertBefore(compose, message.nextSibling)
345 + }
346 + }))
347 +
348 +
349 + if (msg.value.author == id)
350 + buttons.appendChild(h('button.btn', 'Edit', {
351 + onclick: function () {
352 + opts.type = 'edit'
353 + if (!fallback.messageText)
354 + fallback.messageText = msg.value.content.text
355 +
356 + if (!opts.updated)
357 + opts.updated = msg.key
358 + opts.original = msg.key
359 +
360 + var r = message.childNodes.length - 1
361 + fallback.buttons = message.childNodes[r]
362 + message.removeChild(message.childNodes[r])
363 + var compose = h('div#edit:' + msg.key.substring(0, 44), composer(opts, fallback))
364 + message.replaceChild(compose, message.lastElementChild)
365 + }
366 + }))
367 +
368 +
369 + var inputter = h('input', {placeholder: 'Add a label to this post ie art, books, new'})
370 +
371 + var labeler = h('div',
372 + inputter,
373 + h('button.btn', 'Publish label', {
374 + onclick: function () {
375 + var post = {}
376 + post.type = 'label',
377 + post.label = inputter.value,
378 + post.link = msg.key
379 +
380 + sbot.publish(post, function (err, msg){
381 + console.log(msg)
382 + labeler.parentNode.replaceChild(buttons, labeler)
383 + })
384 + }
385 + })
386 + )
387 +
388 + var labels = h('button.btn', 'Add label', {
389 + onclick: function () {
390 + buttons.parentNode.replaceChild(labeler, buttons)
391 + }
392 + })
393 +
394 + buttons.appendChild(labels)
395 + buttons.appendChild(tools.queueButton(msg))
396 + buttons.appendChild(tools.star(msg))
397 + message.appendChild(buttons)
398 + return message
399 +
400 + } else if (msg.value.content.type == 'vote') {
401 + if (msg.value.content.vote.value == 1)
402 + var link = h('span', ' ', h('img.emoji', {src: config.emojiUrl + 'star.png'}), ' ', h('a', {href: '#' + msg.value.content.vote.link}, tools.messageLink(msg.value.content.vote.link)))
403 + else if (msg.value.content.vote.value == -1)
404 + var link = h('span', ' ', h('img.emoji', {src: config.emojiUrl + 'stars.png'}), ' ', h('a', {href: '#' + msg.value.content.vote.link}, tools.messageLink(msg.value.content.vote.link)))
405 + message.appendChild(tools.mini(msg, link))
406 + return message
407 + } else if (typeof msg.value.content === 'string') {
408 + var unboxed = ssbKeys.unbox(msg.value.content, keys)
409 + if (unboxed) {
410 + msg.value.content = unboxed
411 + msg.value.private = true
412 + return module.exports(msg)
413 + } else {
414 + var privateMsg = h('span', ' sent a private message.')
415 + message.appendChild(tools.mini(msg, privateMsg))
416 + return message
417 + //return h('div')
418 + }
419 + } else {
420 +
421 + //FULL FALLBACK
422 + message.appendChild(tools.header(msg))
423 + message.appendChild(h('pre', tools.rawJSON(msg.value)))
424 +
425 + //MINI FALLBACK
426 + //var fallback = h('span', ' ' + msg.value.content.type)
427 + //message.appendChild(tools.mini(msg, fallback))
428 + return h('div', message)
429 + }
430 +}
mvd/style.cssView
@@ -1,0 +1,302 @@
1 +body {
2 + margin: 0;
3 + background: black;
4 + font-family: sans-serif;
5 + color: #f5f5f5;
6 + font-size: 14px;
7 + line-height: 20px;
8 +}
9 +
10 +#screen {
11 + position: absolute;
12 + top: 35px;
13 + bottom: 0px;
14 + left: 0px;
15 + right: 0px;
16 +}
17 +
18 +.hyperscroll {
19 + width: 100%;
20 +}
21 +
22 +.search {
23 + margin-top: 1.5px;
24 + float: right;
25 + width: 200px;
26 +}
27 +
28 +.header {
29 + padding-bottom: .7em;
30 + border-bottom: 1px solid #252525;
31 +}
32 +
33 +mark p, mark a {
34 + color: black;
35 +}
36 +
37 +h1, h2, h3, h4, h5, h6 {
38 + font-size: 1.2em;
39 + margin-top: .35ex;
40 +}
41 +
42 +hr {
43 + border: solid #222;
44 + clear: both;
45 + border-width: 1px 0 0;
46 + height: 0;
47 + margin-bottom: .9em;
48 +}
49 +
50 +
51 +p {
52 + margin-top: .35ex;
53 + margin-bottom: 10px;
54 +}
55 +
56 +a {
57 + color: cyan;
58 + text-decoration: none;
59 +}
60 +
61 +a:hover, a:focus {
62 + color: violet;
63 + text-decoration: underline;
64 +}
65 +
66 +.breadcrumbs {
67 + color: #363636;
68 + background: #f5f5f5;
69 +}
70 +
71 +/*.navbar a {
72 + color: #999;
73 + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
74 + text-decoration: none;
75 +}
76 +
77 +.navbar a:hover, .navbar a:focus {
78 + color: #fff;
79 + text-decoration: none;
80 +}*/
81 +
82 +.navbar {
83 + background: #1b1b1b;
84 + background: linear-gradient(#222, #111);
85 + border-bottom: 1px solid #252525;
86 +}
87 +
88 +.navbar {
89 + width: 100%;
90 + position: fixed;
91 + z-index: 1000;
92 + margin: 0;
93 + padding-top: .3em;
94 + padding-bottom: .3em;
95 + left: 0; right: 0;
96 + top: 0;
97 +}
98 +
99 +.navbar .internal {
100 + max-width: 97%;
101 + margin-left: auto;
102 + margin-right: auto;
103 +}
104 +
105 +.navbar li {
106 + margin-top: .3em;
107 + float: left;
108 + margin-right: .6em;
109 + margin-left: .3em;
110 + list-style-type: none;
111 +}
112 +
113 +.navbar li.right {
114 + padding-left: .4em;
115 + padding-right: .4em;
116 + margin-top: .3em;
117 + margin-right: 1.7em;
118 + float: right;
119 + list-style-type: none;
120 + background: #333;
121 + border-radius: 100%;
122 +}
123 +
124 +.content {
125 + max-width: 680px;
126 + margin-left: auto;
127 + margin-right: auto;
128 +}
129 +
130 +.hyperscroll > .content {
131 + max-width: 680px;
132 + margin-left: auto;
133 + margin-right: auto;
134 +}
135 +
136 +.message, .message > *, .navbar, .navbar > * {
137 + animation: fadein .5s;
138 +}
139 +
140 +@keyframes fadein {
141 + from { opacity: 0; }
142 + to { opacity: 1; }
143 +}
144 +
145 +.message {
146 + display: block;
147 + margin: .6em;
148 + background: #111;
149 + padding: .7em;
150 + border-radius: 3px;
151 + border: 1px solid #252525;
152 +}
153 +
154 +.message:hover, .embedded:hover {
155 + background: #141414;
156 +}
157 +
158 +.message img, .message video {
159 + max-width: 100%;
160 +}
161 +
162 +img {
163 + border-radius: 3px;
164 +}
165 +
166 +.timestamp, .votes {
167 + float: right;
168 +}
169 +
170 +.avatar--small img {
171 + vertical-align: top;
172 + width: 1.4em;
173 + height: 1.4em;
174 + margin-right: .2em;
175 +}
176 +
177 +.avatar--medium img {
178 + float: left;
179 + vertical-align: top;
180 + width: 5em;
181 + height: 5em;
182 + margin-right: .5em;
183 + margin-bottom: .5em;
184 +}
185 +
186 +.compose, textarea, input {
187 + font-family: sans-serif;
188 + font-size: 14px;
189 + line-height: 20px;
190 + background: #111;
191 + color: #ccc;
192 + border: none;
193 + border-radius: 3px;
194 +}
195 +
196 +textarea {
197 + width: 100%;
198 + height: 200px;
199 +}
200 +
201 +.compose:hover {
202 + background: #141414;
203 +}
204 +
205 +.compose:focus {
206 + outline: none;
207 +}
208 +
209 +.emoji {
210 + padding: .2em;
211 +}
212 +
213 +.right {
214 + float: right;
215 + margin-right: .25em;
216 +}
217 +
218 +.emoji {
219 + *float: left;
220 + width: 1em;
221 + vertical-align: top;
222 +}
223 +
224 +pre {
225 + width: 100%;
226 + display: block;
227 +}
228 +
229 +code {
230 + display: inline-block;
231 + vertical-align: bottom;
232 +}
233 +
234 +code, pre {
235 +overflow: auto;
236 +word-break: break-all;
237 +word-wrap: break-word;
238 +white-space: pre;
239 +white-space: -moz-pre-wrap;
240 +white-space: pre-wrap;
241 +white-space: pre\9;
242 +}
243 +
244 +code, pre {
245 + font-size: 12px;
246 + color: #ccc;
247 +}
248 +
249 +code {
250 + color: #ccc;
251 +}
252 +
253 +pre {
254 + margin: 0 0 10px;
255 + font-size: 13px;
256 + line-height: 20px;
257 +}
258 +
259 +button {margin: 0; margin-top: -.2em;}
260 +
261 +input {width: 88%; }
262 +
263 +#profile input {width: 50%;}
264 +
265 +.btn {
266 + display: inline-block;
267 + *display: inline;
268 + padding: 2px 6px;
269 + margin-bottom: 0;
270 + margin-right: .2em;
271 + font-size: 14px;
272 + line-height: 20px;
273 + color: #d5d5d5;
274 + text-align: center;
275 + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.75);
276 + vertical-align: middle;
277 + cursor: pointer;
278 + background-color: #222;
279 + border: 1px solid #222;
280 + border-radius: 4px;
281 +}
282 +
283 +
284 +.btn:hover,
285 +.btn:focus,
286 +.btn:active,
287 +.btn.active,
288 +.btn.disabled,
289 +.btn[disabled] {
290 + color: white;
291 + background-color: black;
292 +}
293 +
294 +.btn:active,
295 +.btn.active {
296 + background-color: #111;
297 +}
298 +
299 +.btn:first-child {
300 + *margin-left: 0;
301 +}
302 +
mvd/style.css.jsonView
@@ -1,0 +1,1 @@
1 +"body {\n margin: 0;\n background: black;\n font-family: sans-serif;\n color: #f5f5f5;\n font-size: 14px; \n line-height: 20px;\n}\n\n#screen {\n position: absolute;\n top: 35px;\n bottom: 0px;\n left: 0px;\n right: 0px;\n}\n\n.hyperscroll {\n width: 100%;\n}\n\n.search {\n margin-top: 1.5px;\n float: right;\n width: 200px;\n}\n\n.header {\n padding-bottom: .7em;\n border-bottom: 1px solid #252525;\n}\n\nmark p, mark a {\n color: black;\n}\n\nh1, h2, h3, h4, h5, h6 {\n font-size: 1.2em;\n margin-top: .35ex;\n}\n\nhr {\n border: solid #222;\n clear: both;\n border-width: 1px 0 0;\n height: 0;\n margin-bottom: .9em;\n}\n\n\np {\n margin-top: .35ex;\n margin-bottom: 10px;\n}\n\na {\n color: cyan;\n text-decoration: none;\n}\n\na:hover, a:focus {\n color: violet;\n text-decoration: underline; \n}\n\n.breadcrumbs {\n color: #363636;\n background: #f5f5f5;\n}\n\n/*.navbar a {\n color: #999;\n text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);\n text-decoration: none;\n}\n\n.navbar a:hover, .navbar a:focus {\n color: #fff;\n text-decoration: none;\n}*/\n\n.navbar {\n background: #1b1b1b;\n background: linear-gradient(#222, #111);\n border-bottom: 1px solid #252525;\n}\n\n.navbar {\n width: 100%;\n position: fixed;\n z-index: 1000;\n margin: 0;\n padding-top: .3em;\n padding-bottom: .3em;\n left: 0; right: 0;\n top: 0;\n}\n\n.navbar .internal {\n max-width: 97%;\n margin-left: auto;\n margin-right: auto;\n}\n\n.navbar li {\n margin-top: .3em;\n float: left;\n margin-right: .6em;\n margin-left: .3em;\n list-style-type: none;\n}\n\n.navbar li.right {\n padding-left: .4em;\n padding-right: .4em;\n margin-top: .3em;\n margin-right: 1.7em;\n float: right;\n list-style-type: none;\n background: #333;\n border-radius: 100%;\n}\n\n.content {\n max-width: 680px;\n margin-left: auto;\n margin-right: auto;\n}\n\n.hyperscroll > .content {\n max-width: 680px;\n margin-left: auto;\n margin-right: auto;\n}\n\n.message, .message > *, .navbar, .navbar > * {\n animation: fadein .5s;\n}\n\n@keyframes fadein {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n.message {\n display: block;\n margin: .6em;\n background: #111;\n padding: .7em;\n border-radius: 3px;\n border: 1px solid #252525;\n}\n\n.message:hover, .embedded:hover {\n background: #141414;\n}\n\n.message img, .message video {\n max-width: 100%;\n}\n\nimg {\n border-radius: 3px;\n}\n\n.timestamp, .votes {\n float: right;\n}\n \n.avatar--small img {\n vertical-align: top;\n width: 1.4em;\n height: 1.4em;\n margin-right: .2em;\n}\n\n.avatar--medium img {\n float: left;\n vertical-align: top;\n width: 5em;\n height: 5em;\n margin-right: .5em;\n margin-bottom: .5em;\n}\n\n.compose, textarea, input {\n font-family: sans-serif;\n font-size: 14px;\n line-height: 20px;\n background: #111;\n color: #ccc;\n border: none;\n border-radius: 3px;\n}\n\ntextarea {\n width: 100%;\n height: 200px;\n}\n\n.compose:hover {\n background: #141414;\n}\n\n.compose:focus {\n outline: none;\n}\n\n.emoji {\n padding: .2em;\n}\n\n.right {\n float: right;\n margin-right: .25em;\n}\n\n.emoji {\n *float: left;\n width: 1em;\n vertical-align: top;\n}\n\npre {\n width: 100%;\n display: block;\n}\n\ncode {\n display: inline-block;\n vertical-align: bottom;\n}\n\ncode, pre {\noverflow: auto;\nword-break: break-all;\nword-wrap: break-word;\nwhite-space: pre;\nwhite-space: -moz-pre-wrap;\nwhite-space: pre-wrap;\nwhite-space: pre\\9;\n}\n\ncode, pre {\n font-size: 12px;\n color: #ccc;\n}\n\ncode {\n color: #ccc;\n}\n\npre {\n margin: 0 0 10px;\n font-size: 13px;\n line-height: 20px;\n}\n\nbutton {margin: 0; margin-top: -.2em;}\n\ninput {width: 88%; }\n\n#profile input {width: 50%;}\n\n.btn {\n display: inline-block;\n *display: inline;\n padding: 2px 6px;\n margin-bottom: 0;\n margin-right: .2em;\n font-size: 14px;\n line-height: 20px;\n color: #d5d5d5;\n text-align: center;\n text-shadow: 0 1px 1px rgba(0, 0, 0, 0.75);\n vertical-align: middle;\n cursor: pointer;\n background-color: #222;\n border: 1px solid #222;\n border-radius: 4px;\n}\n\n\n.btn:hover,\n.btn:focus,\n.btn:active,\n.btn.active,\n.btn.disabled,\n.btn[disabled] {\n color: white;\n background-color: black;\n}\n\n.btn:active,\n.btn.active {\n background-color: #111;\n}\n\n.btn:first-child {\n *margin-left: 0;\n}\n\n"
mvd/style.jsView
@@ -1,0 +1,8 @@
1 +var fs = require('fs')
2 +var path = require('path')
3 +
4 +fs.writeFileSync(
5 + path.join(__dirname, 'style.css.json'),
6 + JSON.stringify(fs.readFileSync(path.join(__dirname, 'style.css'), 'utf8'))
7 +)
8 +
mvd/tools.jsView
@@ -1,0 +1,596 @@
1 +var h = require('hyperscript')
2 +var human = require('human-time')
3 +var avatar = require('./avatar')
4 +var ref = require('ssb-ref')
5 +
6 +var ssbKeys = require('ssb-keys')
7 +
8 +var pull = require('pull-stream')
9 +
10 +var sbot = require('./scuttlebot')
11 +
12 +var config = require('./config')()
13 +
14 +var id = require('./keys').id
15 +
16 +
17 +module.exports.getBlocks = function (src) {
18 + var blocks = h('div.blocks', 'Blocking: ')
19 +
20 + pull(
21 + sbot.query({query: [{$filter: { value: { author: src, content: {type: 'contact'}}}}], live: true}),
22 + pull.drain(function (msg) {
23 + if (msg.value) {
24 + if (msg.value.content.blocking == true) {
25 + console.log(msg.value)
26 + var gotIt = document.getElementById('blocks:' + msg.value.content.contact.substring(0, 44))
27 + if (gotIt == null) {
28 + blocks.appendChild(h('a#blocks:'+ msg.value.content.contact.substring(0, 44), {title: avatar.cachedName(msg.value.content.contact).textContent, href: '#' + msg.value.content.contact}, h('span.avatar--small', avatar.cachedImage(msg.value.content.contact))))
29 + }
30 + }
31 + if (msg.value.content.blocking == false) {
32 + var gotIt = document.getElementById('blocks:' + msg.value.content.contact.substring(0, 44))
33 + if (gotIt != null) {
34 + gotIt.outerHTML = ''
35 + }
36 + }
37 + }
38 + })
39 + )
40 +
41 + return blocks
42 +
43 +}
44 +
45 +module.exports.getBlocked = function (src) {
46 + var blocked = h('div.blocked', 'Blocked by: ')
47 +
48 + pull(
49 + sbot.query({query: [{$filter: { value: { content: {type: 'contact', contact: src}}}}], live: true}),
50 + pull.drain(function (msg) {
51 + if (msg.value) {
52 + if (msg.value.content.blocking == true) {
53 + console.log(msg.value)
54 + var gotIt = document.getElementById('blocked:' + msg.value.content.contact.substring(0, 44))
55 + if (gotIt == null) {
56 + blocked.appendChild(h('a#blocked:'+ msg.value.author.substring(0, 44), {title: avatar.cachedName(msg.value.author).textContent, href: '#' + msg.value.author}, h('span.avatar--small', avatar.cachedImage(msg.value.author))))
57 + }
58 + }
59 + if (msg.value.content.blocking == false) {
60 + var gotIt = document.getElementById('blocked:' + msg.value.author.substring(0, 44))
61 + if (gotIt != null) {
62 + gotIt.outerHTML = ''
63 + }
64 + }
65 + }
66 + })
67 + )
68 +
69 + return blocked
70 +
71 +}
72 +
73 +module.exports.getFollowing = function (src) {
74 + var followingCount = 0
75 +
76 + var following = h('div.following', 'Following: ')
77 +
78 + following.appendChild(h('span#followingcount', '0'))
79 + following.appendChild(h('br'))
80 +
81 + pull(
82 + sbot.query({query: [{$filter: { value: { author: src, content: {type: 'contact'}}}}], live: true}),
83 + pull.drain(function (msg) {
84 + if (msg.value) {
85 + if (msg.value.content.following == true) {
86 + followingcount = document.getElementById('followingcount')
87 + followingCount++
88 + followingcount.textContent = followingCount
89 + var gotIt = document.getElementById('following:' + msg.value.content.contact.substring(0, 44))
90 + if (gotIt == null) {
91 + following.appendChild(h('a#following:'+ msg.value.content.contact.substring(0, 44), {title: avatar.cachedName(msg.value.content.contact).textContent, href: '#' + msg.value.content.contact}, h('span.avatar--small', avatar.cachedImage(msg.value.content.contact))))
92 + }
93 + }
94 + if (msg.value.content.following == false) {
95 + followingcount = document.getElementById('followingcount')
96 + followingCount--
97 + followingcount.textContent = followingCount
98 + var gotIt = document.getElementById('following:' + msg.value.content.contact.substring(0, 44))
99 + if (gotIt != null) {
100 + gotIt.outerHTML = ''
101 + }
102 + }
103 + }
104 + })
105 + )
106 + return following
107 +}
108 +
109 +module.exports.getFollowers = function (src) {
110 + var followerCount = 0
111 +
112 + var followers = h('div.followers', 'Followers: ')
113 +
114 + followers.appendChild(h('span#followercount', '0'))
115 + followers.appendChild(h('br'))
116 +
117 + pull(
118 + sbot.query({query: [{$filter: { value: { content: {type: 'contact', contact: src}}}}], live: true}),
119 + pull.drain(function (msg) {
120 + if (msg.value) {
121 + if (msg.value.content.following == true) {
122 + followcount = document.getElementById('followercount')
123 + followerCount++
124 + followcount.textContent = followerCount
125 + var gotIt = document.getElementById('followers:' + msg.value.author.substring(0, 44))
126 + if (gotIt == null) {
127 + followers.appendChild(h('a#followers:'+ msg.value.author.substring(0, 44), {title: avatar.cachedName(msg.value.author).textContent, href: '#' + msg.value.author}, h('span.avatar--small', avatar.cachedImage(msg.value.author))))
128 + }
129 + }
130 + if (msg.value.content.following == false) {
131 + followcount = document.getElementById('followercount')
132 + followerCount--
133 + followcount.textContent = followerCount
134 + var gotIt = document.getElementById('followers:' + msg.value.author.substring(0, 44))
135 + if (gotIt != null) {
136 + gotIt.outerHTML = ''
137 + }
138 + }
139 + }
140 + })
141 + )
142 +
143 + return followers
144 +}
145 +
146 +module.exports.queueButton = function (src) {
147 + var queueButton = h('span.queue:' + src.key.substring(0,44))
148 +
149 + var addToQueue = h('button.btn.right', 'Queue', {
150 + onclick: function () {
151 + var content = {
152 + type: 'queue',
153 + message: src.key,
154 + queue: true
155 + }
156 + sbot.publish(content, function (err, publish) {
157 + if (err) throw err
158 + console.log(publish)
159 + })
160 + }
161 + })
162 +
163 + var removeFromQueue = h('button.btn.right#', 'Done', {
164 + onclick: function () {
165 + var content = {
166 + type: 'queue',
167 + message: src.key,
168 + queue: false
169 + }
170 + sbot.publish(content, function (err, publish) {
171 + if (err) throw err
172 + console.log(publish)
173 + if (window.location.hash.substring(1) == 'queue') {
174 + setTimeout(function () {
175 + var gotIt = document.getElementById(src.key.substring(0,44))
176 + if (gotIt != null) {
177 + gotIt.outerHTML = ''
178 + }
179 + }, 100)
180 +
181 + }
182 + })
183 + }
184 + })
185 +
186 + pull(
187 + sbot.query({query: [{$filter: { value: { author: id, content: {type: 'queue', message: src.key}}}}], live: true}),
188 + pull.drain(function (msg) {
189 + if (msg.value) {
190 + if (msg.value.content.queue == true) {
191 + queueButton.removeChild(queueButton.childNodes[0])
192 + queueButton.appendChild(removeFromQueue)
193 + }
194 + if (msg.value.content.queue == false) {
195 + queueButton.removeChild(queueButton.childNodes[0])
196 + queueButton.appendChild(addToQueue)
197 + }
198 + }
199 + })
200 + )
201 +
202 + queueButton.appendChild(addToQueue)
203 +
204 + return queueButton
205 +}
206 +module.exports.block = function (src) {
207 + var button = h('span.button')
208 +
209 + var followButton = h('button.btn', 'Block (Private)', avatar.name(src), {
210 + onclick: function () {
211 + var content = {
212 + type: 'contact',
213 + contact: src,
214 + blocking: true,
215 + recps: id
216 + }
217 + sbot.publish(content, function (err, publish) {
218 + if (err) throw err
219 + console.log(publish)
220 + })
221 + }
222 + })
223 +
224 + var unfollowButton = h('button.btn', 'Unblock (Private)', avatar.name(src), {
225 + onclick: function () {
226 + var content = {
227 + type: 'contact',
228 + contact: src,
229 + blocking: false,
230 + recps: id
231 + }
232 + sbot.publish(content, function (err, publish) {
233 + if (err) throw err
234 + console.log(publish)
235 + })
236 + }
237 + })
238 +
239 + pull(
240 + sbot.query({query: [{$filter: { value: { author: id, content: {type: 'contact', contact: src}}}}], live: true}),
241 + pull.drain(function (msg) {
242 + if (msg.value) {
243 + if (msg.value.content.blocking == true) {
244 + button.removeChild(button.firstChild)
245 + button.appendChild(unfollowButton)
246 + }
247 + if (msg.value.content.blocking == false) {
248 + button.removeChild(button.firstChild)
249 + button.appendChild(followButton)
250 + }
251 + }
252 + })
253 + )
254 +
255 + button.appendChild(followButton)
256 +
257 + return button
258 +}
259 +
260 +module.exports.box = function (content) {
261 + return ssbKeys.box(content, content.recps.map(function (e) {
262 + return ref.isFeed(e) ? e : e.link
263 + }))
264 +}
265 +
266 +module.exports.publish = function (content, cb) {
267 + if(content.recps)
268 + content = exports.box(content)
269 + sbot.publish(content, function (err, msg) {
270 + if(err) throw err
271 + console.log('Published!', msg)
272 + if(cb) cb(err, msg)
273 + })
274 +}
275 +
276 +
277 +
278 +module.exports.follow = function (src) {
279 + var button = h('span.button')
280 +
281 + var followButton = h('button.btn', 'Follow ', avatar.name(src), {
282 + onclick: function () {
283 + var content = {
284 + type: 'contact',
285 + contact: src,
286 + following: true
287 + }
288 + sbot.publish(content, function (err, publish) {
289 + if (err) throw err
290 + console.log(publish)
291 + })
292 + }
293 + })
294 +
295 + var unfollowButton = h('button.btn', 'Unfollow ', avatar.name(src), {
296 + onclick: function () {
297 + var content = {
298 + type: 'contact',
299 + contact: src,
300 + following: false
301 + }
302 + sbot.publish(content, function (err, publish) {
303 + if (err) throw err
304 + console.log(publish)
305 + })
306 + }
307 + })
308 +
309 + pull(
310 + sbot.query({query: [{$filter: { value: { author: id, content: {type: 'contact', contact: src}}}}], live: true}),
311 + pull.drain(function (msg) {
312 + if (msg.value) {
313 + if (msg.value.content.following == true) {
314 + button.removeChild(button.firstChild)
315 + button.appendChild(unfollowButton)
316 + }
317 + if (msg.value.content.following == false) {
318 + button.removeChild(button.firstChild)
319 + button.appendChild(followButton)
320 + }
321 + }
322 + })
323 + )
324 +
325 + button.appendChild(followButton)
326 +
327 + return button
328 +}
329 +
330 +module.exports.box = function (content) {
331 + return ssbKeys.box(content, content.recps.map(function (e) {
332 + return ref.isFeed(e) ? e : e.link
333 + }))
334 +}
335 +
336 +module.exports.publish = function (content, cb) {
337 + if(content.recps)
338 + content = exports.box(content)
339 + sbot.publish(content, function (err, msg) {
340 + if(err) throw err
341 + console.log('Published!', msg)
342 + if(cb) cb(err, msg)
343 + })
344 +}
345 +
346 +
347 +
348 +module.exports.mute = function (src) {
349 + if (!localStorage[src])
350 + var cache = {mute: false}
351 + else
352 + var cache = JSON.parse(localStorage[src])
353 +
354 + if (cache.mute == true) {
355 + var mute = h('button.btn', 'Unmute', {
356 + onclick: function () {
357 + cache.mute = false
358 + localStorage[src] = JSON.stringify(cache)
359 + location.hash = '#'
360 + location.hash = src
361 + }
362 + })
363 + return mute
364 + } else {
365 + var mute = h('button.btn', 'Mute', {
366 + onclick: function () {
367 + cache.mute = true
368 + localStorage[src] = JSON.stringify(cache)
369 + location.hash = '#'
370 + location.hash = src
371 + }
372 + })
373 + return mute
374 + }
375 +}
376 +
377 +module.exports.star = function (msg) {
378 + var votebutton = h('span.star:' + msg.key.substring(0,44))
379 +
380 + var vote = {
381 + type: 'vote',
382 + vote: { link: msg.key, expression: 'Star' }
383 + }
384 +
385 + if (msg.value.content.recps) {
386 + vote.recps = msg.value.content.recps
387 + }
388 +
389 + var star = h('button.btn.right', 'Star ',
390 + h('img.emoji', {src: config.emojiUrl + 'star.png'}), {
391 + onclick: function () {
392 + vote.vote.value = 1
393 + if (vote.recps) {
394 + vote = exports.box(vote)
395 + }
396 + sbot.publish(vote, function (err, voted) {
397 + if(err) throw err
398 + })
399 + }
400 + }
401 + )
402 +
403 + var unstar = h('button.btn.right ', 'Unstar ',
404 + h('img.emoji', {src: config.emojiUrl + 'stars.png'}), {
405 + onclick: function () {
406 + vote.vote.value = -1
407 + sbot.publish(vote, function (err, voted) {
408 + if(err) throw err
409 + })
410 + }
411 + }
412 + )
413 +
414 + votebutton.appendChild(star)
415 +
416 + pull(
417 + sbot.links({rel: 'vote', dest: msg.key, live: true}),
418 + pull.drain(function (link) {
419 + if (link.key) {
420 + sbot.get(link.key, function (err, data) {
421 + if (err) throw err
422 + if (data.content.vote) {
423 + if (data.author == id) {
424 + while (votebutton.firstChild) {
425 + votebutton.removeChild(votebutton.firstChild)
426 + }
427 + if (data.content.vote.value == 1)
428 + //votebutton.removeChild(votebutton.childNodes[0])
429 + votebutton.appendChild(unstar)
430 + if (data.content.vote.value == -1)
431 + //votebutton.removeChild(votebutton.childNodes[0])
432 + votebutton.appendChild(star)
433 + }
434 + }
435 + })
436 + }
437 + })
438 + )
439 +
440 + return votebutton
441 +}
442 +
443 +function votes (msg) {
444 + var votes = h('div.votes.right')
445 + if (msg.key) {
446 + pull(
447 + sbot.links({rel: 'vote', dest: msg.key, live: true }),
448 + pull.drain(function (link) {
449 + if (link.key) {
450 + sbot.get(link.key, function (err, data) {
451 + if (err) throw err
452 + if (data.content.vote) {
453 + if (data.content.vote.value == 1) {
454 + if (localStorage[data.author + 'name'])
455 + name = localStorage[data.author + 'name']
456 + else
457 + name = data.author
458 + votes.appendChild(h('a#vote:' + data.author.substring(0, 44), {href:'#' + data.author, title: name}, h('img.emoji', {src: config.emojiUrl + 'star.png'})))
459 + }
460 + else if (data.content.vote.value == -1) {
461 + var lookFor = 'vote:' + data.author.substring(0, 44)
462 + document.getElementById(lookFor, function (err, gotit) {
463 + if (err) throw err
464 + gotit.parentNode.removeChild(remove)
465 + })
466 + }
467 + }
468 + })
469 + }
470 + })
471 + )
472 + }
473 + return votes
474 +}
475 +
476 +module.exports.timestamp = function (msg, edited) {
477 + var timestamp
478 + if (edited)
479 + timestamp = h('span.timestmap.right', 'Edited by: ', h('a', {href: '#' + msg.value.author}, h('span.avatar--small', avatar.cachedImage(msg.value.author))), h('a', {href: '#' + msg.key}, human(new Date(msg.value.timestamp))))
480 + else
481 + timestamp = h('span.timestamp.right', h('a', {href: '#' + msg.key}, human(new Date(msg.value.timestamp))))
482 + return timestamp
483 +}
484 +
485 +
486 +module.exports.mini = function (msg, content) {
487 + var mini = h('div.mini')
488 +
489 + mini.appendChild(
490 + h('span.avatar',
491 + h('a', {href: '#' + msg.value.author},
492 + h('span.avatar--small', avatar.cachedImage(msg.value.author)),
493 + avatar.cachedName(msg.value.author)
494 + )
495 + )
496 + )
497 + var lock = h('span.right', h('img.emoji', {src: config.emojiUrl + 'lock.png'}))
498 +
499 +
500 + mini.appendChild(h('span', content))
501 + mini.appendChild(exports.timestamp(msg))
502 +
503 + if (msg.value.content.recps) {
504 + mini.appendChild(lock)
505 + }
506 +
507 + if (typeof msg.value.content === 'string') {
508 + mini.appendChild(lock)
509 + }
510 +
511 + return mini
512 +}
513 +
514 +module.exports.header = function (msg) {
515 + var header = h('div.header')
516 +
517 + header.appendChild(h('span.avatar',
518 + h('a', {href: '#' + msg.value.author},
519 + h('span.avatar--small', avatar.cachedImage(msg.value.author)),
520 + avatar.cachedName(msg.value.author)
521 + )
522 + )
523 + )
524 +
525 + header.appendChild(exports.timestamp(msg))
526 + header.appendChild(votes(msg))
527 +
528 + if (msg.value.private) {
529 + header.appendChild(h('span.right', ' ', h('img.emoji', {src: config.emojiUrl + 'lock.png'})))
530 + }
531 + if (msg.value.content.type == 'edit') {
532 + header.appendChild(h('span.right', ' Edited: ', h('a', {href: '#' + msg.value.content.original}, exports.messageLink(msg.value.content.original))))
533 + }
534 + return header
535 +}
536 +
537 +
538 +
539 +
540 +module.exports.messageName = function (id, cb) {
541 + // gets the first few characters of a message, for message-link
542 + function title (s) {
543 + var m = /^\n*([^\n]{0,40})/.exec(s)
544 + return m && (m[1].length == 40 ? m[1]+'...' : m[1])
545 + }
546 +
547 + sbot.get(id, function (err, value) {
548 + if(err && err.name == 'NotFoundError')
549 + return cb(null, id.substring(0, 10)+'...(missing)')
550 + if(value.content.type === 'post' && 'string' === typeof value.content.text)
551 + return cb(null, title(value.content.text))
552 + else if('string' === typeof value.content.text)
553 + return cb(null, value.content.type + ':'+title(value.content.text))
554 + else
555 + return cb(null, id.substring(0, 10)+'...')
556 + })
557 +}
558 +
559 +var messageName = exports.messageName
560 +
561 +module.exports.messageLink = function (id) {
562 + if (ref.isMsg(id)) {
563 + var link = h('a', {href: '#'+id}, id.substring(0, 10)+'...')
564 + messageName(id, function (err, name) {
565 + if(err) console.error(err)
566 + else link.textContent = name
567 + })
568 + } else {
569 + var link = id
570 + }
571 + return link
572 +}
573 +
574 +module.exports.rawJSON = function (obj) {
575 + return JSON.stringify(obj, null, 2)
576 + .split(/([%@&][a-zA-Z0-9\/\+]{43}=*\.[\w]+)/)
577 + .map(function (e) {
578 + if(ref.isMsg(e) || ref.isFeed(e) || ref.isBlob(e)) {
579 + return h('a', {href: '#' + e}, e)
580 + }
581 + return e
582 + })
583 +}
584 +
585 +var markdown = require('ssb-markdown')
586 +var config = require('./config')()
587 +
588 +module.exports.markdown = function (msg, md) {
589 + return {innerHTML: markdown.block(msg, {toUrl: function (url, image) {
590 + if(url[0] == '%' || url[0] == '@' || url[0] == '#') return '#' + url
591 + if(url[0] !== '&') return url
592 + //if(url[0] == '&') return config.blobsUrl + url
593 + //if(!image) return url
594 + return config.blobsUrl + url
595 + }})}
596 +}
mvd/views.jsView
@@ -1,0 +1,756 @@
1 +var pull = require('pull-stream')
2 +var human = require('human-time')
3 +var sbot = require('./scuttlebot')
4 +var hyperscroll = require('hyperscroll')
5 +var hyperfile = require('hyperfile')
6 +var dataurl = require('dataurl-')
7 +var More = require('pull-more')
8 +var stream = require('hyperloadmore/stream')
9 +var h = require('hyperscript')
10 +var render = require('./render')
11 +var ref = require('ssb-ref')
12 +var client = require('ssb-client')
13 +var Next = require('pull-next-query')
14 +var config = require('./config')()
15 +var tools = require('./tools')
16 +var avatar = require('./avatar')
17 +var id = require('./keys').id
18 +var ssbKeys = require('ssb-keys')
19 +var keys = require('./keys')
20 +var compose = require('./compose')
21 +
22 +
23 +var labelStream = function (label){
24 + var content = h('div.content')
25 + var screen = document.getElementById('screen')
26 + screen.appendChild(hyperscroll(content))
27 + content.appendChild(h('div.breadcrumbs.message', h('a', {href: '/'}, 'label'), ' ⯈ ' , h('a', {href: '/#label/' + label}, label)))
28 + function createStream (opts) {
29 + return pull(
30 + Next(sbot.query, opts, ['value', 'timestamp']),
31 + pull.map(function (msg){
32 + if (msg.value) {
33 + sbot.get(msg.value.content.link, function (err, data) {
34 + if (data) {
35 + var message = {}
36 + message.value = data
37 + message.key = msg.value.content.link
38 + content.appendChild(render(message))
39 + }
40 + })
41 + }
42 + })
43 + )
44 + }
45 +
46 + pull(
47 + createStream({
48 + limit: 10,
49 + reverse: true,
50 + live: false,
51 + query: [{$filter: { value: { content: {type: 'label', label: label }, timestamp: { $gt: 0 }}}}]
52 + }),
53 + stream.bottom(content)
54 + )
55 +
56 + pull(
57 + createStream({
58 + limit: 10,
59 + old: false,
60 + live: true,
61 + query: [{$filter: { value: { content: {type: 'label', label: label }, timestamp: { $gt: 0 }}}}]
62 + }),
63 + stream.top(content)
64 + )
65 +}
66 +
67 +
68 +var privateStream = function () {
69 + var screen = document.getElementById('screen')
70 + var content = h('div.content')
71 +
72 + screen.appendChild(hyperscroll(content))
73 +
74 + function createStream (opts) {
75 + return pull(
76 + Next(sbot.query, opts, ['value', 'timestamp']),
77 + pull.filter(function (msg) {
78 + return ((msg.value.private == true) || ('string' == typeof msg.value.content))
79 + }),
80 + pull.map(function (msg) {
81 + /*if (msg.value.private != true) {
82 + var unboxed = ssbKeys.unbox(msg.value.content, keys)
83 + if (unboxed) {
84 + msg.value.content = unboxed
85 + msg.value.private = true
86 + return render(msg)
87 + } else {
88 + return render(msg)
89 + }
90 + } else {return render(msg)}*/
91 + return render(msg)
92 + })
93 + )
94 + }
95 +
96 + pull(
97 + createStream({
98 + limit: 100,
99 + reverse: true,
100 + live: false,
101 + query: [{$filter: { value: { timestamp: { $gt: 0 }}}}]
102 + }),
103 + stream.bottom(content)
104 + )
105 +
106 + pull(
107 + createStream({
108 + limit: 100,
109 + old: false,
110 + live: true,
111 + query: [{$filter: { value: { timestamp: { $gt: 0 }}}}]
112 + }),
113 + stream.top(content)
114 + )
115 +
116 +
117 + /*function createStream (opts) {
118 + return pull(
119 + Next(sbot.query, opts, ['value', 'timestamp']),
120 + pull.map(function (msg) {
121 + if (msg.value) {
122 + if (msg.value.timestamp > Date.now()) {
123 + return h('div.future')
124 + } else {
125 + return render(msg)
126 + }
127 + }
128 + })
129 + )
130 + }
131 +
132 + pull(
133 + createStream({
134 + limit: 10,
135 + reverse: true,
136 + live: false,
137 + query: [{$filter: { value: { private: true, timestamp: { $gt: 0 }}}}]
138 + }),
139 + stream.bottom(content)
140 + )
141 +
142 + pull(
143 + createStream({
144 + limit: 10,
145 + old: false,
146 + live: true,
147 + query: [{$filter: { value: { private: true, timestamp: { $gt: 0 }}}}]
148 + }),
149 + stream.top(content)
150 + )*/
151 +
152 +
153 + /*function createStream (opts) {
154 + return pull(
155 + More(sbot.createLogStream, opts),
156 + pull.filter(function (msg) {
157 + return 'string' == typeof msg.value.content
158 + }),
159 + pull.filter(function (msg) {
160 + var unboxed = ssbKeys.unbox(msg.value.content, keys)
161 + if (unboxed) {
162 + msg.value.content = unboxed
163 + msg.value.private = true
164 + return msg
165 + } else {
166 + return msg
167 + }
168 + }),
169 + pull.map(function (msg) {
170 + return render(msg)
171 + })
172 + )
173 + }
174 +
175 + pull(
176 + createStream({old: false, limit: 1000}),
177 + stream.top(content)
178 + )
179 +
180 + pull(
181 + createStream({reverse: true, live: false, limit: 1000}),
182 + stream.bottom(content)
183 + )*/
184 +}
185 +
186 +var queueStream = function () {
187 + var content = h('div.content')
188 + var screen = document.getElementById('screen')
189 + screen.appendChild(hyperscroll(content))
190 +
191 + pull(
192 + sbot.query({query: [{$filter: { value: {author: id, content: {type: 'queue'}}}}]}),
193 + pull.drain(function (msg) {
194 + if (msg.value) {
195 + if (ref.isMsg(msg.value.content.message)) {
196 + if (msg.value.content.queue == true) {
197 + sbot.get(msg.value.content.message, function (err, data) {
198 + if (data) {
199 + var message = {}
200 + message.value = data
201 + message.key = msg.value.content.message
202 + content.appendChild(render(message))
203 + }
204 + })
205 + }
206 + if (msg.value.content.queue == false) {
207 + setTimeout(function () {
208 + var gotIt = document.getElementById(msg.value.content.message.substring(0,44))
209 + if (gotIt != null) {
210 + gotIt.outerHTML = ''
211 + }
212 + }, 100)
213 + }
214 + }
215 + }
216 + })
217 + )
218 +}
219 +
220 +var mentionsStream = function (src) {
221 + var content = h('div.content')
222 +
223 + var screen = document.getElementById('screen')
224 +
225 + screen.appendChild(hyperscroll(content))
226 +
227 + function createStream (opts) {
228 + return pull(
229 + Next(sbot.backlinks, opts, ['value', 'timestamp']),
230 + pull.map(function (msg) {
231 + if (msg.value.private == true) return h('div.private')
232 + return render(msg)
233 + })
234 + )
235 + }
236 +
237 + pull(
238 + createStream({
239 + limit: 10,
240 + reverse: true,
241 + index: 'DTA',
242 + live: false,
243 + query: [{$filter: {dest: src}}]
244 + }),
245 + stream.bottom(content)
246 + )
247 +
248 + pull(
249 + createStream({
250 + limit: 10,
251 + old: false,
252 + index: 'DTA',
253 + live: true,
254 + query: [{$filter: {dest: src}}]
255 + }),
256 + stream.top(content)
257 + )
258 +}
259 +
260 +var userStream = function (src) {
261 + var content = h('div.content')
262 + var screen = document.getElementById('screen')
263 +
264 + screen.appendChild(hyperscroll(content))
265 +
266 + function createStream (opts) {
267 + return pull(
268 + More(sbot.userStream, opts, ['value', 'sequence']),
269 + pull.map(function (msg) {
270 + return render(h('div', msg))
271 + })
272 + )
273 + }
274 +
275 + pull(
276 + createStream({old: false, limit: 10, id: src}),
277 + stream.top(content)
278 + )
279 +
280 + pull(
281 + createStream({reverse: true, live: false, limit: 10, id: src}),
282 + stream.bottom(content)
283 + )
284 +
285 + var profile = h('div.content#profile', h('div.message'))
286 +
287 + if (screen.firstChild.firstChild) {
288 + screen.firstChild.insertBefore(profile, screen.firstChild.firstChild)
289 + } else {
290 + screen.firstChild.appendChild(profile)
291 + }
292 +
293 + var name = avatar.name(src)
294 +
295 + var editname = h('span',
296 + avatar.name(src),
297 + h('button.btn', 'New name', {
298 + onclick: function () {
299 + var nameput = h('input', {placeholder: name.textContent})
300 + var nameedit =
301 + h('span', nameput,
302 + h('button.btn', 'Preview', {
303 + onclick: function () {
304 + if (nameput.value[0] != '@')
305 + tobename = nameput.value
306 + else
307 + tobename = nameput.value.substring(1, 100)
308 + var newname = h('span', h('a', {href: '#' + src}, '@' + tobename), h('button.btn', 'Publish', {
309 + onclick: function () {
310 + var donename = h('span', h('a', {href: '#' + src}, '@' + tobename))
311 + sbot.publish({type: 'about', about: src, name: tobename})
312 + localStorage[src + 'name'] = tobename
313 + newname.parentNode.replaceChild(donename, newname)
314 + }
315 + }))
316 + nameedit.parentNode.replaceChild(newname, nameedit)
317 + }
318 + })
319 + )
320 + editname.parentNode.replaceChild(nameedit, editname)
321 + }
322 + })
323 + )
324 +
325 + var editimage = h('span',
326 + h('button.btn', 'New image', {
327 + onclick: function () {
328 + var upload =
329 + h('span',
330 + hyperfile.asDataURL(function (data) {
331 + if(data) {
332 + //img.src = data
333 + var _data = dataurl.parse(data)
334 + pull(
335 + pull.once(_data.data),
336 + sbot.addblob(function (err, hash) {
337 + if(err) return alert(err.stack)
338 + selected = {
339 + link: hash,
340 + size: _data.data.length,
341 + type: _data.mimetype
342 + }
343 + })
344 + )
345 + }
346 + }),
347 + h('button.btn', 'Preview image', {
348 + onclick: function() {
349 + if (selected) {
350 + console.log(selected)
351 + var oldImage = document.getElementById('profileImage')
352 + var newImage = h('span.avatar--medium', h('img', {src: config.blobsUrl + selected.link}))
353 + var publish = h('button.btn', 'Publish image', {
354 + onclick: function () {
355 + sbot.publish({
356 + type: 'about',
357 + about: src,
358 + image: selected
359 + }, function (err, published) {
360 + console.log(published)
361 + })
362 + }
363 + })
364 + upload.parentNode.replaceChild(publish, upload)
365 + oldImage.parentNode.replaceChild(newImage, oldImage)
366 + }
367 + /*if(selected) {
368 + api.message_confirm({
369 + type: 'about',
370 + about: id,
371 + image: selected
372 + })
373 + } else { alert('select an image before hitting preview')}*/
374 + }
375 + })
376 + )
377 + editimage.parentNode.replaceChild(upload, editimage)
378 + }
379 + })
380 + )
381 +
382 + var avatars = h('div.avatars',
383 + h('a', {href: '#' + src},
384 + h('span.avatar--medium#profileImage', avatar.image(src)),
385 + editname,
386 + h('br'),
387 + editimage
388 + )
389 + )
390 +
391 + pull(
392 + sbot.userStream({id: src, reverse: false, limit: 1}),
393 + pull.drain(function (msg) {
394 + var howlong = h('span', h('br'), ' arrived ', human(new Date(msg.value.timestamp)))
395 + avatars.appendChild(howlong)
396 + console.log(msg)
397 + })
398 + )
399 +
400 +
401 + var buttons = h('div.buttons')
402 +
403 + profile.firstChild.appendChild(avatars)
404 + profile.firstChild.appendChild(buttons)
405 + buttons.appendChild(tools.mute(src))
406 +
407 + var writeMessage = h('button.btn', 'Public message ', avatar.name(src), {
408 + onclick: function () {
409 + opts = {}
410 + opts.type = 'post'
411 + opts.mentions = '[' + name.textContent + '](' + src + ')'
412 + var composer = h('div#composer', h('div.message', compose(opts)))
413 + profile.appendChild(composer)
414 + }
415 + })
416 +
417 + var writePrivate = h('button.btn', 'Private message ', avatar.name(src), {
418 + onclick: function () {
419 + opts = {}
420 + opts.type = 'post'
421 + opts.mentions = '[' + name.textContent + '](' + src + ')'
422 + opts.recps = [src, id]
423 + var composer = h('div#composer', h('div.message', compose(opts)))
424 + profile.appendChild(composer)
425 + }
426 + })
427 +
428 + buttons.appendChild(writeMessage)
429 + buttons.appendChild(writePrivate)
430 + buttons.appendChild(tools.follow(src))
431 + buttons.appendChild(tools.block(src))
432 +
433 + buttons.appendChild(h('button.btn', 'Generate follows', {
434 + onclick: function () {
435 + profile.firstChild.appendChild(tools.getFollowing(src))
436 + profile.firstChild.appendChild(tools.getFollowers(src))
437 + }
438 + }))
439 +
440 + buttons.appendChild(h('button.btn', 'Generate blocks', {
441 + onclick: function () {
442 + profile.firstChild.appendChild(tools.getBlocks(src))
443 + profile.firstChild.appendChild(tools.getBlocked(src))
444 + }
445 + }))
446 + buttons.appendChild(h('a', {href: '#wall/' + src}, h('button.btn', avatar.name(src), "'s wall")))
447 +
448 +}
449 +
450 +var privateMsg = function (src) {
451 + var content = h('div.content')
452 + var screen = document.getElementById('screen')
453 + screen.appendChild(hyperscroll(content))
454 +
455 + sbot.get(src, function (err, data) {
456 + if (err) {
457 + var message = h('div.message', 'Missing message!')
458 + content.appendChild(message)
459 + }
460 + if (data) {
461 + console.log(data)
462 + data.value = data
463 + data.key = src
464 +
465 + content.appendChild(render(data))
466 + }
467 +
468 + })
469 +}
470 +
471 +var msgThread = function (src) {
472 +
473 + var content = h('div.content')
474 + var screen = document.getElementById('screen')
475 + screen.appendChild(hyperscroll(content))
476 +
477 + pull(
478 + sbot.query({query: [{$filter: { value: { content: {root: src}, timestamp: { $gt: 1 }}}}], live: true}),
479 + pull.drain(function (msg) {
480 + if (msg.value) {
481 + content.appendChild(render(msg))
482 + }
483 + })
484 + )
485 +
486 + sbot.get(src, function (err, data) {
487 + if (err) {
488 + var message = h('div.message', 'Missing message!')
489 + content.appendChild(message)
490 + }
491 + if (data) {
492 + var message = {}
493 + message.value = data
494 + message.key = src
495 + console.log(message)
496 + var rootMsg = render(message)
497 +
498 + if (content.firstChild) {
499 + content.insertBefore(rootMsg, content.firstChild)
500 + } else {
501 + content.appendChild(rootMsg)
502 + }
503 + if (message.value.content.type == 'git-repo') {
504 + pull(
505 + sbot.backlinks({query: [{$filter: {value: {content: {type: 'git-update'}}, dest: src}}]}),
506 + pull.drain(function (msg) {
507 + if (msg.value) {
508 + content.appendChild(render(msg))
509 + }
510 + })
511 + )
512 + }
513 +
514 + }
515 + })
516 +
517 +}
518 +
519 +var keyPage = function () {
520 + var screen = document.getElementById('screen')
521 +
522 + var importKey = h('textarea.import', {placeholder: 'Import a new public/private key', name: 'textarea', style: 'width: 97%; height: 100px;'})
523 +
524 + var content = h('div.content',
525 + h('div.message#key',
526 + h('h1', 'Your Key'),
527 + h('p', {innerHTML: 'Your public/private key is: <pre><code>' + localStorage[config.caps.shs + '/secret'] + '</code></pre>'},
528 + h('button.btn', {onclick: function (e){
529 + localStorage[config.caps.shs +'/secret'] = ''
530 + alert('Your public/private key has been deleted')
531 + e.preventDefault()
532 + location.hash = ""
533 + location.reload()
534 + }}, 'Delete Key')
535 + ),
536 + h('hr'),
537 + h('form',
538 + importKey,
539 + h('button.btn', {onclick: function (e){
540 + if(importKey.value) {
541 + localStorage[config.caps.shs + '/secret'] = importKey.value.replace(/\s+/g, ' ')
542 + e.preventDefault()
543 + alert('Your public/private key has been updated')
544 + }
545 + location.hash = ""
546 + location.reload()
547 + }}, 'Import key'),
548 + )
549 + )
550 + )
551 +
552 + screen.appendChild(hyperscroll(content))
553 +}
554 +
555 +
556 +function friendsStream (src) {
557 +
558 + var screen = document.getElementById('screen')
559 + var content = h('div.content')
560 +
561 + screen.appendChild(hyperscroll(content))
562 +
563 + function createStream (opts) {
564 + return pull(
565 + Next(sbot.query, opts, ['value', 'timestamp']),
566 + pull.map(function (msg) {
567 + sbot.friends.get({source: src, dest: msg.value.author}, function (err, data) {
568 + if (data === true) {
569 + return content.appendChild(render(msg))
570 + console.log(msg)
571 + } else {
572 + return content.appendChild(h('div'))
573 + }
574 + })
575 + })
576 + )
577 + }
578 +
579 + pull(
580 + createStream({
581 + limit: 1000,
582 + reverse: true,
583 + live: false,
584 + query: [{$filter: { value: { timestamp: { $gt: 0 }}}}]
585 + }),
586 + stream.bottom(content)
587 + )
588 +
589 +}
590 +
591 +function everythingStream () {
592 +
593 + var screen = document.getElementById('screen')
594 + var content = h('div.content')
595 +
596 + screen.appendChild(hyperscroll(content))
597 +
598 + function createStream (opts) {
599 + return pull(
600 + Next(sbot.query, opts, ['value', 'timestamp']),
601 + pull.map(function (msg) {
602 + if (msg.value) {
603 + if (msg.value.timestamp > Date.now()) {
604 + return h('div.future')
605 + } else {
606 + return render(msg)
607 + }
608 + }
609 + })
610 + )
611 + }
612 +
613 + pull(
614 + createStream({
615 + limit: 10,
616 + reverse: true,
617 + live: false,
618 + query: [{$filter: { value: { timestamp: { $gt: 0 }}}}]
619 + }),
620 + stream.bottom(content)
621 + )
622 +
623 + pull(
624 + createStream({
625 + limit: 10,
626 + old: false,
627 + live: true,
628 + query: [{$filter: { value: { timestamp: { $gt: 0 }}}}]
629 + }),
630 + stream.top(content)
631 + )
632 +}
633 +
634 +
635 +
636 +
637 +function backchannel () {
638 +
639 + var screen = document.getElementById('screen')
640 + var content = h('div.content')
641 +
642 + screen.appendChild(hyperscroll(content))
643 +
644 + var chatbox = h('input', {placeholder: 'Backchannel'})
645 +
646 + var chat = h('div.content')
647 +
648 + var publish = h('button.btn', 'Publish', {
649 + onclick: function () {
650 + if (chatbox.value) {
651 + var content = {
652 + text: chatbox.value,
653 + type: 'scat_message'
654 + }
655 + sbot.publish(content, function (err, msg) {
656 + if (err) throw err
657 + chatbox.value = ''
658 + console.log('Published!', msg)
659 + })
660 + }
661 + }
662 + })
663 +
664 + chat.appendChild(h('div.message', chatbox, publish))
665 +
666 + if (screen.firstChild.firstChild) {
667 + screen.firstChild.insertBefore(chat, screen.firstChild.firstChild)
668 + } else {
669 + screen.firstChild.appendChild(chat)
670 + }
671 +
672 + function createStream (opts) {
673 + return pull(
674 + Next(sbot.query, opts, ['value', 'timestamp']),
675 + pull.map(function (msg) {
676 + if (msg.value) {
677 + return render(msg)
678 + }
679 + })
680 + )
681 + }
682 +
683 + pull(
684 + createStream({
685 + limit: 10,
686 + reverse: true,
687 + live: false,
688 + query: [{$filter: { value: { content: {type: 'scat_message'}, timestamp: { $gt: 0 }}}}]
689 + }),
690 + stream.bottom(content)
691 + )
692 +
693 + pull(
694 + createStream({
695 + limit: 10,
696 + old: false,
697 + live: true,
698 + query: [{$filter: { value: { content: {type: 'scat_message'}, timestamp: { $gt: 0 }}}}]
699 + }),
700 + stream.top(content)
701 + )
702 +}
703 +
704 +function search (src) {
705 + console.log('search' + src)
706 +
707 + var content = h('div.content')
708 + var screen = document.getElementById('screen')
709 + screen.appendChild(hyperscroll(content))
710 +
711 + pull(
712 + sbot.search.query({query: src, limit: 100}),
713 + pull.drain(function (search) {
714 + content.appendChild(render(search))
715 + })
716 + )
717 +
718 +}
719 +
720 +function hash () {
721 + return window.location.hash.substring(1)
722 +}
723 +
724 +module.exports = function () {
725 + var src = hash()
726 +
727 + if (src.substring(52, 59) == '?unbox=') {
728 + privateMsg(src)
729 + } else if (ref.isFeed(src)) {
730 + userStream(src)
731 + } else if (ref.isMsg(src)) {
732 + msgThread(src)
733 + } else if (ref.isFeed(src.substring(5))) {
734 + mentionsStream(src.substring(5))
735 + } else if (ref.isFeed(src.substring(8))) {
736 + friendsStream(src.substring(8))
737 + } else if (src.substring(0, 6) === 'label/') {
738 + labelStream(src.substring(6))
739 + } else if (src == 'queue') {
740 + queueStream()
741 + } else if (src == 'backchannel') {
742 + backchannel()
743 + } else if (src == 'private') {
744 + privateStream()
745 + } else if (src == 'key') {
746 + keyPage()
747 + } else if (src[0] == '?' || (src[0] == '#')) {
748 + if (src[0] == '#')
749 + search(src.split('%20').join(' '))
750 + else
751 + search(src.substr(1).split('%20').join(' '))
752 + } else {
753 + everythingStream()
754 + }
755 +
756 +}
ui/avatar.jsView
@@ -1,92 +1,0 @@
1-var pull = require('pull-stream')
2-var query = require('./scuttlebot').query
3-var h = require('hyperscript')
4-var visualize = require('visualize-buffer')
5-
6-var avatar = require('ssb-avatar')
7-
8-var sbot = require('./scuttlebot')
9-
10-var config = require('./config')()
11-
12-var id = require('./keys').id
13-
14-var ref = require('ssb-ref')
15-
16-module.exports.name = function (key) {
17-
18- var avatarname = h('span', key.substring(0, 10))
19- if (ref.isFeedId(key)) {
20- avatar(sbot, id, key, function (err, data) {
21- if (err) throw err
22- if (data.name) {
23- if (data.name[0] != '@') {
24- var name = '@' + data.name
25- } else {
26- var name = data.name
27- }
28- localStorage[key + 'name'] = name
29- avatarname.textContent = name
30- }
31- })
32- }
33- return avatarname
34-}
35-
36-module.exports.image = function (key) {
37- var img = visualize(new Buffer(key.substring(1), 'base64'), 256)
38-
39- if (ref.isFeedId(key)) {
40- avatar(sbot, id, key, function (err, data) {
41- if (err) throw err
42- if (data.image) {
43- localStorage[key + 'image'] = data.image
44- img.src = config.blobsUrl + data.image
45- }
46- })
47- }
48- return img
49-}
50-
51-module.exports.cachedName = function (key) {
52- var avatarname = h('span', key.substring(0, 10))
53-
54- if (localStorage[key + 'name']) {
55- avatarname.textContent = localStorage[key + 'name']
56- } else {
57- if (ref.isFeedId(key)) {
58- avatar(sbot, id, key, function (err, data) {
59- if (data.name) {
60- if (data.name[0] != '@') {
61- var name = '@' + data.name
62- } else {
63- var name = data.name
64- }
65- localStorage[key + 'name'] = name
66- avatarname.textContent = name
67- }
68- })
69- }
70- }
71-
72- return avatarname
73-}
74-
75-module.exports.cachedImage = function (key) {
76- var img = visualize(new Buffer(key.substring(1), 'base64'), 256)
77-
78- if (localStorage[key + 'image']) {
79- img.src = config.blobsUrl + localStorage[key + 'image']
80- } else {
81- if (ref.isFeedId(key)) {
82- avatar(sbot, id, key, function (err, data) {
83- if (data.image) {
84- localStorage[key + 'image'] = data.image
85- img.src = config.blobsUrl + data.image
86- }
87- })
88- }
89- }
90-
91- return img
92-}
ui/compose.jsView
@@ -1,187 +1,0 @@
1-var h = require('hyperscript')
2-var pull = require('pull-stream')
3-var sbot = require('./scuttlebot')
4-var human = require('human-time')
5-var id = require('./keys').id
6-var mentions = require('ssb-mentions')
7-
8-var avatar = require('./avatar')
9-var tools = require('./tools')
10-
11-var mime = require('simple-mime')('application/octect-stream')
12-var split = require('split-buffer')
13-
14-var route = require('./views')
15-
16-function file_input (onAdded) {
17- return h('label.btn', 'Upload file',
18- h('input', { type: 'file', hidden: true,
19- onchange: function (ev) {
20- var file = ev.target.files[0]
21- if (!file) return
22- var reader = new FileReader()
23- reader.onload = function () {
24- pull(
25- pull.values(split(new Buffer(reader.result), 64*1024)),
26- sbot.addblob(function (err, blob) {
27- if(err) return console.error(err)
28- onAdded({
29- link: blob,
30- name: file.name,
31- size: reader.result.length || reader.result.byteLength,
32- type: mime(file.name)
33- })
34- })
35- )
36- }
37- reader.readAsArrayBuffer(file)
38- }
39- }))
40-}
41-
42-module.exports = function (opts, fallback) {
43- var files = []
44- var filesById = {}
45-
46- var composer = h('div.composer')
47- var container = h('div.container')
48- if (opts.boostAuthor) {
49- var boostName = avatar.cachedName(opts.boostAuthor)
50- }
51- if (opts.boostContent) {
52- var textarea = h('textarea.compose')
53- var str = opts.boostContent
54- var lines = str.split("\n")
55- for(var i=0; i<lines.length; i++) {
56- lines[i] = "> " + lines[i]
57- }
58- var newContent = lines.join("\n")
59- var content = 'Boosting: ' + opts.boostKey + '\n\n' + newContent + ' - [' + boostName.textContent + ']('+ opts.boostAuthor + ')'
60- textarea.value = content
61- }
62-
63- else if (opts.mentions) {
64- var textarea = h('textarea.compose', opts.mentions)
65- }
66-
67- else if (opts.type == 'wiki')
68- var textarea = h('textarea.compose', {placeholder: opts.placeholder || 'Write a wiki (anyone can edit)'})
69- else if (opts.type == 'post')
70- var textarea = h('textarea.compose', {placeholder: opts.placeholder || 'Write a message (only you can edit)'})
71- else
72- var textarea = h('textarea.compose', {placeholder: opts.placeholder || 'Write a message (only you can edit)'}, fallback.messageText)
73-
74- var cancelBtn = h('button.btn', 'Cancel', {
75- onclick: function () {
76- var cancel
77- console.log(opts)
78-
79- if (opts.type == 'edit') {
80- cancel = document.getElementById('edit:' + opts.branch.substring(0,44))
81- var oldMessage = h('div.message__body', tools.markdown(fallback.messageText))
82- cancel.parentNode.replaceChild(oldMessage, cancel)
83- oldMessage.parentNode.appendChild(fallback.buttons)
84- } else if (opts.branch) {
85- //cancel reply composer
86- cancel = document.getElementById('re:' + opts.branch.substring(0,44))
87- cancel.parentNode.removeChild(cancel)
88- message = document.getElementById(opts.branch.substring(0,44))
89- message.appendChild(fallback.buttons)
90- } else {
91- // cancel generic composer
92- cancel = document.getElementById('composer')
93- cancel.parentNode.removeChild(cancel)
94- }
95- }
96-
97- })
98-
99- var initialButtons = h('span',
100- h('button.btn', 'Preview', {
101- onclick: function () {
102- if (textarea.value) {
103- var msg = {}
104-
105- msg.value = {
106- "author": id,
107- "content": opts
108- }
109-
110- msg.value.content.text = textarea.value
111- msg.value.content.mentions = mentions(textarea.value).map(
112- function (mention) {
113- var file = filesById[mention.link]
114- if (file) {
115- if (file.type) mention.type = file.type
116- if (file.size) mention.size = file.size
117- }
118- return mention
119- }
120- )
121-
122- if (opts.recps)
123- msg.value.private = true
124-
125- console.log(msg)
126- if (opts.type == 'post' || opts.type == 'wiki')
127- var header = tools.header(msg)
128- if (opts.type == 'update')
129- var header = tools.timestamp(msg, {edited: true})
130- var preview = h('div',
131- header,
132- h('div.message__content', tools.markdown(msg.value.content.text)),
133- h('button.btn', 'Publish', {
134- onclick: function () {
135- if (msg.value.content) {
136- sbot.publish(msg.value.content, function (err, msg) {
137- if(err) throw err
138- console.log('Published!', msg)
139- if (opts.type == 'edit') {
140- var message = document.getElementById(opts.branch.substring(0,44))
141- fallback.messageText = msg.value.content.text
142- var editBody = h('div.message__body',
143- tools.timestamp(msg, {edited: true}),
144- h('div', tools.markdown(msg.value.content.text))
145- )
146-
147- message.replaceChild(editBody, message.childNodes[message.childNodes.length - 1])
148- editBody.parentNode.appendChild(fallback.buttons)
149- } else {
150- if (opts.branch)
151- cancel = document.getElementById('re:' + opts.branch.substring(0,44))
152- else
153- cancel = document.getElementById('composer')
154- cancel.parentNode.removeChild(cancel)
155- }
156- })
157- }
158- }
159- }),
160- h('button.btn', 'Cancel', {
161- onclick: function () {
162- composer.replaceChild(container, composer.firstChild)
163- container.appendChild(textarea)
164- container.appendChild(initialButtons)
165- }
166- })
167- )
168- composer.replaceChild(preview, composer.firstChild)
169- }
170- }
171- }),
172- file_input(function (file) {
173- files.push(file)
174- filesById[file.link] = file
175- var embed = file.type.indexOf('image/') === 0 ? '!' : ''
176- textarea.value += embed + '['+file.name+']('+file.link+')'
177- }),
178- cancelBtn
179- )
180-
181- composer.appendChild(container)
182- container.appendChild(textarea)
183- container.appendChild(initialButtons)
184-
185- return composer
186-}
187-
ui/index.jsView
@@ -1,81 +1,0 @@
1-var h = require('hyperscript')
2-var route = require('./views')
3-var avatar = require('./avatar')
4-
5-var compose = require('./compose')
6-
7-var id = require('./keys').id
8-
9-document.head.appendChild(h('style', require('./style.css.json')))
10-
11-var screen = h('div#screen')
12-
13-var search = h('input.search', {placeholder: 'Search'})
14-
15-var nav = h('div.navbar',
16- h('div.internal',
17- h('li', h('a', {href: '#' + id}, h('span.avatar--small', avatar.image(id)))),
18- h('li', h('a', {href: '#' + id}, avatar.name(id))),
19- h('li', h('a', 'New Post', {
20- onclick: function () {
21- if (document.getElementById('composer')) { return }
22- else {
23- var currentScreen = document.getElementById('screen')
24- var opts = {}
25- opts.type = 'post'
26- var composer = h('div.content#composer', h('div.message', compose(opts)))
27- if (currentScreen.firstChild.firstChild) {
28- currentScreen.firstChild.insertBefore(composer, currentScreen.firstChild.firstChild)
29- } else {
30- currentScreen.firstChild.appendChild(composer)
31- }
32- }
33- }
34- })),
35- h('li', h('a', 'New Wiki', {
36- onclick: function () {
37- if (document.getElementById('composer')) { return }
38- else {
39- var currentScreen = document.getElementById('screen')
40- var opts = {}
41- opts.type = 'wiki'
42- var composer = h('div.content#composer', h('div.message', compose(opts)))
43- if (currentScreen.firstChild.firstChild) {
44- currentScreen.firstChild.insertBefore(composer, currentScreen.firstChild.firstChild)
45- } else {
46- currentScreen.firstChild.appendChild(composer)
47- }
48- }
49- }
50- })),
51- h('li', h('a', {href: '#' }, 'All')),
52- h('li', h('a', {href: '#private' }, 'Private')),
53- h('li', h('a', {href: '#friends/' + id }, 'Friends')),
54- h('li', h('a', {href: '#wall/' + id }, 'Wall')),
55- h('li', h('a', {href: '#queue'}, 'Queue')),
56- h('li', h('a', {href: '#key' }, 'Key')),
57- h('li.right', h('a', {href: 'http://gitmx.com/#%NPNNvcnTMZUFZSWl/2Z4XX+YSdqsqOhyPacp+lgpQUw=.sha256'}, '?')),
58- h('form.search', {
59- onsubmit: function (e) {
60- if (search.value[0] == '#')
61- window.location.hash = '#' + search.value
62- else
63- window.location.hash = '?' + search.value
64- e.preventDefault()
65- }},
66- search
67- )
68- )
69-)
70-
71-document.body.appendChild(nav)
72-document.body.appendChild(screen)
73-route()
74-
75-window.onhashchange = function () {
76- var oldscreen = document.getElementById('screen')
77- var newscreen = h('div#screen')
78- oldscreen.parentNode.replaceChild(newscreen, oldscreen)
79- route()
80-}
81-
ui/keys.jsView
@@ -1,6 +1,0 @@
1-
2-var config = require('./config')()
3-var ssbKeys = require('ssb-keys')
4-var path = require('path')
5-
6-module.exports = ssbKeys.loadOrCreateSync(path.join(config.caps.shs + '/secret'))
ui/mvd-indexes.jsView
@@ -1,20 +1,0 @@
1-var Indexes = require('flumeview-query/indexes')
2-var pkg = require('./package.json')
3-exports.name = 'mvd-indexes'
4-exports.version = pkg.version
5-exports.manifest = {}
6-
7-exports.init = function (sbot, config) {
8-
9- var view =
10- sbot._flumeUse('query/mvd', Indexes(1, {
11- indexes: [
12- {key: 'chr', value: [['value', 'timestamp' ]]}
13- ]
14- }))
15-
16- var indexes = view.indexes()
17- sbot.query.add(indexes[0])
18-
19- return {}
20-}
ui/render.jsView
@@ -1,430 +1,0 @@
1-var h = require('hyperscript')
2-var pull = require('pull-stream')
3-var human = require('human-time')
4-
5-var sbot = require('./scuttlebot')
6-var composer = require('./compose')
7-var tools = require('./tools')
8-
9-var config = require('./config')()
10-var id = require('./keys').id
11-var avatar = require('./avatar')
12-var ssbAvatar = require('ssb-avatar')
13-
14-var ssbKeys = require('ssb-keys')
15-var keys = require('./keys')
16-
17-var diff = require('diff')
18-
19-function hash () {
20- return window.location.hash.substring(1)
21-}
22-
23-module.exports = function (msg) {
24- var message = h('div.message#' + msg.key.substring(0, 44))
25-
26- if (!localStorage[msg.value.author])
27- var cache = {mute: false}
28- else
29- var cache = JSON.parse(localStorage[msg.value.author])
30-
31- if (cache.mute == true) {
32- var muted = h('span', ' muted')
33- message.appendChild(tools.mini(msg, muted))
34- return message
35- }
36-
37- else if (msg.value.content.type == 'about') {
38- if (msg.value.content.image) {
39- var image = h('span.avatar--small',
40- ' identified ',
41- h('a', {href: '#' + msg.value.content.about}, avatar.cachedName(msg.value.content.about)),
42- ' as ',
43- h('img', {src: config.blobsUrl + msg.value.content.image.link})
44- )
45- message.appendChild(tools.mini(msg, image))
46- }
47- if (msg.value.content.name) {
48- var name = h('span',
49- ' identified ',
50- h('a', {href: '#' + msg.value.content.about}, avatar.cachedName(msg.value.content.about)),
51- ' as ', msg.value.content.name
52- )
53- message.appendChild(tools.mini(msg, name))
54- }
55-
56- return message
57- }
58-
59- else if (msg.value.content.type == 'label'){
60- var content = h('span', ' labeled ', tools.messageLink(msg.value.content.link), ' as ', h('mark', h('a', {href: '#label/' + msg.value.content.label}, msg.value.content.label)))
61- message.appendChild(tools.mini(msg, content))
62- return message
63- }
64-
65- else if (msg.value.content.type == 'queue') {
66- if (msg.value.content.queue == true) {
67- var content = h('span', ' added ', tools.messageLink(msg.value.content.message), ' to their ', h('a', {href: '#queue'}, 'queue'))
68- message.appendChild(tools.mini(msg, content))
69- }
70- if (msg.value.content.queue == false) {
71- var content = h('span', ' removed ', tools.messageLink(msg.value.content.message), ' from their ', h('a', {href: '#queue'}, 'queue'))
72- message.appendChild(tools.mini(msg, content))
73-
74- }
75- return message
76- }
77-
78- else if (msg.value.content.type == 'edit') {
79- message.appendChild(tools.header(msg))
80- if (msg.value.content.text) {
81- var current = msg.value.content.text
82- sbot.get(msg.value.content.updated, function (err, updated) {
83- if (updated) {
84- // quick fix, need to decrypt messages if they're private
85- if (updated.content.text) {
86- fragment = document.createDocumentFragment()
87- var previous = updated.content.text
88- var ready = diff.diffWords(previous, current)
89- ready.forEach(function (part) {
90- if (part.added === true) {
91- color = 'cyan'
92- } else if (part.removed === true) {
93- color = 'gray'
94- } else {color = '#333'}
95- var span = h('span')
96- span.style.color = color
97- if (part.removed === true) {
98- span.appendChild(h('del', document.createTextNode(part.value)))
99- } else {
100- span.appendChild(document.createTextNode(part.value))
101- }
102- fragment.appendChild(span)
103- })
104- message.appendChild(h('code', fragment))
105- }
106- }
107- })
108- }
109- return message
110- }
111-
112- else if (msg.value.content.type == 'scat_message') {
113- var src = hash()
114- if (src != 'backchannel') {
115- message.appendChild(h('button.btn.right', h('a', {href: '#backchannel'}, 'Chat')))
116- }
117- message.appendChild(tools.mini(msg, ' ' + msg.value.content.text))
118- return message
119- }
120- else if (msg.value.content.type == 'contact') {
121- if (msg.value.content.contact) {
122- var contact = h('a', {href: '#' + msg.value.content.contact}, avatar.name(msg.value.content.contact))
123- } else { var contact = h('p', 'no contact named')}
124-
125- if (msg.value.content.following == true) {
126- var following = h('span', ' follows ', contact)
127- message.appendChild(tools.mini(msg, following))
128- }
129- if (msg.value.content.following == false) {
130- var unfollowing = h('span', ' unfollows ', contact)
131- message.appendChild(tools.mini(msg, unfollowing))
132- }
133- if (msg.value.content.blocking == true) {
134- var blocking = h('span', ' blocks ', contact)
135- message.appendChild(tools.mini(msg, blocking))
136- }
137- if (msg.value.content.blocking == false) {
138- var unblocking = h('span', ' unblocks ', contact)
139- message.appendChild(tools.mini(msg, unblocking))
140- }
141- return message
142-
143- }
144-
145- else if (msg.value.content.type == 'git-update') {
146-
147- message.appendChild(tools.header(msg))
148-
149- var reponame = h('p', 'pushed to ', h('a', {href: '#' + msg.value.content.repo}, msg.value.content.repo))
150-
151- var cloneurl = h('pre', 'git clone ssb://' + msg.value.content.repo)
152-
153- message.appendChild(reponame)
154-
155-
156- ssbAvatar(sbot, id, msg.value.content.repo, function (err, data) {
157- if (data) {
158- var actualname = h('p', 'pushed to ', h('a', {href: '#' + msg.value.content.repo}, '%' + data.name))
159- reponame.parentNode.replaceChild(actualname, reponame)
160- }
161- })
162-
163- message.appendChild(cloneurl)
164-
165- var commits = h('ul')
166- //if (msg.value.content.commits[0]) {
167- if (msg.value.content.commits) {
168- msg.value.content.commits.map(function (commit) {
169- commits.appendChild(h('li', h('code', commit.sha1), ' - ', commit.title))
170- })
171-
172- }
173-
174- message.appendChild(commits)
175-
176- return message
177-
178- }
179- else if (msg.value.content.type == 'git-repo') {
180- message.appendChild(tools.header(msg))
181-
182- var reponame = h('p', 'git-ssb repo ', h('a', {href: '#' + msg.key}, msg.key))
183-
184- message.appendChild(reponame)
185-
186- ssbAvatar(sbot, id, msg.key, function (err, data) {
187- if (data)
188- var actualname = h('p', 'git-ssb repo ', h('a', {href: '#' + msg.key}, '%' + data.name))
189- reponame.parentNode.replaceChild(actualname, reponame)
190- })
191-
192- var cloneurl = h('pre', 'git clone ssb://' + msg.key)
193- message.appendChild(cloneurl)
194- return message
195- }
196-
197- else if (msg.value.content.type == 'wiki') {
198- var fallback = {}
199-
200- var opts = {
201- type: 'wiki',
202- branch: msg.key
203- }
204-
205- if (msg.value.content.root)
206- opts.root = msg.value.content.root
207- else
208- opts.root = msg.key
209-
210- message.appendChild(tools.header(msg))
211-
212- message.appendChild(h('div.message__body', tools.markdown(msg.value.content.text)))
213-
214- pull(
215- sbot.query({query: [{$filter: {value: {content: {type: 'edit', original: msg.key}}}}], limit: 100}),
216- pull.drain(function (update) {
217- if (update.sync) {
218- } else {
219- var newMessage = h('div', tools.markdown(update.value.content.text))
220- var latest = h('div.message__body',
221- tools.timestamp(update, {edited: true}),
222- newMessage
223- )
224- message.replaceChild(latest, message.childNodes[message.childNodes.length - 2])
225- fallback.messageText = update.value.content.text
226- opts.updated = update.key
227- opts.original = msg.key
228- }
229- })
230- )
231-
232- var buttons = h('div.buttons')
233-
234- buttons.appendChild(h('button.btn', 'Edit wiki', {
235- onclick: function () {
236- opts.type = 'edit'
237- if (!fallback.messageText)
238- fallback.messageText = msg.value.content.text
239-
240- if (!opts.updated)
241- opts.updated = msg.key
242- opts.original = msg.key
243-
244- var r = message.childNodes.length - 1
245- fallback.buttons = message.childNodes[r]
246- message.removeChild(message.childNodes[r])
247- var compose = h('div#edit:' + msg.key.substring(0, 44), composer(opts, fallback))
248- message.replaceChild(compose, message.lastElementChild)
249- }
250- }))
251-
252- buttons.appendChild(tools.star(msg))
253- message.appendChild(buttons)
254- return message
255-
256- } else if (msg.value.content.type == 'post') {
257- var opts = {
258- type: 'post',
259- branch: msg.key
260- }
261- var fallback = {}
262-
263-
264- if (msg.value.content.root)
265- opts.root = msg.value.content.root
266- else
267- opts.root = msg.key
268-
269- message.appendChild(tools.header(msg))
270-
271- if (msg.value.content.root)
272- message.appendChild(h('span', 're: ', tools.messageLink(msg.value.content.root)))
273-
274- message.appendChild(h('div.message__body', tools.markdown(msg.value.content.text)))
275-
276- pull(
277- sbot.query({query: [{$filter: {value: {content: {type: 'edit', original: msg.key}}}}], limit: 100}),
278- pull.drain(function (update) {
279- if (update.sync) {
280- } else {
281- var newMessage = h('div', tools.markdown(update.value.content.text))
282- var latest = h('div.message__body',
283- tools.timestamp(update, {edited: true}),
284- newMessage
285- )
286- message.replaceChild(latest, message.childNodes[message.childNodes.length - 2])
287- fallback.messageText = update.value.content.text
288- opts.updated = update.key
289- opts.original = msg.key
290- }
291- })
292- )
293-
294- pull(
295- sbot.query({query: [{$filter: {value: { content: {type: 'label', link: msg.key}}}}], limit: 100, live: true}),
296- pull.drain(function (labels){
297- console.log(labels)
298- if (labels.value){
299- message.appendChild(h('span', ' ', h('mark', h('a', {href: '#label/' + labels.value.content.label}, labels.value.content.label))))
300-
301- }
302- })
303- )
304-
305- var name = avatar.name(msg.value.author)
306-
307- var buttons = h('div.buttons')
308-
309- buttons.appendChild(h('button.btn', 'Reply', {
310- onclick: function () {
311- opts.type = 'post'
312- opts.mentions = '[' + name.textContent + '](' + msg.value.author + ')'
313- if (msg.value.content.recps) {
314- opts.recps = msg.value.content.recps
315- }
316- var r = message.childNodes.length - 1
317- delete opts.updated
318- delete opts.original
319- delete fallback.messageText
320- fallback.buttons = message.childNodes[r]
321- var compose = h('div.message#re:' + msg.key.substring(0, 44), composer(opts, fallback))
322- message.removeChild(message.childNodes[r])
323- message.parentNode.insertBefore(compose, message.nextSibling)
324- }
325- }))
326-
327- buttons.appendChild(h('button.btn', 'Boost', {
328- onclick: function () {
329- opts.type = 'post'
330- opts.mentions = '[' + name.textContent + '](' + msg.value.author + ')'
331- if (msg.value.content.recps) {
332- opts.recps = msg.value.content.recps
333- }
334- var r = message.childNodes.length - 1
335- delete opts.updated
336- delete opts.original
337- delete fallback.messageText
338- opts.boostContent = msg.value.content.text
339- opts.boostKey = msg.key
340- opts.boostAuthor = msg.value.author
341- fallback.buttons = message.childNodes[r]
342- var compose = h('div.message#re:' + msg.key.substring(0, 44), composer(opts, fallback))
343- message.removeChild(message.childNodes[r])
344- message.parentNode.insertBefore(compose, message.nextSibling)
345- }
346- }))
347-
348-
349- if (msg.value.author == id)
350- buttons.appendChild(h('button.btn', 'Edit', {
351- onclick: function () {
352- opts.type = 'edit'
353- if (!fallback.messageText)
354- fallback.messageText = msg.value.content.text
355-
356- if (!opts.updated)
357- opts.updated = msg.key
358- opts.original = msg.key
359-
360- var r = message.childNodes.length - 1
361- fallback.buttons = message.childNodes[r]
362- message.removeChild(message.childNodes[r])
363- var compose = h('div#edit:' + msg.key.substring(0, 44), composer(opts, fallback))
364- message.replaceChild(compose, message.lastElementChild)
365- }
366- }))
367-
368-
369- var inputter = h('input', {placeholder: 'Add a label to this post ie art, books, new'})
370-
371- var labeler = h('div',
372- inputter,
373- h('button.btn', 'Publish label', {
374- onclick: function () {
375- var post = {}
376- post.type = 'label',
377- post.label = inputter.value,
378- post.link = msg.key
379-
380- sbot.publish(post, function (err, msg){
381- console.log(msg)
382- labeler.parentNode.replaceChild(buttons, labeler)
383- })
384- }
385- })
386- )
387-
388- var labels = h('button.btn', 'Add label', {
389- onclick: function () {
390- buttons.parentNode.replaceChild(labeler, buttons)
391- }
392- })
393-
394- buttons.appendChild(labels)
395- buttons.appendChild(tools.queueButton(msg))
396- buttons.appendChild(tools.star(msg))
397- message.appendChild(buttons)
398- return message
399-
400- } else if (msg.value.content.type == 'vote') {
401- if (msg.value.content.vote.value == 1)
402- var link = h('span', ' ', h('img.emoji', {src: config.emojiUrl + 'star.png'}), ' ', h('a', {href: '#' + msg.value.content.vote.link}, tools.messageLink(msg.value.content.vote.link)))
403- else if (msg.value.content.vote.value == -1)
404- var link = h('span', ' ', h('img.emoji', {src: config.emojiUrl + 'stars.png'}), ' ', h('a', {href: '#' + msg.value.content.vote.link}, tools.messageLink(msg.value.content.vote.link)))
405- message.appendChild(tools.mini(msg, link))
406- return message
407- } else if (typeof msg.value.content === 'string') {
408- var unboxed = ssbKeys.unbox(msg.value.content, keys)
409- if (unboxed) {
410- msg.value.content = unboxed
411- msg.value.private = true
412- return module.exports(msg)
413- } else {
414- var privateMsg = h('span', ' sent a private message.')
415- message.appendChild(tools.mini(msg, privateMsg))
416- return message
417- //return h('div')
418- }
419- } else {
420-
421- //FULL FALLBACK
422- message.appendChild(tools.header(msg))
423- message.appendChild(h('pre', tools.rawJSON(msg.value)))
424-
425- //MINI FALLBACK
426- //var fallback = h('span', ' ' + msg.value.content.type)
427- //message.appendChild(tools.mini(msg, fallback))
428- return h('div', message)
429- }
430-}
ui/style.cssView
@@ -1,302 +1,0 @@
1-body {
2- margin: 0;
3- background: black;
4- font-family: sans-serif;
5- color: #f5f5f5;
6- font-size: 14px;
7- line-height: 20px;
8-}
9-
10-#screen {
11- position: absolute;
12- top: 35px;
13- bottom: 0px;
14- left: 0px;
15- right: 0px;
16-}
17-
18-.hyperscroll {
19- width: 100%;
20-}
21-
22-.search {
23- margin-top: 1.5px;
24- float: right;
25- width: 200px;
26-}
27-
28-.header {
29- padding-bottom: .7em;
30- border-bottom: 1px solid #252525;
31-}
32-
33-mark p, mark a {
34- color: black;
35-}
36-
37-h1, h2, h3, h4, h5, h6 {
38- font-size: 1.2em;
39- margin-top: .35ex;
40-}
41-
42-hr {
43- border: solid #222;
44- clear: both;
45- border-width: 1px 0 0;
46- height: 0;
47- margin-bottom: .9em;
48-}
49-
50-
51-p {
52- margin-top: .35ex;
53- margin-bottom: 10px;
54-}
55-
56-a {
57- color: cyan;
58- text-decoration: none;
59-}
60-
61-a:hover, a:focus {
62- color: violet;
63- text-decoration: underline;
64-}
65-
66-.breadcrumbs {
67- color: #363636;
68- background: #f5f5f5;
69-}
70-
71-/*.navbar a {
72- color: #999;
73- text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
74- text-decoration: none;
75-}
76-
77-.navbar a:hover, .navbar a:focus {
78- color: #fff;
79- text-decoration: none;
80-}*/
81-
82-.navbar {
83- background: #1b1b1b;
84- background: linear-gradient(#222, #111);
85- border-bottom: 1px solid #252525;
86-}
87-
88-.navbar {
89- width: 100%;
90- position: fixed;
91- z-index: 1000;
92- margin: 0;
93- padding-top: .3em;
94- padding-bottom: .3em;
95- left: 0; right: 0;
96- top: 0;
97-}
98-
99-.navbar .internal {
100- max-width: 97%;
101- margin-left: auto;
102- margin-right: auto;
103-}
104-
105-.navbar li {
106- margin-top: .3em;
107- float: left;
108- margin-right: .6em;
109- margin-left: .3em;
110- list-style-type: none;
111-}
112-
113-.navbar li.right {
114- padding-left: .4em;
115- padding-right: .4em;
116- margin-top: .3em;
117- margin-right: 1.7em;
118- float: right;
119- list-style-type: none;
120- background: #333;
121- border-radius: 100%;
122-}
123-
124-.content {
125- max-width: 680px;
126- margin-left: auto;
127- margin-right: auto;
128-}
129-
130-.hyperscroll > .content {
131- max-width: 680px;
132- margin-left: auto;
133- margin-right: auto;
134-}
135-
136-.message, .message > *, .navbar, .navbar > * {
137- animation: fadein .5s;
138-}
139-
140-@keyframes fadein {
141- from { opacity: 0; }
142- to { opacity: 1; }
143-}
144-
145-.message {
146- display: block;
147- margin: .6em;
148- background: #111;
149- padding: .7em;
150- border-radius: 3px;
151- border: 1px solid #252525;
152-}
153-
154-.message:hover, .embedded:hover {
155- background: #141414;
156-}
157-
158-.message img, .message video {
159- max-width: 100%;
160-}
161-
162-img {
163- border-radius: 3px;
164-}
165-
166-.timestamp, .votes {
167- float: right;
168-}
169-
170-.avatar--small img {
171- vertical-align: top;
172- width: 1.4em;
173- height: 1.4em;
174- margin-right: .2em;
175-}
176-
177-.avatar--medium img {
178- float: left;
179- vertical-align: top;
180- width: 5em;
181- height: 5em;
182- margin-right: .5em;
183- margin-bottom: .5em;
184-}
185-
186-.compose, textarea, input {
187- font-family: sans-serif;
188- font-size: 14px;
189- line-height: 20px;
190- background: #111;
191- color: #ccc;
192- border: none;
193- border-radius: 3px;
194-}
195-
196-textarea {
197- width: 100%;
198- height: 200px;
199-}
200-
201-.compose:hover {
202- background: #141414;
203-}
204-
205-.compose:focus {
206- outline: none;
207-}
208-
209-.emoji {
210- padding: .2em;
211-}
212-
213-.right {
214- float: right;
215- margin-right: .25em;
216-}
217-
218-.emoji {
219- *float: left;
220- width: 1em;
221- vertical-align: top;
222-}
223-
224-pre {
225- width: 100%;
226- display: block;
227-}
228-
229-code {
230- display: inline-block;
231- vertical-align: bottom;
232-}
233-
234-code, pre {
235-overflow: auto;
236-word-break: break-all;
237-word-wrap: break-word;
238-white-space: pre;
239-white-space: -moz-pre-wrap;
240-white-space: pre-wrap;
241-white-space: pre\9;
242-}
243-
244-code, pre {
245- font-size: 12px;
246- color: #ccc;
247-}
248-
249-code {
250- color: #ccc;
251-}
252-
253-pre {
254- margin: 0 0 10px;
255- font-size: 13px;
256- line-height: 20px;
257-}
258-
259-button {margin: 0; margin-top: -.2em;}
260-
261-input {width: 88%; }
262-
263-#profile input {width: 50%;}
264-
265-.btn {
266- display: inline-block;
267- *display: inline;
268- padding: 2px 6px;
269- margin-bottom: 0;
270- margin-right: .2em;
271- font-size: 14px;
272- line-height: 20px;
273- color: #d5d5d5;
274- text-align: center;
275- text-shadow: 0 1px 1px rgba(0, 0, 0, 0.75);
276- vertical-align: middle;
277- cursor: pointer;
278- background-color: #222;
279- border: 1px solid #222;
280- border-radius: 4px;
281-}
282-
283-
284-.btn:hover,
285-.btn:focus,
286-.btn:active,
287-.btn.active,
288-.btn.disabled,
289-.btn[disabled] {
290- color: white;
291- background-color: black;
292-}
293-
294-.btn:active,
295-.btn.active {
296- background-color: #111;
297-}
298-
299-.btn:first-child {
300- *margin-left: 0;
301-}
302-
ui/style.css.jsonView
@@ -1,1 +1,0 @@
1-"body {\n margin: 0;\n background: black;\n font-family: sans-serif;\n color: #f5f5f5;\n font-size: 14px; \n line-height: 20px;\n}\n\n#screen {\n position: absolute;\n top: 35px;\n bottom: 0px;\n left: 0px;\n right: 0px;\n}\n\n.hyperscroll {\n width: 100%;\n}\n\n.search {\n margin-top: 1.5px;\n float: right;\n width: 200px;\n}\n\n.header {\n padding-bottom: .7em;\n border-bottom: 1px solid #252525;\n}\n\nmark p, mark a {\n color: black;\n}\n\nh1, h2, h3, h4, h5, h6 {\n font-size: 1.2em;\n margin-top: .35ex;\n}\n\nhr {\n border: solid #222;\n clear: both;\n border-width: 1px 0 0;\n height: 0;\n margin-bottom: .9em;\n}\n\n\np {\n margin-top: .35ex;\n margin-bottom: 10px;\n}\n\na {\n color: cyan;\n text-decoration: none;\n}\n\na:hover, a:focus {\n color: violet;\n text-decoration: underline; \n}\n\n.breadcrumbs {\n color: #363636;\n background: #f5f5f5;\n}\n\n/*.navbar a {\n color: #999;\n text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);\n text-decoration: none;\n}\n\n.navbar a:hover, .navbar a:focus {\n color: #fff;\n text-decoration: none;\n}*/\n\n.navbar {\n background: #1b1b1b;\n background: linear-gradient(#222, #111);\n border-bottom: 1px solid #252525;\n}\n\n.navbar {\n width: 100%;\n position: fixed;\n z-index: 1000;\n margin: 0;\n padding-top: .3em;\n padding-bottom: .3em;\n left: 0; right: 0;\n top: 0;\n}\n\n.navbar .internal {\n max-width: 97%;\n margin-left: auto;\n margin-right: auto;\n}\n\n.navbar li {\n margin-top: .3em;\n float: left;\n margin-right: .6em;\n margin-left: .3em;\n list-style-type: none;\n}\n\n.navbar li.right {\n padding-left: .4em;\n padding-right: .4em;\n margin-top: .3em;\n margin-right: 1.7em;\n float: right;\n list-style-type: none;\n background: #333;\n border-radius: 100%;\n}\n\n.content {\n max-width: 680px;\n margin-left: auto;\n margin-right: auto;\n}\n\n.hyperscroll > .content {\n max-width: 680px;\n margin-left: auto;\n margin-right: auto;\n}\n\n.message, .message > *, .navbar, .navbar > * {\n animation: fadein .5s;\n}\n\n@keyframes fadein {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n.message {\n display: block;\n margin: .6em;\n background: #111;\n padding: .7em;\n border-radius: 3px;\n border: 1px solid #252525;\n}\n\n.message:hover, .embedded:hover {\n background: #141414;\n}\n\n.message img, .message video {\n max-width: 100%;\n}\n\nimg {\n border-radius: 3px;\n}\n\n.timestamp, .votes {\n float: right;\n}\n \n.avatar--small img {\n vertical-align: top;\n width: 1.4em;\n height: 1.4em;\n margin-right: .2em;\n}\n\n.avatar--medium img {\n float: left;\n vertical-align: top;\n width: 5em;\n height: 5em;\n margin-right: .5em;\n margin-bottom: .5em;\n}\n\n.compose, textarea, input {\n font-family: sans-serif;\n font-size: 14px;\n line-height: 20px;\n background: #111;\n color: #ccc;\n border: none;\n border-radius: 3px;\n}\n\ntextarea {\n width: 100%;\n height: 200px;\n}\n\n.compose:hover {\n background: #141414;\n}\n\n.compose:focus {\n outline: none;\n}\n\n.emoji {\n padding: .2em;\n}\n\n.right {\n float: right;\n margin-right: .25em;\n}\n\n.emoji {\n *float: left;\n width: 1em;\n vertical-align: top;\n}\n\npre {\n width: 100%;\n display: block;\n}\n\ncode {\n display: inline-block;\n vertical-align: bottom;\n}\n\ncode, pre {\noverflow: auto;\nword-break: break-all;\nword-wrap: break-word;\nwhite-space: pre;\nwhite-space: -moz-pre-wrap;\nwhite-space: pre-wrap;\nwhite-space: pre\\9;\n}\n\ncode, pre {\n font-size: 12px;\n color: #ccc;\n}\n\ncode {\n color: #ccc;\n}\n\npre {\n margin: 0 0 10px;\n font-size: 13px;\n line-height: 20px;\n}\n\nbutton {margin: 0; margin-top: -.2em;}\n\ninput {width: 88%; }\n\n#profile input {width: 50%;}\n\n.btn {\n display: inline-block;\n *display: inline;\n padding: 2px 6px;\n margin-bottom: 0;\n margin-right: .2em;\n font-size: 14px;\n line-height: 20px;\n color: #d5d5d5;\n text-align: center;\n text-shadow: 0 1px 1px rgba(0, 0, 0, 0.75);\n vertical-align: middle;\n cursor: pointer;\n background-color: #222;\n border: 1px solid #222;\n border-radius: 4px;\n}\n\n\n.btn:hover,\n.btn:focus,\n.btn:active,\n.btn.active,\n.btn.disabled,\n.btn[disabled] {\n color: white;\n background-color: black;\n}\n\n.btn:active,\n.btn.active {\n background-color: #111;\n}\n\n.btn:first-child {\n *margin-left: 0;\n}\n\n"
ui/style.jsView
@@ -1,8 +1,0 @@
1-var fs = require('fs')
2-var path = require('path')
3-
4-fs.writeFileSync(
5- path.join(__dirname, 'style.css.json'),
6- JSON.stringify(fs.readFileSync(path.join(__dirname, 'style.css'), 'utf8'))
7-)
8-
ui/tools.jsView
@@ -1,596 +1,0 @@
1-var h = require('hyperscript')
2-var human = require('human-time')
3-var avatar = require('./avatar')
4-var ref = require('ssb-ref')
5-
6-var ssbKeys = require('ssb-keys')
7-
8-var pull = require('pull-stream')
9-
10-var sbot = require('./scuttlebot')
11-
12-var config = require('./config')()
13-
14-var id = require('./keys').id
15-
16-
17-module.exports.getBlocks = function (src) {
18- var blocks = h('div.blocks', 'Blocking: ')
19-
20- pull(
21- sbot.query({query: [{$filter: { value: { author: src, content: {type: 'contact'}}}}], live: true}),
22- pull.drain(function (msg) {
23- if (msg.value) {
24- if (msg.value.content.blocking == true) {
25- console.log(msg.value)
26- var gotIt = document.getElementById('blocks:' + msg.value.content.contact.substring(0, 44))
27- if (gotIt == null) {
28- blocks.appendChild(h('a#blocks:'+ msg.value.content.contact.substring(0, 44), {title: avatar.cachedName(msg.value.content.contact).textContent, href: '#' + msg.value.content.contact}, h('span.avatar--small', avatar.cachedImage(msg.value.content.contact))))
29- }
30- }
31- if (msg.value.content.blocking == false) {
32- var gotIt = document.getElementById('blocks:' + msg.value.content.contact.substring(0, 44))
33- if (gotIt != null) {
34- gotIt.outerHTML = ''
35- }
36- }
37- }
38- })
39- )
40-
41- return blocks
42-
43-}
44-
45-module.exports.getBlocked = function (src) {
46- var blocked = h('div.blocked', 'Blocked by: ')
47-
48- pull(
49- sbot.query({query: [{$filter: { value: { content: {type: 'contact', contact: src}}}}], live: true}),
50- pull.drain(function (msg) {
51- if (msg.value) {
52- if (msg.value.content.blocking == true) {
53- console.log(msg.value)
54- var gotIt = document.getElementById('blocked:' + msg.value.content.contact.substring(0, 44))
55- if (gotIt == null) {
56- blocked.appendChild(h('a#blocked:'+ msg.value.author.substring(0, 44), {title: avatar.cachedName(msg.value.author).textContent, href: '#' + msg.value.author}, h('span.avatar--small', avatar.cachedImage(msg.value.author))))
57- }
58- }
59- if (msg.value.content.blocking == false) {
60- var gotIt = document.getElementById('blocked:' + msg.value.author.substring(0, 44))
61- if (gotIt != null) {
62- gotIt.outerHTML = ''
63- }
64- }
65- }
66- })
67- )
68-
69- return blocked
70-
71-}
72-
73-module.exports.getFollowing = function (src) {
74- var followingCount = 0
75-
76- var following = h('div.following', 'Following: ')
77-
78- following.appendChild(h('span#followingcount', '0'))
79- following.appendChild(h('br'))
80-
81- pull(
82- sbot.query({query: [{$filter: { value: { author: src, content: {type: 'contact'}}}}], live: true}),
83- pull.drain(function (msg) {
84- if (msg.value) {
85- if (msg.value.content.following == true) {
86- followingcount = document.getElementById('followingcount')
87- followingCount++
88- followingcount.textContent = followingCount
89- var gotIt = document.getElementById('following:' + msg.value.content.contact.substring(0, 44))
90- if (gotIt == null) {
91- following.appendChild(h('a#following:'+ msg.value.content.contact.substring(0, 44), {title: avatar.cachedName(msg.value.content.contact).textContent, href: '#' + msg.value.content.contact}, h('span.avatar--small', avatar.cachedImage(msg.value.content.contact))))
92- }
93- }
94- if (msg.value.content.following == false) {
95- followingcount = document.getElementById('followingcount')
96- followingCount--
97- followingcount.textContent = followingCount
98- var gotIt = document.getElementById('following:' + msg.value.content.contact.substring(0, 44))
99- if (gotIt != null) {
100- gotIt.outerHTML = ''
101- }
102- }
103- }
104- })
105- )
106- return following
107-}
108-
109-module.exports.getFollowers = function (src) {
110- var followerCount = 0
111-
112- var followers = h('div.followers', 'Followers: ')
113-
114- followers.appendChild(h('span#followercount', '0'))
115- followers.appendChild(h('br'))
116-
117- pull(
118- sbot.query({query: [{$filter: { value: { content: {type: 'contact', contact: src}}}}], live: true}),
119- pull.drain(function (msg) {
120- if (msg.value) {
121- if (msg.value.content.following == true) {
122- followcount = document.getElementById('followercount')
123- followerCount++
124- followcount.textContent = followerCount
125- var gotIt = document.getElementById('followers:' + msg.value.author.substring(0, 44))
126- if (gotIt == null) {
127- followers.appendChild(h('a#followers:'+ msg.value.author.substring(0, 44), {title: avatar.cachedName(msg.value.author).textContent, href: '#' + msg.value.author}, h('span.avatar--small', avatar.cachedImage(msg.value.author))))
128- }
129- }
130- if (msg.value.content.following == false) {
131- followcount = document.getElementById('followercount')
132- followerCount--
133- followcount.textContent = followerCount
134- var gotIt = document.getElementById('followers:' + msg.value.author.substring(0, 44))
135- if (gotIt != null) {
136- gotIt.outerHTML = ''
137- }
138- }
139- }
140- })
141- )
142-
143- return followers
144-}
145-
146-module.exports.queueButton = function (src) {
147- var queueButton = h('span.queue:' + src.key.substring(0,44))
148-
149- var addToQueue = h('button.btn.right', 'Queue', {
150- onclick: function () {
151- var content = {
152- type: 'queue',
153- message: src.key,
154- queue: true
155- }
156- sbot.publish(content, function (err, publish) {
157- if (err) throw err
158- console.log(publish)
159- })
160- }
161- })
162-
163- var removeFromQueue = h('button.btn.right#', 'Done', {
164- onclick: function () {
165- var content = {
166- type: 'queue',
167- message: src.key,
168- queue: false
169- }
170- sbot.publish(content, function (err, publish) {
171- if (err) throw err
172- console.log(publish)
173- if (window.location.hash.substring(1) == 'queue') {
174- setTimeout(function () {
175- var gotIt = document.getElementById(src.key.substring(0,44))
176- if (gotIt != null) {
177- gotIt.outerHTML = ''
178- }
179- }, 100)
180-
181- }
182- })
183- }
184- })
185-
186- pull(
187- sbot.query({query: [{$filter: { value: { author: id, content: {type: 'queue', message: src.key}}}}], live: true}),
188- pull.drain(function (msg) {
189- if (msg.value) {
190- if (msg.value.content.queue == true) {
191- queueButton.removeChild(queueButton.childNodes[0])
192- queueButton.appendChild(removeFromQueue)
193- }
194- if (msg.value.content.queue == false) {
195- queueButton.removeChild(queueButton.childNodes[0])
196- queueButton.appendChild(addToQueue)
197- }
198- }
199- })
200- )
201-
202- queueButton.appendChild(addToQueue)
203-
204- return queueButton
205-}
206-module.exports.block = function (src) {
207- var button = h('span.button')
208-
209- var followButton = h('button.btn', 'Block (Private)', avatar.name(src), {
210- onclick: function () {
211- var content = {
212- type: 'contact',
213- contact: src,
214- blocking: true,
215- recps: id
216- }
217- sbot.publish(content, function (err, publish) {
218- if (err) throw err
219- console.log(publish)
220- })
221- }
222- })
223-
224- var unfollowButton = h('button.btn', 'Unblock (Private)', avatar.name(src), {
225- onclick: function () {
226- var content = {
227- type: 'contact',
228- contact: src,
229- blocking: false,
230- recps: id
231- }
232- sbot.publish(content, function (err, publish) {
233- if (err) throw err
234- console.log(publish)
235- })
236- }
237- })
238-
239- pull(
240- sbot.query({query: [{$filter: { value: { author: id, content: {type: 'contact', contact: src}}}}], live: true}),
241- pull.drain(function (msg) {
242- if (msg.value) {
243- if (msg.value.content.blocking == true) {
244- button.removeChild(button.firstChild)
245- button.appendChild(unfollowButton)
246- }
247- if (msg.value.content.blocking == false) {
248- button.removeChild(button.firstChild)
249- button.appendChild(followButton)
250- }
251- }
252- })
253- )
254-
255- button.appendChild(followButton)
256-
257- return button
258-}
259-
260-module.exports.box = function (content) {
261- return ssbKeys.box(content, content.recps.map(function (e) {
262- return ref.isFeed(e) ? e : e.link
263- }))
264-}
265-
266-module.exports.publish = function (content, cb) {
267- if(content.recps)
268- content = exports.box(content)
269- sbot.publish(content, function (err, msg) {
270- if(err) throw err
271- console.log('Published!', msg)
272- if(cb) cb(err, msg)
273- })
274-}
275-
276-
277-
278-module.exports.follow = function (src) {
279- var button = h('span.button')
280-
281- var followButton = h('button.btn', 'Follow ', avatar.name(src), {
282- onclick: function () {
283- var content = {
284- type: 'contact',
285- contact: src,
286- following: true
287- }
288- sbot.publish(content, function (err, publish) {
289- if (err) throw err
290- console.log(publish)
291- })
292- }
293- })
294-
295- var unfollowButton = h('button.btn', 'Unfollow ', avatar.name(src), {
296- onclick: function () {
297- var content = {
298- type: 'contact',
299- contact: src,
300- following: false
301- }
302- sbot.publish(content, function (err, publish) {
303- if (err) throw err
304- console.log(publish)
305- })
306- }
307- })
308-
309- pull(
310- sbot.query({query: [{$filter: { value: { author: id, content: {type: 'contact', contact: src}}}}], live: true}),
311- pull.drain(function (msg) {
312- if (msg.value) {
313- if (msg.value.content.following == true) {
314- button.removeChild(button.firstChild)
315- button.appendChild(unfollowButton)
316- }
317- if (msg.value.content.following == false) {
318- button.removeChild(button.firstChild)
319- button.appendChild(followButton)
320- }
321- }
322- })
323- )
324-
325- button.appendChild(followButton)
326-
327- return button
328-}
329-
330-module.exports.box = function (content) {
331- return ssbKeys.box(content, content.recps.map(function (e) {
332- return ref.isFeed(e) ? e : e.link
333- }))
334-}
335-
336-module.exports.publish = function (content, cb) {
337- if(content.recps)
338- content = exports.box(content)
339- sbot.publish(content, function (err, msg) {
340- if(err) throw err
341- console.log('Published!', msg)
342- if(cb) cb(err, msg)
343- })
344-}
345-
346-
347-
348-module.exports.mute = function (src) {
349- if (!localStorage[src])
350- var cache = {mute: false}
351- else
352- var cache = JSON.parse(localStorage[src])
353-
354- if (cache.mute == true) {
355- var mute = h('button.btn', 'Unmute', {
356- onclick: function () {
357- cache.mute = false
358- localStorage[src] = JSON.stringify(cache)
359- location.hash = '#'
360- location.hash = src
361- }
362- })
363- return mute
364- } else {
365- var mute = h('button.btn', 'Mute', {
366- onclick: function () {
367- cache.mute = true
368- localStorage[src] = JSON.stringify(cache)
369- location.hash = '#'
370- location.hash = src
371- }
372- })
373- return mute
374- }
375-}
376-
377-module.exports.star = function (msg) {
378- var votebutton = h('span.star:' + msg.key.substring(0,44))
379-
380- var vote = {
381- type: 'vote',
382- vote: { link: msg.key, expression: 'Star' }
383- }
384-
385- if (msg.value.content.recps) {
386- vote.recps = msg.value.content.recps
387- }
388-
389- var star = h('button.btn.right', 'Star ',
390- h('img.emoji', {src: config.emojiUrl + 'star.png'}), {
391- onclick: function () {
392- vote.vote.value = 1
393- if (vote.recps) {
394- vote = exports.box(vote)
395- }
396- sbot.publish(vote, function (err, voted) {
397- if(err) throw err
398- })
399- }
400- }
401- )
402-
403- var unstar = h('button.btn.right ', 'Unstar ',
404- h('img.emoji', {src: config.emojiUrl + 'stars.png'}), {
405- onclick: function () {
406- vote.vote.value = -1
407- sbot.publish(vote, function (err, voted) {
408- if(err) throw err
409- })
410- }
411- }
412- )
413-
414- votebutton.appendChild(star)
415-
416- pull(
417- sbot.links({rel: 'vote', dest: msg.key, live: true}),
418- pull.drain(function (link) {
419- if (link.key) {
420- sbot.get(link.key, function (err, data) {
421- if (err) throw err
422- if (data.content.vote) {
423- if (data.author == id) {
424- while (votebutton.firstChild) {
425- votebutton.removeChild(votebutton.firstChild)
426- }
427- if (data.content.vote.value == 1)
428- //votebutton.removeChild(votebutton.childNodes[0])
429- votebutton.appendChild(unstar)
430- if (data.content.vote.value == -1)
431- //votebutton.removeChild(votebutton.childNodes[0])
432- votebutton.appendChild(star)
433- }
434- }
435- })
436- }
437- })
438- )
439-
440- return votebutton
441-}
442-
443-function votes (msg) {
444- var votes = h('div.votes.right')
445- if (msg.key) {
446- pull(
447- sbot.links({rel: 'vote', dest: msg.key, live: true }),
448- pull.drain(function (link) {
449- if (link.key) {
450- sbot.get(link.key, function (err, data) {
451- if (err) throw err
452- if (data.content.vote) {
453- if (data.content.vote.value == 1) {
454- if (localStorage[data.author + 'name'])
455- name = localStorage[data.author + 'name']
456- else
457- name = data.author
458- votes.appendChild(h('a#vote:' + data.author.substring(0, 44), {href:'#' + data.author, title: name}, h('img.emoji', {src: config.emojiUrl + 'star.png'})))
459- }
460- else if (data.content.vote.value == -1) {
461- var lookFor = 'vote:' + data.author.substring(0, 44)
462- document.getElementById(lookFor, function (err, gotit) {
463- if (err) throw err
464- gotit.parentNode.removeChild(remove)
465- })
466- }
467- }
468- })
469- }
470- })
471- )
472- }
473- return votes
474-}
475-
476-module.exports.timestamp = function (msg, edited) {
477- var timestamp
478- if (edited)
479- timestamp = h('span.timestmap.right', 'Edited by: ', h('a', {href: '#' + msg.value.author}, h('span.avatar--small', avatar.cachedImage(msg.value.author))), h('a', {href: '#' + msg.key}, human(new Date(msg.value.timestamp))))
480- else
481- timestamp = h('span.timestamp.right', h('a', {href: '#' + msg.key}, human(new Date(msg.value.timestamp))))
482- return timestamp
483-}
484-
485-
486-module.exports.mini = function (msg, content) {
487- var mini = h('div.mini')
488-
489- mini.appendChild(
490- h('span.avatar',
491- h('a', {href: '#' + msg.value.author},
492- h('span.avatar--small', avatar.cachedImage(msg.value.author)),
493- avatar.cachedName(msg.value.author)
494- )
495- )
496- )
497- var lock = h('span.right', h('img.emoji', {src: config.emojiUrl + 'lock.png'}))
498-
499-
500- mini.appendChild(h('span', content))
501- mini.appendChild(exports.timestamp(msg))
502-
503- if (msg.value.content.recps) {
504- mini.appendChild(lock)
505- }
506-
507- if (typeof msg.value.content === 'string') {
508- mini.appendChild(lock)
509- }
510-
511- return mini
512-}
513-
514-module.exports.header = function (msg) {
515- var header = h('div.header')
516-
517- header.appendChild(h('span.avatar',
518- h('a', {href: '#' + msg.value.author},
519- h('span.avatar--small', avatar.cachedImage(msg.value.author)),
520- avatar.cachedName(msg.value.author)
521- )
522- )
523- )
524-
525- header.appendChild(exports.timestamp(msg))
526- header.appendChild(votes(msg))
527-
528- if (msg.value.private) {
529- header.appendChild(h('span.right', ' ', h('img.emoji', {src: config.emojiUrl + 'lock.png'})))
530- }
531- if (msg.value.content.type == 'edit') {
532- header.appendChild(h('span.right', ' Edited: ', h('a', {href: '#' + msg.value.content.original}, exports.messageLink(msg.value.content.original))))
533- }
534- return header
535-}
536-
537-
538-
539-
540-module.exports.messageName = function (id, cb) {
541- // gets the first few characters of a message, for message-link
542- function title (s) {
543- var m = /^\n*([^\n]{0,40})/.exec(s)
544- return m && (m[1].length == 40 ? m[1]+'...' : m[1])
545- }
546-
547- sbot.get(id, function (err, value) {
548- if(err && err.name == 'NotFoundError')
549- return cb(null, id.substring(0, 10)+'...(missing)')
550- if(value.content.type === 'post' && 'string' === typeof value.content.text)
551- return cb(null, title(value.content.text))
552- else if('string' === typeof value.content.text)
553- return cb(null, value.content.type + ':'+title(value.content.text))
554- else
555- return cb(null, id.substring(0, 10)+'...')
556- })
557-}
558-
559-var messageName = exports.messageName
560-
561-module.exports.messageLink = function (id) {
562- if (ref.isMsg(id)) {
563- var link = h('a', {href: '#'+id}, id.substring(0, 10)+'...')
564- messageName(id, function (err, name) {
565- if(err) console.error(err)
566- else link.textContent = name
567- })
568- } else {
569- var link = id
570- }
571- return link
572-}
573-
574-module.exports.rawJSON = function (obj) {
575- return JSON.stringify(obj, null, 2)
576- .split(/([%@&][a-zA-Z0-9\/\+]{43}=*\.[\w]+)/)
577- .map(function (e) {
578- if(ref.isMsg(e) || ref.isFeed(e) || ref.isBlob(e)) {
579- return h('a', {href: '#' + e}, e)
580- }
581- return e
582- })
583-}
584-
585-var markdown = require('ssb-markdown')
586-var config = require('./config')()
587-
588-module.exports.markdown = function (msg, md) {
589- return {innerHTML: markdown.block(msg, {toUrl: function (url, image) {
590- if(url[0] == '%' || url[0] == '@' || url[0] == '#') return '#' + url
591- if(url[0] !== '&') return url
592- //if(url[0] == '&') return config.blobsUrl + url
593- //if(!image) return url
594- return config.blobsUrl + url
595- }})}
596-}
ui/views.jsView
@@ -1,756 +1,0 @@
1-var pull = require('pull-stream')
2-var human = require('human-time')
3-var sbot = require('./scuttlebot')
4-var hyperscroll = require('hyperscroll')
5-var hyperfile = require('hyperfile')
6-var dataurl = require('dataurl-')
7-var More = require('pull-more')
8-var stream = require('hyperloadmore/stream')
9-var h = require('hyperscript')
10-var render = require('./render')
11-var ref = require('ssb-ref')
12-var client = require('ssb-client')
13-var Next = require('pull-next-query')
14-var config = require('./config')()
15-var tools = require('./tools')
16-var avatar = require('./avatar')
17-var id = require('./keys').id
18-var ssbKeys = require('ssb-keys')
19-var keys = require('./keys')
20-var compose = require('./compose')
21-
22-
23-var labelStream = function (label){
24- var content = h('div.content')
25- var screen = document.getElementById('screen')
26- screen.appendChild(hyperscroll(content))
27- content.appendChild(h('div.breadcrumbs.message', h('a', {href: '/'}, 'label'), ' ⯈ ' , h('a', {href: '/#label/' + label}, label)))
28- function createStream (opts) {
29- return pull(
30- Next(sbot.query, opts, ['value', 'timestamp']),
31- pull.map(function (msg){
32- if (msg.value) {
33- sbot.get(msg.value.content.link, function (err, data) {
34- if (data) {
35- var message = {}
36- message.value = data
37- message.key = msg.value.content.link
38- content.appendChild(render(message))
39- }
40- })
41- }
42- })
43- )
44- }
45-
46- pull(
47- createStream({
48- limit: 10,
49- reverse: true,
50- live: false,
51- query: [{$filter: { value: { content: {type: 'label', label: label }, timestamp: { $gt: 0 }}}}]
52- }),
53- stream.bottom(content)
54- )
55-
56- pull(
57- createStream({
58- limit: 10,
59- old: false,
60- live: true,
61- query: [{$filter: { value: { content: {type: 'label', label: label }, timestamp: { $gt: 0 }}}}]
62- }),
63- stream.top(content)
64- )
65-}
66-
67-
68-var privateStream = function () {
69- var screen = document.getElementById('screen')
70- var content = h('div.content')
71-
72- screen.appendChild(hyperscroll(content))
73-
74- function createStream (opts) {
75- return pull(
76- Next(sbot.query, opts, ['value', 'timestamp']),
77- pull.filter(function (msg) {
78- return ((msg.value.private == true) || ('string' == typeof msg.value.content))
79- }),
80- pull.map(function (msg) {
81- /*if (msg.value.private != true) {
82- var unboxed = ssbKeys.unbox(msg.value.content, keys)
83- if (unboxed) {
84- msg.value.content = unboxed
85- msg.value.private = true
86- return render(msg)
87- } else {
88- return render(msg)
89- }
90- } else {return render(msg)}*/
91- return render(msg)
92- })
93- )
94- }
95-
96- pull(
97- createStream({
98- limit: 100,
99- reverse: true,
100- live: false,
101- query: [{$filter: { value: { timestamp: { $gt: 0 }}}}]
102- }),
103- stream.bottom(content)
104- )
105-
106- pull(
107- createStream({
108- limit: 100,
109- old: false,
110- live: true,
111- query: [{$filter: { value: { timestamp: { $gt: 0 }}}}]
112- }),
113- stream.top(content)
114- )
115-
116-
117- /*function createStream (opts) {
118- return pull(
119- Next(sbot.query, opts, ['value', 'timestamp']),
120- pull.map(function (msg) {
121- if (msg.value) {
122- if (msg.value.timestamp > Date.now()) {
123- return h('div.future')
124- } else {
125- return render(msg)
126- }
127- }
128- })
129- )
130- }
131-
132- pull(
133- createStream({
134- limit: 10,
135- reverse: true,
136- live: false,
137- query: [{$filter: { value: { private: true, timestamp: { $gt: 0 }}}}]
138- }),
139- stream.bottom(content)
140- )
141-
142- pull(
143- createStream({
144- limit: 10,
145- old: false,
146- live: true,
147- query: [{$filter: { value: { private: true, timestamp: { $gt: 0 }}}}]
148- }),
149- stream.top(content)
150- )*/
151-
152-
153- /*function createStream (opts) {
154- return pull(
155- More(sbot.createLogStream, opts),
156- pull.filter(function (msg) {
157- return 'string' == typeof msg.value.content
158- }),
159- pull.filter(function (msg) {
160- var unboxed = ssbKeys.unbox(msg.value.content, keys)
161- if (unboxed) {
162- msg.value.content = unboxed
163- msg.value.private = true
164- return msg
165- } else {
166- return msg
167- }
168- }),
169- pull.map(function (msg) {
170- return render(msg)
171- })
172- )
173- }
174-
175- pull(
176- createStream({old: false, limit: 1000}),
177- stream.top(content)
178- )
179-
180- pull(
181- createStream({reverse: true, live: false, limit: 1000}),
182- stream.bottom(content)
183- )*/
184-}
185-
186-var queueStream = function () {
187- var content = h('div.content')
188- var screen = document.getElementById('screen')
189- screen.appendChild(hyperscroll(content))
190-
191- pull(
192- sbot.query({query: [{$filter: { value: {author: id, content: {type: 'queue'}}}}]}),
193- pull.drain(function (msg) {
194- if (msg.value) {
195- if (ref.isMsg(msg.value.content.message)) {
196- if (msg.value.content.queue == true) {
197- sbot.get(msg.value.content.message, function (err, data) {
198- if (data) {
199- var message = {}
200- message.value = data
201- message.key = msg.value.content.message
202- content.appendChild(render(message))
203- }
204- })
205- }
206- if (msg.value.content.queue == false) {
207- setTimeout(function () {
208- var gotIt = document.getElementById(msg.value.content.message.substring(0,44))
209- if (gotIt != null) {
210- gotIt.outerHTML = ''
211- }
212- }, 100)
213- }
214- }
215- }
216- })
217- )
218-}
219-
220-var mentionsStream = function (src) {
221- var content = h('div.content')
222-
223- var screen = document.getElementById('screen')
224-
225- screen.appendChild(hyperscroll(content))
226-
227- function createStream (opts) {
228- return pull(
229- Next(sbot.backlinks, opts, ['value', 'timestamp']),
230- pull.map(function (msg) {
231- if (msg.value.private == true) return h('div.private')
232- return render(msg)
233- })
234- )
235- }
236-
237- pull(
238- createStream({
239- limit: 10,
240- reverse: true,
241- index: 'DTA',
242- live: false,
243- query: [{$filter: {dest: src}}]
244- }),
245- stream.bottom(content)
246- )
247-
248- pull(
249- createStream({
250- limit: 10,
251- old: false,
252- index: 'DTA',
253- live: true,
254- query: [{$filter: {dest: src}}]
255- }),
256- stream.top(content)
257- )
258-}
259-
260-var userStream = function (src) {
261- var content = h('div.content')
262- var screen = document.getElementById('screen')
263-
264- screen.appendChild(hyperscroll(content))
265-
266- function createStream (opts) {
267- return pull(
268- More(sbot.userStream, opts, ['value', 'sequence']),
269- pull.map(function (msg) {
270- return render(h('div', msg))
271- })
272- )
273- }
274-
275- pull(
276- createStream({old: false, limit: 10, id: src}),
277- stream.top(content)
278- )
279-
280- pull(
281- createStream({reverse: true, live: false, limit: 10, id: src}),
282- stream.bottom(content)
283- )
284-
285- var profile = h('div.content#profile', h('div.message'))
286-
287- if (screen.firstChild.firstChild) {
288- screen.firstChild.insertBefore(profile, screen.firstChild.firstChild)
289- } else {
290- screen.firstChild.appendChild(profile)
291- }
292-
293- var name = avatar.name(src)
294-
295- var editname = h('span',
296- avatar.name(src),
297- h('button.btn', 'New name', {
298- onclick: function () {
299- var nameput = h('input', {placeholder: name.textContent})
300- var nameedit =
301- h('span', nameput,
302- h('button.btn', 'Preview', {
303- onclick: function () {
304- if (nameput.value[0] != '@')
305- tobename = nameput.value
306- else
307- tobename = nameput.value.substring(1, 100)
308- var newname = h('span', h('a', {href: '#' + src}, '@' + tobename), h('button.btn', 'Publish', {
309- onclick: function () {
310- var donename = h('span', h('a', {href: '#' + src}, '@' + tobename))
311- sbot.publish({type: 'about', about: src, name: tobename})
312- localStorage[src + 'name'] = tobename
313- newname.parentNode.replaceChild(donename, newname)
314- }
315- }))
316- nameedit.parentNode.replaceChild(newname, nameedit)
317- }
318- })
319- )
320- editname.parentNode.replaceChild(nameedit, editname)
321- }
322- })
323- )
324-
325- var editimage = h('span',
326- h('button.btn', 'New image', {
327- onclick: function () {
328- var upload =
329- h('span',
330- hyperfile.asDataURL(function (data) {
331- if(data) {
332- //img.src = data
333- var _data = dataurl.parse(data)
334- pull(
335- pull.once(_data.data),
336- sbot.addblob(function (err, hash) {
337- if(err) return alert(err.stack)
338- selected = {
339- link: hash,
340- size: _data.data.length,
341- type: _data.mimetype
342- }
343- })
344- )
345- }
346- }),
347- h('button.btn', 'Preview image', {
348- onclick: function() {
349- if (selected) {
350- console.log(selected)
351- var oldImage = document.getElementById('profileImage')
352- var newImage = h('span.avatar--medium', h('img', {src: config.blobsUrl + selected.link}))
353- var publish = h('button.btn', 'Publish image', {
354- onclick: function () {
355- sbot.publish({
356- type: 'about',
357- about: src,
358- image: selected
359- }, function (err, published) {
360- console.log(published)
361- })
362- }
363- })
364- upload.parentNode.replaceChild(publish, upload)
365- oldImage.parentNode.replaceChild(newImage, oldImage)
366- }
367- /*if(selected) {
368- api.message_confirm({
369- type: 'about',
370- about: id,
371- image: selected
372- })
373- } else { alert('select an image before hitting preview')}*/
374- }
375- })
376- )
377- editimage.parentNode.replaceChild(upload, editimage)
378- }
379- })
380- )
381-
382- var avatars = h('div.avatars',
383- h('a', {href: '#' + src},
384- h('span.avatar--medium#profileImage', avatar.image(src)),
385- editname,
386- h('br'),
387- editimage
388- )
389- )
390-
391- pull(
392- sbot.userStream({id: src, reverse: false, limit: 1}),
393- pull.drain(function (msg) {
394- var howlong = h('span', h('br'), ' arrived ', human(new Date(msg.value.timestamp)))
395- avatars.appendChild(howlong)
396- console.log(msg)
397- })
398- )
399-
400-
401- var buttons = h('div.buttons')
402-
403- profile.firstChild.appendChild(avatars)
404- profile.firstChild.appendChild(buttons)
405- buttons.appendChild(tools.mute(src))
406-
407- var writeMessage = h('button.btn', 'Public message ', avatar.name(src), {
408- onclick: function () {
409- opts = {}
410- opts.type = 'post'
411- opts.mentions = '[' + name.textContent + '](' + src + ')'
412- var composer = h('div#composer', h('div.message', compose(opts)))
413- profile.appendChild(composer)
414- }
415- })
416-
417- var writePrivate = h('button.btn', 'Private message ', avatar.name(src), {
418- onclick: function () {
419- opts = {}
420- opts.type = 'post'
421- opts.mentions = '[' + name.textContent + '](' + src + ')'
422- opts.recps = [src, id]
423- var composer = h('div#composer', h('div.message', compose(opts)))
424- profile.appendChild(composer)
425- }
426- })
427-
428- buttons.appendChild(writeMessage)
429- buttons.appendChild(writePrivate)
430- buttons.appendChild(tools.follow(src))
431- buttons.appendChild(tools.block(src))
432-
433- buttons.appendChild(h('button.btn', 'Generate follows', {
434- onclick: function () {
435- profile.firstChild.appendChild(tools.getFollowing(src))
436- profile.firstChild.appendChild(tools.getFollowers(src))
437- }
438- }))
439-
440- buttons.appendChild(h('button.btn', 'Generate blocks', {
441- onclick: function () {
442- profile.firstChild.appendChild(tools.getBlocks(src))
443- profile.firstChild.appendChild(tools.getBlocked(src))
444- }
445- }))
446- buttons.appendChild(h('a', {href: '#wall/' + src}, h('button.btn', avatar.name(src), "'s wall")))
447-
448-}
449-
450-var privateMsg = function (src) {
451- var content = h('div.content')
452- var screen = document.getElementById('screen')
453- screen.appendChild(hyperscroll(content))
454-
455- sbot.get(src, function (err, data) {
456- if (err) {
457- var message = h('div.message', 'Missing message!')
458- content.appendChild(message)
459- }
460- if (data) {
461- console.log(data)
462- data.value = data
463- data.key = src
464-
465- content.appendChild(render(data))
466- }
467-
468- })
469-}
470-
471-var msgThread = function (src) {
472-
473- var content = h('div.content')
474- var screen = document.getElementById('screen')
475- screen.appendChild(hyperscroll(content))
476-
477- pull(
478- sbot.query({query: [{$filter: { value: { content: {root: src}, timestamp: { $gt: 1 }}}}], live: true}),
479- pull.drain(function (msg) {
480- if (msg.value) {
481- content.appendChild(render(msg))
482- }
483- })
484- )
485-
486- sbot.get(src, function (err, data) {
487- if (err) {
488- var message = h('div.message', 'Missing message!')
489- content.appendChild(message)
490- }
491- if (data) {
492- var message = {}
493- message.value = data
494- message.key = src
495- console.log(message)
496- var rootMsg = render(message)
497-
498- if (content.firstChild) {
499- content.insertBefore(rootMsg, content.firstChild)
500- } else {
501- content.appendChild(rootMsg)
502- }
503- if (message.value.content.type == 'git-repo') {
504- pull(
505- sbot.backlinks({query: [{$filter: {value: {content: {type: 'git-update'}}, dest: src}}]}),
506- pull.drain(function (msg) {
507- if (msg.value) {
508- content.appendChild(render(msg))
509- }
510- })
511- )
512- }
513-
514- }
515- })
516-
517-}
518-
519-var keyPage = function () {
520- var screen = document.getElementById('screen')
521-
522- var importKey = h('textarea.import', {placeholder: 'Import a new public/private key', name: 'textarea', style: 'width: 97%; height: 100px;'})
523-
524- var content = h('div.content',
525- h('div.message#key',
526- h('h1', 'Your Key'),
527- h('p', {innerHTML: 'Your public/private key is: <pre><code>' + localStorage[config.caps.shs + '/secret'] + '</code></pre>'},
528- h('button.btn', {onclick: function (e){
529- localStorage[config.caps.shs +'/secret'] = ''
530- alert('Your public/private key has been deleted')
531- e.preventDefault()
532- location.hash = ""
533- location.reload()
534- }}, 'Delete Key')
535- ),
536- h('hr'),
537- h('form',
538- importKey,
539- h('button.btn', {onclick: function (e){
540- if(importKey.value) {
541- localStorage[config.caps.shs + '/secret'] = importKey.value.replace(/\s+/g, ' ')
542- e.preventDefault()
543- alert('Your public/private key has been updated')
544- }
545- location.hash = ""
546- location.reload()
547- }}, 'Import key'),
548- )
549- )
550- )
551-
552- screen.appendChild(hyperscroll(content))
553-}
554-
555-
556-function friendsStream (src) {
557-
558- var screen = document.getElementById('screen')
559- var content = h('div.content')
560-
561- screen.appendChild(hyperscroll(content))
562-
563- function createStream (opts) {
564- return pull(
565- Next(sbot.query, opts, ['value', 'timestamp']),
566- pull.map(function (msg) {
567- sbot.friends.get({source: src, dest: msg.value.author}, function (err, data) {
568- if (data === true) {
569- return content.appendChild(render(msg))
570- console.log(msg)
571- } else {
572- return content.appendChild(h('div'))
573- }
574- })
575- })
576- )
577- }
578-
579- pull(
580- createStream({
581- limit: 1000,
582- reverse: true,
583- live: false,
584- query: [{$filter: { value: { timestamp: { $gt: 0 }}}}]
585- }),
586- stream.bottom(content)
587- )
588-
589-}
590-
591-function everythingStream () {
592-
593- var screen = document.getElementById('screen')
594- var content = h('div.content')
595-
596- screen.appendChild(hyperscroll(content))
597-
598- function createStream (opts) {
599- return pull(
600- Next(sbot.query, opts, ['value', 'timestamp']),
601- pull.map(function (msg) {
602- if (msg.value) {
603- if (msg.value.timestamp > Date.now()) {
604- return h('div.future')
605- } else {
606- return render(msg)
607- }
608- }
609- })
610- )
611- }
612-
613- pull(
614- createStream({
615- limit: 10,
616- reverse: true,
617- live: false,
618- query: [{$filter: { value: { timestamp: { $gt: 0 }}}}]
619- }),
620- stream.bottom(content)
621- )
622-
623- pull(
624- createStream({
625- limit: 10,
626- old: false,
627- live: true,
628- query: [{$filter: { value: { timestamp: { $gt: 0 }}}}]
629- }),
630- stream.top(content)
631- )
632-}
633-
634-
635-
636-
637-function backchannel () {
638-
639- var screen = document.getElementById('screen')
640- var content = h('div.content')
641-
642- screen.appendChild(hyperscroll(content))
643-
644- var chatbox = h('input', {placeholder: 'Backchannel'})
645-
646- var chat = h('div.content')
647-
648- var publish = h('button.btn', 'Publish', {
649- onclick: function () {
650- if (chatbox.value) {
651- var content = {
652- text: chatbox.value,
653- type: 'scat_message'
654- }
655- sbot.publish(content, function (err, msg) {
656- if (err) throw err
657- chatbox.value = ''
658- console.log('Published!', msg)
659- })
660- }
661- }
662- })
663-
664- chat.appendChild(h('div.message', chatbox, publish))
665-
666- if (screen.firstChild.firstChild) {
667- screen.firstChild.insertBefore(chat, screen.firstChild.firstChild)
668- } else {
669- screen.firstChild.appendChild(chat)
670- }
671-
672- function createStream (opts) {
673- return pull(
674- Next(sbot.query, opts, ['value', 'timestamp']),
675- pull.map(function (msg) {
676- if (msg.value) {
677- return render(msg)
678- }
679- })
680- )
681- }
682-
683- pull(
684- createStream({
685- limit: 10,
686- reverse: true,
687- live: false,
688- query: [{$filter: { value: { content: {type: 'scat_message'}, timestamp: { $gt: 0 }}}}]
689- }),
690- stream.bottom(content)
691- )
692-
693- pull(
694- createStream({
695- limit: 10,
696- old: false,
697- live: true,
698- query: [{$filter: { value: { content: {type: 'scat_message'}, timestamp: { $gt: 0 }}}}]
699- }),
700- stream.top(content)
701- )
702-}
703-
704-function search (src) {
705- console.log('search' + src)
706-
707- var content = h('div.content')
708- var screen = document.getElementById('screen')
709- screen.appendChild(hyperscroll(content))
710-
711- pull(
712- sbot.search.query({query: src, limit: 100}),
713- pull.drain(function (search) {
714- content.appendChild(render(search))
715- })
716- )
717-
718-}
719-
720-function hash () {
721- return window.location.hash.substring(1)
722-}
723-
724-module.exports = function () {
725- var src = hash()
726-
727- if (src.substring(52, 59) == '?unbox=') {
728- privateMsg(src)
729- } else if (ref.isFeed(src)) {
730- userStream(src)
731- } else if (ref.isMsg(src)) {
732- msgThread(src)
733- } else if (ref.isFeed(src.substring(5))) {
734- mentionsStream(src.substring(5))
735- } else if (ref.isFeed(src.substring(8))) {
736- friendsStream(src.substring(8))
737- } else if (src.substring(0, 6) === 'label/') {
738- labelStream(src.substring(6))
739- } else if (src == 'queue') {
740- queueStream()
741- } else if (src == 'backchannel') {
742- backchannel()
743- } else if (src == 'private') {
744- privateStream()
745- } else if (src == 'key') {
746- keyPage()
747- } else if (src[0] == '?' || (src[0] == '#')) {
748- if (src[0] == '#')
749- search(src.split('%20').join(' '))
750- else
751- search(src.substr(1).split('%20').join(' '))
752- } else {
753- everythingStream()
754- }
755-
756-}
plugins/friends.mdView
@@ -1,0 +1,75 @@
1 +# ssb-server friends plugin
2 +
3 +Query the follow and flag graphs.
4 +
5 +
6 +## all: async
7 +
8 +Fetch the graph structure.
9 +
10 +```bash
11 +all [graph]
12 +```
13 +
14 +```js
15 +all(graph, cb)
16 +```
17 +
18 + - `graph` (string, default: `follow`): Which graph to view. May be `follow` or `flag`.
19 +
20 +
21 +
22 +## hops: async
23 +
24 +List the degrees-of-connection of all known feeds from the given feed.
25 +
26 +```bash
27 +hops [start] [graph] [--dunbar number] [--hops number]
28 +```
29 +
30 +```js
31 +hops(start, graph, { dunbar:, hops: }, cb)
32 +```
33 +
34 + - `start` (FeedID, default: local user): Which feed to start from.
35 + - `graph` (string, default: `follow`): Which graph to view. May be `follow` or `flag`.
36 + - `dunbar` (number, default: 150): Limit on how many feeds to include in the list.
37 + - `hops` (number, default: 3): Limit on how many hops out the feed needs to be, to be included.
38 +
39 +
40 +
41 +## createFriendStream: source
42 +
43 +Live-stream the ids of feeds which meet the given hops query. If `meta`
44 +option is set, then will return steam of `{id, hops}`
45 +
46 +```bash
47 +createFriendStream [--start feedid] [--graph follow|flag] [--dunbar number] [--hops number] [--meta]
48 +```
49 +
50 +```js
51 +createFriendStream({ start:, graph:, dunbar:, hops: , meta: }, cb)
52 +```
53 +
54 + - `start` (FeedID, default: local user): Which feed to start from.
55 + - `graph` (string, default: `follow`): Which graph to view. May be `follow` or `flag`.
56 + - `dunbar` (number, default: 150): Limit on how many feeds to include in the list.
57 + - `hops` (number, default: 3): Limit on how many hops out the feed needs to be, to be included.
58 +
59 +
60 +
61 +## get: async
62 +
63 +Get the edge between two different feeds.
64 +
65 +```bash
66 +get --source {feedid} --dest {feedid} [--graph follow|flag]
67 +```
68 +
69 +```js
70 +get({ source:, dest:, graph: }, cb)
71 +```
72 +
73 + - `source` (FeedID): Edge source.
74 + - `dest` (FeedID): Edge destination.
75 + - `graph` (string, default: `follow`): Which graph to query. May be `follow` or `flag`.
plugins/gossip.mdView
@@ -1,0 +1,131 @@
1 +# ssb-server gossip plugin
2 +
3 +Schedule connections randomly with a peerlist constructed from config, multicast UDP announcements, feed announcements, and API-calls.
4 +
5 +
6 +
7 +## peers: sync
8 +
9 +Get the current peerlist.
10 +
11 +```bash
12 +peers
13 +```
14 +
15 +```js
16 +peers(cb)
17 +```
18 +
19 +
20 +
21 +## add: sync
22 +
23 +Add an address to the peer table.
24 +
25 +```bash
26 +add {addr}
27 +add --host {string} --port {number} --key {feedid}
28 +```
29 +
30 +```js
31 +add(addr, cb)
32 +add({ host:, port:, key: }, cb)
33 +```
34 +
35 + - `addr` (address string): An address string, of the following format: `hostname:port:feedid`.
36 + - `host` (host string): IP address or hostname.
37 + - `port` (port number)
38 + - `key` (feedid)
39 +
40 +## remove: sync
41 +
42 +Remove an address from the peer table.
43 +
44 +```bash
45 +remove {addr}
46 +remove --host {string} --port {number} --key {feedid}
47 +```
48 +
49 +```js
50 +remove(addr)
51 +remove({ host:, port:, key: })
52 +```
53 +
54 +## ping: duplex
55 +
56 +used internally by the gossip plugin to measure latency and clock skew
57 +
58 +## connect: async
59 +
60 +Add an address to the peer table, and connect immediately.
61 +
62 +```bash
63 +connect {addr}
64 +connect --host {string} --port {number} --key {feedid}
65 +```
66 +
67 +```js
68 +connect(addr, cb)
69 +connect({ host:, port:, key: }, cb)
70 +```
71 +
72 + - `addr` (address string): An address string, of the following format: `hostname:port:feedid`.
73 + - `host` (host string): IP address or hostname.
74 + - `port` (port number)
75 + - `key` (feedid)
76 +
77 +
78 +## changes: source
79 +
80 +Listen for gossip events.
81 +
82 +```bash
83 +changes
84 +```
85 +
86 +```js
87 +changes()
88 +```
89 +
90 +Events come in the following forms:
91 +
92 +```
93 +{ type: 'discover', peer:, source: }
94 +{ type: 'connect', peer: }
95 +{ type: 'connect-failure', peer: }
96 +{ type: 'disconnect', peer: }
97 +```
98 +
99 +## reconnect: sync
100 +
101 +Tell ssb-server to reinitiate gossip connections now.
102 +
103 +
104 +## enable: sync
105 +
106 +Update the config to enable a gossip type.
107 +
108 +```bash
109 +enable {type}
110 +```
111 +```js
112 +enable(type, cb)
113 +```
114 +
115 + - type (string): The type of gossip to enable: local, global, or seed. Default
116 + global.
117 +
118 +
119 +## disable: sync
120 +
121 +Update the config to disable a gossip type.
122 +
123 +```bash
124 +disable {type}
125 +```
126 +```js
127 +disable(type, cb)
128 +```
129 +
130 + - type (string): The type of gossip to enable: local, global, or seed. Default
131 + global.
plugins/gossip/index.jsView
@@ -1,0 +1,417 @@
1 +'use strict'
2 +var pull = require('pull-stream')
3 +var Notify = require('pull-notify')
4 +var mdm = require('mdmanifest')
5 +var valid = require('../../lib/validators')
6 +var apidoc = require('../../lib/apidocs').gossip
7 +var u = require('../../lib/util')
8 +var ref = require('ssb-ref')
9 +var ping = require('pull-ping')
10 +var stats = require('statistics')
11 +var Schedule = require('./schedule')
12 +var Init = require('./init')
13 +var AtomicFile = require('atomic-file')
14 +var fs = require('fs')
15 +var path = require('path')
16 +var deepEqual = require('deep-equal')
17 +
18 +function isFunction (f) {
19 + return 'function' === typeof f
20 +}
21 +
22 +function stringify(peer) {
23 + return [peer.host, peer.port, peer.key].join(':')
24 +}
25 +
26 +function isObject (o) {
27 + return o && 'object' == typeof o
28 +}
29 +
30 +function toBase64 (s) {
31 + if(isString(s)) return s.substring(1, s.indexOf('.'))
32 + else s.toString('base64') //assume a buffer
33 +}
34 +
35 +function isString (s) {
36 + return 'string' == typeof s
37 +}
38 +
39 +function coearseAddress (address) {
40 + if(isObject(address)) {
41 + if(ref.isAddress(address.address))
42 + return address.address
43 + var protocol = 'net'
44 + if (address.host && address.host.endsWith(".onion"))
45 + protocol = 'onion'
46 + return [protocol, address.host, address.port].join(':') +'~'+['shs', toBase64(address.key)].join(':')
47 + }
48 + return address
49 +}
50 +
51 +/*
52 +Peers : [{
53 + //modern:
54 + address: <multiserver address>,
55 +
56 +
57 + //legacy
58 + key: id,
59 + host: ip,
60 + port: int,
61 +
62 + //to be backwards compatible with patchwork...
63 + announcers: {length: int}
64 + //TODO: availability
65 + //availability: 0-1, //online probability estimate
66 +
67 + //where this peer was added from. TODO: remove "pub" peers.
68 + source: 'pub'|'manual'|'local'
69 +}]
70 +*/
71 +
72 +
73 +module.exports = {
74 + name: 'gossip',
75 + version: '1.0.0',
76 + manifest: mdm.manifest(apidoc),
77 + permissions: {
78 + anonymous: {allow: ['ping']}
79 + },
80 + init: function (server, config) {
81 + var notify = Notify()
82 + var closed = false, closeScheduler
83 + var conf = config.gossip || {}
84 +
85 + var gossipJsonPath = path.join(config.path, 'gossip.json')
86 + var stateFile = AtomicFile(gossipJsonPath)
87 +
88 + var status = {}
89 +
90 + //Known Peers
91 + var peers = []
92 +
93 + function getPeer(id) {
94 + return u.find(peers, function (e) {
95 + return e && e.key === id
96 + })
97 + }
98 +
99 + function simplify (peer) {
100 + return {
101 + address: peer.address || coearseAddress(peer),
102 + source: peer.source,
103 + state: peer.state, stateChange: peer.stateChange,
104 + failure: peer.failure,
105 + client: peer.client,
106 + stats: {
107 + duration: peer.duration || undefined,
108 + rtt: peer.ping ? peer.ping.rtt : undefined,
109 + skew: peer.ping ? peer.ping.skew : undefined,
110 + }
111 + }
112 + }
113 +
114 + server.status.hook(function (fn) {
115 + var _status = fn()
116 + _status.gossip = status
117 + peers.forEach(function (peer) {
118 + if(peer.stateChange + 3e3 > Date.now() || peer.state === 'connected')
119 + status[peer.key] = simplify(peer)
120 + })
121 + return _status
122 +
123 + })
124 +
125 + server.close.hook(function (fn, args) {
126 + closed = true
127 + closeScheduler()
128 + for(var id in server.peers)
129 + server.peers[id].forEach(function (peer) {
130 + peer.close(true)
131 + })
132 + return fn.apply(this, args)
133 + })
134 +
135 + var timer_ping = 5*6e4
136 +
137 + function setConfig(name, value) {
138 + config.gossip = config.gossip || {}
139 + config.gossip[name] = value
140 +
141 + var cfgPath = path.join(config.path, 'config')
142 + var existingConfig = {}
143 +
144 + // load ~/.ssb/config
145 + try { existingConfig = JSON.parse(fs.readFileSync(cfgPath, 'utf-8')) }
146 + catch (e) {}
147 +
148 + // update the plugins config
149 + existingConfig.gossip = existingConfig.gossip || {}
150 + existingConfig.gossip[name] = value
151 +
152 + // write to disc
153 + fs.writeFileSync(cfgPath, JSON.stringify(existingConfig, null, 2), 'utf-8')
154 + }
155 +
156 + var gossip = {
157 + wakeup: 0,
158 + peers: function () {
159 + return peers
160 + },
161 + get: function (addr) {
162 + //addr = ref.parseAddress(addr)
163 + if(ref.isFeed(addr)) return getPeer(addr)
164 + else if(ref.isFeed(addr.key)) return getPeer(addr.key)
165 + else throw new Error('must provide id:'+JSON.stringify(addr))
166 +// return u.find(peers, function (a) {
167 +// return (
168 +// addr.port === a.port
169 +// && addr.host === a.host
170 +// && addr.key === a.key
171 +// )
172 +// })
173 + },
174 + connect: valid.async(function (addr, cb) {
175 + if(ref.isFeed(addr))
176 + addr = gossip.get(addr)
177 + server.emit('log:info', ['ssb-server', stringify(addr), 'CONNECTING'])
178 + if(!ref.isAddress(addr.address))
179 + addr = ref.parseAddress(addr)
180 + if (!addr || typeof addr != 'object')
181 + return cb(new Error('first param must be an address'))
182 +
183 + if(!addr.address)
184 + if(!addr.key) return cb(new Error('address must have ed25519 key'))
185 + // add peer to the table, incase it isn't already.
186 + gossip.add(addr, 'manual')
187 + var p = gossip.get(addr)
188 + if(!p) return cb()
189 +
190 + p.stateChange = Date.now()
191 + p.state = 'connecting'
192 + server.connect(p.address, function (err, rpc) {
193 + if (err) {
194 + p.error = err.stack
195 + p.state = undefined
196 + p.failure = (p.failure || 0) + 1
197 + p.stateChange = Date.now()
198 + notify({ type: 'connect-failure', peer: p })
199 + server.emit('log:info', ['ssb-server', stringify(p), 'ERR', (err.message || err)])
200 + p.duration = stats(p.duration, 0)
201 + return (cb && cb(err))
202 + }
203 + else {
204 + delete p.error
205 + p.state = 'connected'
206 + p.failure = 0
207 + }
208 + cb && cb(null, rpc)
209 + })
210 +
211 + }, 'string|object'),
212 +
213 + disconnect: valid.async(function (addr, cb) {
214 + var peer = gossip.get(addr)
215 +
216 + peer.state = 'disconnecting'
217 + peer.stateChange = Date.now()
218 + if(!peer || !peer.disconnect) cb && cb()
219 + else peer.disconnect(true, function (err) {
220 + peer.stateChange = Date.now()
221 + cb && cb()
222 + })
223 +
224 + }, 'string|object'),
225 +
226 + changes: function () {
227 + return notify.listen()
228 + },
229 + //add an address to the peer table.
230 + add: valid.sync(function (addr, source) {
231 +
232 + if(isObject(addr)) {
233 + addr.address = coearseAddress(addr)
234 + }
235 + else {
236 + var _addr = ref.parseAddress(addr)
237 + if(!_addr) throw new Error('not a valid address:'+addr)
238 + _addr.address = addr
239 + addr = _addr
240 + }
241 + if(!ref.isAddress(addr.address) /*&& !ref.isAddress(addr)*/)
242 + throw new Error('not a valid address:' + JSON.stringify(addr))
243 + // check that this is a valid address, and not pointing at self.
244 +
245 + if(addr.key === server.id) return
246 +
247 + var f = gossip.get(addr)
248 +
249 + if(!f) {
250 + // new peer
251 + addr.source = source
252 + addr.announcers = 1
253 + addr.duration = addr.duration || null
254 + peers.push(addr)
255 + notify({ type: 'discover', peer: addr, source: source || 'manual' })
256 + return addr
257 + } else if (source === 'friends' || source === 'local') {
258 + // this peer is a friend or local, override old source to prioritize gossip
259 + f.source = source
260 + }
261 + //don't count local over and over
262 + else if(f.source != 'local')
263 + f.announcers ++
264 +
265 + return f
266 + }, 'string|object', 'string?'),
267 + remove: function (addr) {
268 + var peer = gossip.get(addr)
269 + var index = peers.indexOf(peer)
270 + if (~index) {
271 + peers.splice(index, 1)
272 + notify({ type: 'remove', peer: peer })
273 + }
274 + },
275 + ping: function (opts) {
276 + var timeout = config.timers && config.timers.ping || 5*60e3
277 + //between 10 seconds and 30 minutes, default 5 min
278 + timeout = Math.max(10e3, Math.min(timeout, 30*60e3))
279 + return ping({timeout: timeout})
280 + },
281 + reconnect: function () {
282 + for(var id in server.peers)
283 + if(id !== server.id) //don't disconnect local client
284 + server.peers[id].forEach(function (peer) {
285 + peer.close(true)
286 + })
287 + return gossip.wakeup = Date.now()
288 + },
289 + enable: valid.sync(function (type) {
290 + type = type || 'global'
291 + setConfig(type, true)
292 + if(type === 'local' && server.local && server.local.init)
293 + server.local.init()
294 + return 'enabled gossip type ' + type
295 + }, 'string?'),
296 + disable: valid.sync(function (type) {
297 + type = type || 'global'
298 + setConfig(type, false)
299 + return 'disabled gossip type ' + type
300 + }, 'string?')
301 + }
302 +
303 + closeScheduler = Schedule (gossip, config, server)
304 + Init (gossip, config, server)
305 + //get current state
306 +
307 + server.on('rpc:connect', function (rpc, isClient) {
308 +
309 + // if we're not ready, close this connection immediately
310 + if (!server.ready() && rpc.id !== server.id) return rpc.close()
311 +
312 + var peer = getPeer(rpc.id)
313 + //don't track clients that connect, but arn't considered peers.
314 + //maybe we should though?
315 + if(!peer) {
316 + if(rpc.id !== server.id) {
317 + server.emit('log:info', ['ssb-server', rpc.id, 'Connected'])
318 + rpc.on('closed', function () {
319 + server.emit('log:info', ['ssb-server', rpc.id, 'Disconnected'])
320 + })
321 + }
322 + return
323 + }
324 +
325 + status[rpc.id] = simplify(peer)
326 +
327 + server.emit('log:info', ['ssb-server', stringify(peer), 'PEER JOINED'])
328 + //means that we have created this connection, not received it.
329 + peer.client = !!isClient
330 + peer.state = 'connected'
331 + peer.stateChange = Date.now()
332 + peer.disconnect = function (err, cb) {
333 + if(isFunction(err)) cb = err, err = null
334 + rpc.close(err, cb)
335 + }
336 +
337 + if(isClient) {
338 + //default ping is 5 minutes...
339 + var pp = ping({serve: true, timeout: timer_ping}, function (_) {})
340 + peer.ping = {rtt: pp.rtt, skew: pp.skew}
341 + pull(
342 + pp,
343 + rpc.gossip.ping({timeout: timer_ping}, function (err) {
344 + if(err.name === 'TypeError') peer.ping.fail = true
345 + }),
346 + pp
347 + )
348 + }
349 +
350 + rpc.on('closed', function () {
351 + delete status[rpc.id]
352 + server.emit('log:info', ['ssb-server', stringify(peer),
353 + ['DISCONNECTED. state was', peer.state, 'for',
354 + (new Date() - peer.stateChange)/1000, 'seconds'].join(' ')])
355 + //track whether we have successfully connected.
356 + //or how many failures there have been.
357 + var since = peer.stateChange
358 + peer.stateChange = Date.now()
359 +// if(peer.state === 'connected') //may be "disconnecting"
360 + peer.duration = stats(peer.duration, peer.stateChange - since)
361 + peer.state = undefined
362 + notify({ type: 'disconnect', peer: peer })
363 + })
364 +
365 + notify({ type: 'connect', peer: peer })
366 + })
367 +
368 + var last
369 + stateFile.get(function (err, ary) {
370 + last = ary || []
371 + if(Array.isArray(ary))
372 + ary.forEach(function (v) {
373 + delete v.state
374 + // don't add local peers (wait to rediscover)
375 + // adding peers back this way means old format gossip.json
376 + // will be updated to having proper address values.
377 + if(v.source !== 'local') {
378 + gossip.add(v, 'stored')
379 + }
380 + })
381 + })
382 +
383 + var int = setInterval(function () {
384 + var copy = peers.filter(function (e) {
385 + return e.source !== 'local'
386 + }).map(function (e) {
387 + var o = {}
388 + for(var k in e) {
389 + if(k !== 'state') o[k] = e[k]
390 + }
391 +
392 + //try to ensure that the peer always has host and port
393 + //so that the output file is understood by previous versions
394 + //of scuttlebutt.
395 + if(!o.host || !o.port) {
396 + var _addr = ref.parseAddress(e.address)
397 + o.host = _addr.host
398 + o.port = _addr.port
399 + }
400 +
401 +
402 + return o
403 + })
404 + if(deepEqual(copy, last)) return
405 + last = copy
406 + stateFile.set(copy, function(err) {
407 + if (err) console.log(err)
408 + })
409 + }, 10*1000)
410 +
411 + if(int.unref) int.unref()
412 +
413 + return gossip
414 + }
415 +}
416 +
417 +
plugins/gossip/init.jsView
@@ -1,0 +1,29 @@
1 +var isArray = Array.isArray
2 +var pull = require('pull-stream')
3 +var ref = require('ssb-ref')
4 +
5 +module.exports = function (gossip, config, server) {
6 + if (config.offline) return void console.log("Running in offline mode: gossip disabled")
7 +
8 + // populate peertable with configured seeds (mainly used in testing)
9 + var seeds = config.seeds
10 +
11 + ;(isArray(seeds) ? seeds : [seeds]).filter(Boolean)
12 + .forEach(function (addr) { gossip.add(addr, 'seed') })
13 +
14 + // populate peertable with pub announcements on the feed (allow this to be disabled via config)
15 + if(!config.gossip || (config.gossip.autoPopulate !== false && config.gossip.pub !== false))
16 + pull(
17 + server.messagesByType({
18 + type: 'pub', live: true, keys: false
19 + }),
20 + pull.drain(function (msg) {
21 + if(msg.sync) return
22 + if(!msg.content.address) return
23 + if(ref.isAddress(msg.content.address))
24 + gossip.add(msg.content.address, 'pub')
25 + }, function () {
26 + console.warn('[gossip] warning: this can happen if the database closes', arguments)
27 + })
28 + )
29 +}
plugins/gossip/schedule.jsView
@@ -1,0 +1,267 @@
1 +'use strict'
2 +var ip = require('ip')
3 +var onWakeup = require('on-wakeup')
4 +var onNetwork = require('on-change-network')
5 +var hasNetwork = require('../../lib/has-network-debounced')
6 +
7 +var pull = require('pull-stream')
8 +
9 +function not (fn) {
10 + return function (e) { return !fn(e) }
11 +}
12 +
13 +function and () {
14 + var args = [].slice.call(arguments)
15 + return function (value) {
16 + return args.every(function (fn) { return fn.call(null, value) })
17 + }
18 +}
19 +
20 +//min delay (delay since last disconnect of most recent peer in unconnected set)
21 +//unconnected filter delay peer < min delay
22 +function delay (failures, factor, max) {
23 + return Math.min(Math.pow(2, failures)*factor, max || Infinity)
24 +}
25 +
26 +function maxStateChange (M, e) {
27 + return Math.max(M, e.stateChange || 0)
28 +}
29 +
30 +function peerNext(peer, opts) {
31 + return (peer.stateChange|0) + delay(peer.failure|0, opts.factor, opts.max)
32 +}
33 +
34 +
35 +//detect if not connected to wifi or other network
36 +//(i.e. if there is only localhost)
37 +
38 +function isOffline (e) {
39 + if(ip.isLoopback(e.host) || e.host == 'localhost') return false
40 + return !hasNetwork()
41 +}
42 +
43 +var isOnline = not(isOffline)
44 +
45 +function isLocal (e) {
46 + // don't rely on private ip address, because
47 + // cjdns creates fake private ip addresses.
48 + // ignore localhost addresses, because sometimes they get broadcast.
49 + return !ip.isLoopback(e.host) && ip.isPrivate(e.host) && e.source === 'local'
50 +}
51 +
52 +function isSeed (e) {
53 + return e.source === 'seed'
54 +}
55 +
56 +function isFriend (e) {
57 + return e.source === 'friends'
58 +}
59 +
60 +function isUnattempted (e) {
61 + return !e.stateChange
62 +}
63 +
64 +//select peers which have never been successfully connected to yet,
65 +//but have been tried.
66 +function isInactive (e) {
67 + return e.stateChange && (!e.duration || e.duration.mean == 0)
68 +}
69 +
70 +function isLongterm (e) {
71 + return e.ping && e.ping.rtt && e.ping.rtt.mean > 0
72 +}
73 +
74 +//peers which we can connect to, but are not upgraded.
75 +//select peers which we can connect to, but are not upgraded to LT.
76 +//assume any peer is legacy, until we know otherwise...
77 +function isLegacy (peer) {
78 + return peer.duration && (peer.duration && peer.duration.mean > 0) && !exports.isLongterm(peer)
79 +}
80 +
81 +function isConnect (e) {
82 + return 'connected' === e.state || 'connecting' === e.state
83 +}
84 +
85 +//sort oldest to newest then take first n
86 +function earliest(peers, n) {
87 + return peers.sort(function (a, b) {
88 + return a.stateChange - b.stateChange
89 + }).slice(0, Math.max(n, 0))
90 +}
91 +
92 +function select(peers, ts, filter, opts) {
93 + if(opts.disable) return []
94 + //opts: { quota, groupMin, min, factor, max }
95 + var type = peers.filter(filter)
96 + var unconnect = type.filter(not(isConnect))
97 + var count = Math.max(opts.quota - type.filter(isConnect).length, 0)
98 + var min = unconnect.reduce(maxStateChange, 0) + opts.groupMin
99 + if(ts < min) return []
100 +
101 + return earliest(unconnect.filter(function (peer) {
102 + return peerNext(peer, opts) < ts
103 + }), count)
104 +}
105 +
106 +var schedule = exports = module.exports =
107 +function (gossip, config, server) {
108 + var min = 60e3, hour = 60*60e3, closed = false
109 +
110 + //trigger hard reconnect after suspend or local network changes
111 + onWakeup(gossip.reconnect)
112 + onNetwork(gossip.reconnect)
113 +
114 + function conf(name, def) {
115 + if(config.gossip == null) return def
116 + var value = config.gossip[name]
117 + return (value == null || value === '') ? def : value
118 + }
119 +
120 + function connect (peers, ts, name, filter, opts) {
121 + opts.group = name
122 + var connected = peers.filter(isConnect).filter(filter)
123 +
124 + //disconnect if over quota
125 + if(connected.length > opts.quota) {
126 + return earliest(connected, connected.length - opts.quota)
127 + .forEach(function (peer) {
128 + gossip.disconnect(peer)
129 + })
130 + }
131 +
132 + //will return [] if the quota is full
133 + var selected = select(peers, ts, and(filter, isOnline), opts)
134 + selected
135 + .forEach(function (peer) {
136 + gossip.connect(peer)
137 + })
138 + }
139 +
140 + var lastMessageAt
141 + server.post(function (data) {
142 + if(data.value.author != server.id) lastMessageAt = Date.now()
143 + })
144 +
145 + function isCurrentlyDownloading () {
146 + // don't schedule gossip if currently downloading messages
147 + if (lastMessageAt && lastMessageAt > Date.now() - 500) {
148 + return true
149 + }
150 + }
151 +
152 + var connecting = false
153 + function connections () {
154 + if(connecting || closed) return
155 + connecting = true
156 + var timer = setTimeout(function () {
157 + connecting = false
158 +
159 + // don't attempt to connect while migration is running
160 + if (!server.ready() || isCurrentlyDownloading()) return
161 +
162 + var ts = Date.now()
163 + var peers = gossip.peers()
164 +
165 + var connected = peers.filter(and(isConnect, not(isLocal), not(isFriend))).length
166 +
167 + var connectedFriends = peers.filter(and(isConnect, isFriend)).length
168 +
169 + if(conf('friends', true))
170 + connect(peers, ts, 'friends', isFriend, {
171 + quota: 3, factor: 2e3, max: 10*min, groupMin: 1e3,
172 + })
173 +
174 + if(conf('seed', true))
175 + connect(peers, ts, 'seeds', isSeed, {
176 + quota: 3, factor: 2e3, max: 10*min, groupMin: 1e3,
177 + })
178 +
179 + if(conf('local', true))
180 + connect(peers, ts, 'local', isLocal, {
181 + quota: 3, factor: 2e3, max: 10*min, groupMin: 1e3,
182 + })
183 +
184 + if(conf('global', true)) {
185 + // prioritize friends
186 + connect(peers, ts, 'friends', and(exports.isFriend, exports.isLongterm), {
187 + quota: 2, factor: 10e3, max: 10*min, groupMin: 5e3,
188 + })
189 +
190 + if (connectedFriends < 2)
191 + connect(peers, ts, 'attemptFriend', and(exports.isFriend, exports.isUnattempted), {
192 + min: 0, quota: 1, factor: 0, max: 0, groupMin: 0,
193 + })
194 +
195 + connect(peers, ts, 'retryFriends', and(exports.isFriend, exports.isInactive), {
196 + min: 0,
197 + quota: 3, factor: 60e3, max: 3*60*60e3, groupMin: 5*60e3,
198 + })
199 +
200 + // standard longterm peers
201 + connect(peers, ts, 'longterm', and(
202 + exports.isLongterm,
203 + not(exports.isFriend),
204 + not(exports.isLocal)
205 + ), {
206 + quota: 2, factor: 10e3, max: 10*min, groupMin: 5e3,
207 + })
208 +
209 + if(!connected)
210 + connect(peers, ts, 'attempt', exports.isUnattempted, {
211 + min: 0, quota: 1, factor: 0, max: 0, groupMin: 0,
212 + })
213 +
214 + //quota, groupMin, min, factor, max
215 + connect(peers, ts, 'retry', exports.isInactive, {
216 + min: 0,
217 + quota: 3, factor: 5*60e3, max: 3*60*60e3, groupMin: 5*50e3,
218 + })
219 +
220 + var longterm = peers.filter(isConnect).filter(isLongterm).length
221 +
222 + connect(peers, ts, 'legacy', exports.isLegacy, {
223 + quota: 3 - longterm,
224 + factor: 5*min, max: 3*hour, groupMin: 5*min,
225 + })
226 + }
227 +
228 + peers.filter(isConnect).forEach(function (e) {
229 + var permanent = exports.isLongterm(e) || exports.isLocal(e)
230 + if((!permanent || e.state === 'connecting') && e.stateChange + 10e3 < ts) {
231 + gossip.disconnect(e)
232 + }
233 + })
234 +
235 + }, 100*Math.random())
236 + if(timer.unref) timer.unref()
237 + }
238 +
239 + pull(
240 + gossip.changes(),
241 + pull.drain(function (ev) {
242 + if(ev.type == 'disconnect')
243 + connections()
244 + }, function () {
245 + console.warn('[gossip/dc] warning: this can happen if the database closes', arguments)
246 + })
247 + )
248 +
249 + var int = setInterval(connections, 2e3)
250 + if(int.unref) int.unref()
251 +
252 + connections()
253 +
254 + return function onClose () {
255 + closed = true
256 + }
257 +
258 +}
259 +
260 +exports.isUnattempted = isUnattempted
261 +exports.isInactive = isInactive
262 +exports.isLongterm = isLongterm
263 +exports.isLegacy = isLegacy
264 +exports.isLocal = isLocal
265 +exports.isFriend = isFriend
266 +exports.isConnectedOrConnecting = isConnect
267 +exports.select = select
plugins/invite.jsView
@@ -1,0 +1,278 @@
1 +'use strict'
2 +var crypto = require('crypto')
3 +var ssbKeys = require('ssb-keys')
4 +var toAddress = require('../lib/util').toAddress
5 +var cont = require('cont')
6 +var explain = require('explain-error')
7 +var ip = require('ip')
8 +var mdm = require('mdmanifest')
9 +var valid = require('../lib/validators')
10 +var apidoc = require('../lib/apidocs').invite
11 +var ref = require('ssb-ref')
12 +
13 +var ssbClient = require('ssb-client')
14 +
15 +// invite plugin
16 +// adds methods for producing invite-codes,
17 +// which peers can use to command your server to follow them.
18 +
19 +function isFunction (f) {
20 + return 'function' === typeof f
21 +}
22 +
23 +function isString (s) {
24 + return 'string' === typeof s
25 +}
26 +
27 +function isObject(o) {
28 + return o && 'object' === typeof o
29 +}
30 +
31 +function isNumber(n) {
32 + return 'number' === typeof n && !isNaN(n)
33 +}
34 +
35 +module.exports = {
36 + name: 'invite',
37 + version: '1.0.0',
38 + manifest: mdm.manifest(apidoc),
39 + permissions: {
40 + master: {allow: ['create']},
41 + //temp: {allow: ['use']}
42 + },
43 + init: function (server, config) {
44 + var codes = {}
45 + var codesDB = server.sublevel('codes')
46 +
47 + //add an auth hook.
48 + server.auth.hook(function (fn, args) {
49 + var pubkey = args[0], cb = args[1]
50 +
51 + // run normal authentication
52 + fn(pubkey, function (err, auth) {
53 + if(err || auth) return cb(err, auth)
54 +
55 + // if no rights were already defined for this pubkey
56 + // check if the pubkey is one of our invite codes
57 + codesDB.get(pubkey, function (_, code) {
58 + //disallow if this invite has already been used.
59 + if(code && (code.used >= code.total)) cb()
60 + else cb(null, code && code.permissions)
61 + })
62 + })
63 + })
64 +
65 + function getInviteAddress () {
66 + return (config.allowPrivate
67 + ? server.getAddress('public') || server.getAddress('local') || server.getAddress('private')
68 + : server.getAddress('public')
69 + )
70 + }
71 +
72 + return {
73 + create: valid.async(function (opts, cb) {
74 + opts = opts || {}
75 + if(isNumber(opts))
76 + opts = {uses: opts}
77 + else if(isObject(opts)) {
78 + if(opts.modern)
79 + opts.uses = 1
80 + }
81 + else if(isFunction(opts))
82 + cb = opts, opts = {}
83 +
84 + var addr = getInviteAddress()
85 + if(!addr) return cb(new Error(
86 + 'no address available for creating an invite,'+
87 + 'configuration needed for server.\n'+
88 + 'see: https://github.com/ssbc/ssb-config/#connections'
89 + ))
90 + addr = addr.split(';').shift()
91 + var host = ref.parseAddress(addr).host
92 + if(typeof host !== 'string') {
93 + return cb(new Error('Could not parse host portion from server address:' + addr))
94 + }
95 +
96 + if (opts.external)
97 + host = opts.external
98 +
99 + if(!config.allowPrivate && (ip.isPrivate(host) || 'localhost' === host || host === ''))
100 + return cb(new Error('Server has no public ip address, '
101 + + 'cannot create useable invitation'))
102 +
103 + //this stuff is SECURITY CRITICAL
104 + //so it should be moved into the main app.
105 + //there should be something that restricts what
106 + //permissions the plugin can create also:
107 + //it should be able to diminish it's own permissions.
108 +
109 + // generate a key-seed and its key
110 + var seed = crypto.randomBytes(32)
111 + var keyCap = ssbKeys.generate('ed25519', seed)
112 +
113 + // store metadata under the generated pubkey
114 + var owner = server.id
115 + codesDB.put(keyCap.id, {
116 + id: keyCap.id,
117 + total: +opts.uses || 1,
118 + note: opts.note,
119 + used: 0,
120 + permissions: {allow: ['invite.use', 'getAddress'], deny: null}
121 + }, function (err) {
122 + // emit the invite code: our server address, plus the key-seed
123 + if(err) cb(err)
124 + else if(opts.modern) {
125 + var ws_addr = getInviteAddress().split(';').sort(function (a, b) {
126 + return +/^ws/.test(b) - +/^ws/.test(a)
127 + }).shift()
128 +
129 +
130 + if(!/^ws/.test(ws_addr)) throw new Error('not a ws address:'+ws_addr)
131 + cb(null, ws_addr+':'+seed.toString('base64'))
132 + }
133 + else {
134 + addr = ref.parseAddress(addr)
135 + cb(null, [opts.external ? opts.external : addr.host, addr.port, addr.key].join(':') + '~' + seed.toString('base64'))
136 + }
137 + })
138 + }, 'number|object', 'string?'),
139 + use: valid.async(function (req, cb) {
140 + var rpc = this
141 +
142 + // fetch the code
143 + codesDB.get(rpc.id, function(err, invite) {
144 + if(err) return cb(err)
145 +
146 + // check if we're already following them
147 + server.friends.get(function (err, follows) {
148 +// server.friends.all('follow', function(err, follows) {
149 +// if(hops[req.feed] == 1)
150 + if (follows && follows[server.id] && follows[server.id][req.feed])
151 + return cb(new Error('already following'))
152 +
153 + // although we already know the current feed
154 + // it's included so that request cannot be replayed.
155 + if(!req.feed)
156 + return cb(new Error('feed to follow is missing'))
157 +
158 + if(invite.used >= invite.total)
159 + return cb(new Error('invite has expired'))
160 +
161 + invite.used ++
162 +
163 + //never allow this to be used again
164 + if(invite.used >= invite.total) {
165 + invite.permissions = {allow: [], deny: null}
166 + }
167 + //TODO
168 + //okay so there is a small race condition here
169 + //if people use a code massively in parallel
170 + //then it may not be counted correctly...
171 + //this is not a big enough deal to fix though.
172 + //-dominic
173 +
174 + // update code metadata
175 + codesDB.put(rpc.id, invite, function (err) {
176 + server.emit('log:info', ['invite', rpc.id, 'use', req])
177 +
178 + // follow the user
179 + server.publish({
180 + type: 'contact',
181 + contact: req.feed,
182 + following: true,
183 + pub: true,
184 + note: invite.note || undefined
185 + }, cb)
186 + })
187 + })
188 + })
189 + }, 'object'),
190 + accept: valid.async(function (invite, cb) {
191 + // remove surrounding quotes, if found
192 + if (invite.charAt(0) === '"' && invite.charAt(invite.length - 1) === '"')
193 + invite = invite.slice(1, -1)
194 + var opts
195 + // connect to the address in the invite code
196 + // using a keypair generated from the key-seed in the invite code
197 + var modern = false
198 + if(ref.isInvite(invite)) { //legacy ivite
199 + if(ref.isLegacyInvite(invite)) {
200 + var parts = invite.split('~')
201 + opts = ref.parseAddress(parts[0])//.split(':')
202 + //convert legacy code to multiserver invite code.
203 + var protocol = 'net:'
204 + if (opts.host.endsWith(".onion"))
205 + protocol = 'onion:'
206 + invite = protocol+opts.host+':'+opts.port+'~shs:'+opts.key.slice(1, -8)+':'+parts[1]
207 + }
208 + else
209 + modern = true
210 + }
211 +
212 + opts = ref.parseAddress(ref.parseInvite(invite).remote)
213 + function connect (cb) {
214 + ssbClient(null, {
215 + caps: config.caps,
216 + remote: invite,
217 + manifest: {invite: {use: 'async'}, getAddress: 'async'}
218 + }, cb)
219 + }
220 +
221 + // retry 3 times, with timeouts.
222 + // This is an UGLY hack to get the test/invite.js to pass
223 + // it's a race condition, I think because the server isn't ready
224 + // when it connects?
225 +
226 + function retry (fn, cb) {
227 + var n = 0
228 + ;(function next () {
229 + var start = Date.now()
230 + fn(function (err, value) {
231 + n++
232 + if(n >= 3) cb(err, value)
233 + else if(err) setTimeout(next, 500 + (Date.now()-start)*n)
234 + else cb(null, value)
235 + })
236 + })()
237 + }
238 +
239 + retry(connect, function (err, rpc) {
240 +
241 + if(err) return cb(explain(err, 'could not connect to server'))
242 +
243 + // command the peer to follow me
244 + rpc.invite.use({ feed: server.id }, function (err, msg) {
245 + if(err) return cb(explain(err, 'invite not accepted'))
246 +
247 + // follow and announce the pub
248 + cont.para([
249 + server.publish({
250 + type: 'contact',
251 + following: true,
252 + autofollow: true,
253 + contact: opts.key
254 + }),
255 + (
256 + opts.host
257 + ? server.publish({
258 + type: 'pub',
259 + address: opts
260 + })
261 + : function (cb) { cb() }
262 + )
263 + ])
264 + (function (err, results) {
265 + if(err) return cb(err)
266 + rpc.close()
267 + rpc.close()
268 + //ignore err if this is new style invite
269 + if(server.gossip) server.gossip.add(ref.parseInvite(invite).remote, 'seed')
270 + cb(null, results)
271 + })
272 + })
273 + })
274 + }, 'string')
275 + }
276 + }
277 +}
278 +
plugins/invite.mdView
@@ -1,0 +1,65 @@
1 +# ssb-server invite plugin
2 +
3 +Invite-token system, mainly used for pubs.
4 +
5 +
6 +## create: async
7 +
8 +Create a new invite code.
9 +
10 +```bash
11 +create {n} [{note}, {external}]
12 +```
13 +
14 +```js
15 +create(n[, note, external], cb)
16 +```
17 +
18 +This produces an invite-code which encodes the ssb-server instance's public address, and a keypair seed.
19 +The keypair seed is used to generate a keypair, which is then used to authenticate a connection with the ssb-server instance.
20 +The ssb-server instance will then grant access to the `use` call.
21 +
22 +- `n` (number): How many times the invite can be used before it expires.
23 +- `note` (string): A note to associate with the invite code. The ssb-server instance will
24 + include this note in the follow message that it creates when `use` is
25 + called.
26 +- `external` (string): An external hostname to use
27 +
28 +
29 +## accept: async
30 +
31 +Use an invite code.
32 +
33 +```bash
34 +accept {invitecode}
35 +```
36 +
37 +```js
38 +accept(invitecode, cb)
39 +```
40 +
41 +This connects to the server address encoded in the invite-code, then calls `use()` on the server.
42 +It will cause the server to follow the local user.
43 +
44 + - invitecode (string)
45 +
46 +
47 +## use: async
48 +
49 +Use an invite code created by this ssb-server instance (advanced function).
50 +
51 +```bash
52 +use --feed {feedid}
53 +```
54 +
55 +```js
56 +use({ feed: }, cb)
57 +```
58 +
59 +This commands the receiving server to follow the given feed.
60 +
61 +An invite-code encodes the ssb-server instance's address, and a keypair seed.
62 +The keypair seed must be used to generate a keypair, then authenticate a connection with the ssb-server instance, in order to use this function.
63 +
64 + - `feed` (feedid): The feed the server should follow.
65 +
plugins/local.jsView
@@ -1,0 +1,92 @@
1 +var broadcast = require('broadcast-stream')
2 +var ref = require('ssb-ref')
3 +// local plugin
4 +// broadcasts the address:port:pubkey triple of the ssb server
5 +// on the LAN, using multicast UDP
6 +
7 +function isFunction (f) {
8 + return 'function' === typeof f
9 +}
10 +
11 +function isEmpty (o) {
12 + for(var k in o)
13 + return false
14 + return true
15 +}
16 +
17 +/*
18 + idea: instead of broadcasting constantly,
19 + broadcast at startup, or when ip address changes (change networks)
20 + or when you receive a boardcast.
21 +
22 + this should use network more efficiently.
23 +*/
24 +
25 +module.exports = {
26 + name: 'local',
27 + version: '2.0.0',
28 + init: function init (ssbServer, config) {
29 + if(config.gossip && config.gossip.local === false)
30 + return {
31 + init: function () {
32 + delete this.init
33 + init(ssbServer, config)
34 + }
35 + }
36 +
37 + var local = broadcast(config.port)
38 + var addrs = {}
39 + var lastSeen = {}
40 +
41 + // cleanup old local peers
42 + setInterval(function () {
43 + Object.keys(lastSeen).forEach((key) => {
44 + if (Date.now() - lastSeen[key] > 10e3) {
45 + ssbServer.gossip.remove(addrs[key])
46 + delete lastSeen[key]
47 + }
48 + })
49 + }, 5e3)
50 +
51 + // discover new local peers
52 + local.on('data', function (buf) {
53 + if (buf.loopback) return
54 + var data = buf.toString()
55 + var peer = ref.parseAddress(data)
56 + if (peer && peer.key !== ssbServer.id) {
57 + addrs[peer.key] = peer
58 + lastSeen[peer.key] = Date.now()
59 + //note: add the raw data, not the parsed data.
60 + //so we still have the whole address, including protocol (eg, websockets)
61 + ssbServer.gossip.add(data, 'local')
62 + }
63 + })
64 +
65 + ssbServer.status.hook(function (fn) {
66 + var _status = fn()
67 + if(!isEmpty(addrs)) {
68 + _status.local = {}
69 + for(var k in addrs)
70 + _status.local[k] = {address: addrs[k], seen: lastSeen[k]}
71 + }
72 + return _status
73 + })
74 +
75 + setImmediate(function () {
76 + // broadcast self
77 + var int = setInterval(function () {
78 + if(config.gossip && config.gossip.local === false)
79 + return
80 + // TODO: sign beacons, so that receipient can be confidant
81 + // that is really your id.
82 + // (which means they can update their peer table)
83 + // Oh if this includes your local address,
84 + // then it becomes unforgeable.
85 + var addr = ssbServer.getAddress('private') || ssbServer.getAddress('local')
86 + if(addr) local.write(addr)
87 + }, 1000)
88 + if(int.unref) int.unref()
89 + })
90 + }
91 +}
92 +
plugins/logging.jsView
@@ -1,0 +1,78 @@
1 +var color = require('bash-color')
2 +
3 +// logging plugin
4 +// subscribes to 'log:*' events
5 +// and emits using lovely colors
6 +
7 +var LOG_LEVELS = [
8 + 'error',
9 + 'warning',
10 + 'notice',
11 + 'info'
12 +]
13 +var DEFAULT_LEVEL = LOG_LEVELS.indexOf('notice')
14 +
15 +function indent (o) {
16 + return o.split('\n').map(function (e) {
17 + return ' ' + e
18 + }).join('\n')
19 +}
20 +
21 +function isString(s) {
22 + return 'string' === s
23 +}
24 +
25 +function formatter(id, level) {
26 + var b = id.substring(0, 4)
27 + return function (ary) {
28 + var plug = ary[0].substring(0, 4).toUpperCase()
29 + var id = ary[1]
30 + var verb = ary[2]
31 + var data = ary.length > 4 ? ary.slice(3) : ary[3]
32 + var _data = (isString(data) ? data : JSON.stringify(data)) || ''
33 +
34 + var pre = [plug, id, color.cyan(verb)].join(' ')
35 + var length = (5 + pre.length + 1 + _data.length)
36 + var lines = isString(data) && data.split('\n').length > 1
37 +
38 + var c = process.stdout.columns
39 + if((process.stdout.columns > length) && !lines)
40 + console.log([level, b, pre, _data].join(' '))
41 + else {
42 + console.log([level, b, pre].join(' '))
43 + if(lines)
44 + console.log(indent(data))
45 + else if(data && data.stack)
46 + console.log(indent(data.stack))
47 + else if(data) {
48 + console.log(indent(JSON.stringify(data, null, 2)))
49 + }
50 + }
51 + }
52 +}
53 +
54 +module.exports = function logging (server, conf) {
55 + if (conf.logging && conf.logging.level) {
56 + level = LOG_LEVELS.indexOf(conf.logging.level)
57 + } else {
58 + level = DEFAULT_LEVEL
59 + }
60 +
61 + if (level === -1) {
62 + console.log('Warning, logging.level configured to an invalid value:', conf.logging.level)
63 + console.log('Should be one of:', LOG_LEVELS.join(', '))
64 + level = DEFAULT_LEVEL
65 + }
66 +
67 + var id = server.id
68 + if (level >= LOG_LEVELS.indexOf('info'))
69 + server.on('log:info', formatter(id, color.green('info')))
70 + if (level >= LOG_LEVELS.indexOf('notice'))
71 + server.on('log:notice', formatter(id, color.blue('note')))
72 + if (level >= LOG_LEVELS.indexOf('warning'))
73 + server.on('log:warning', formatter(id, color.yellow('warn')))
74 + if (level >= LOG_LEVELS.indexOf('error'))
75 + server.on('log:error', formatter(id, color.red('err!')))
76 +}
77 +
78 +module.exports.init = module.exports
plugins/master.jsView
@@ -1,0 +1,11 @@
1 +// master plugin
2 +// allows you to define "master" IDs in the config
3 +// which are given the full rights of the local main ID
4 +module.exports = function (api, opts) {
5 + var masters = [api.id].concat(opts.master).filter(Boolean)
6 + api.auth.hook(function (fn, args) {
7 + var id = args[0]
8 + var cb = args[1]
9 + cb(null, ~masters.indexOf(id) ? {allow: null, deny: null} : null)
10 + })
11 +}
plugins/no-auth.jsView
@@ -1,0 +1,17 @@
1 +
2 +exports.name = 'no-auth'
3 +exports.version = '1.0.0'
4 +exports.init = function (ssk, config) {
5 + var Noauth = require('multiserver/plugins/noauth')
6 +
7 + ssk.multiserver.transform({
8 + name: 'noauth',
9 + create: function () {
10 + return Noauth({
11 + keys: {
12 + publicKey: Buffer.from(config.keys.public, 'base64')
13 + }
14 + })
15 + }
16 + })
17 +}
plugins/onion.jsView
@@ -1,0 +1,12 @@
1 +exports.name = 'onion'
2 +exports.version = '1.0.0'
3 +exports.init = function (ssk, config) {
4 + var Onion = require('multiserver/plugins/onion')
5 +
6 + ssk.multiserver.transport({
7 + name: 'onion',
8 + create: function (conf) {
9 + return Onion(conf)
10 + }
11 + })
12 +}
plugins/plugins.jsView
@@ -1,0 +1,229 @@
1 +var assert = require('assert')
2 +var path = require('path')
3 +var fs = require('fs')
4 +var pull = require('pull-stream')
5 +var cat = require('pull-cat')
6 +var many = require('pull-many')
7 +var pushable = require('pull-pushable')
8 +var toPull = require('stream-to-pull-stream')
9 +var spawn = require('cross-spawn')
10 +var mkdirp = require('mkdirp')
11 +var osenv = require('osenv')
12 +var rimraf = require('rimraf')
13 +var mv = require('mv')
14 +var mdm = require('mdmanifest')
15 +var explain = require('explain-error')
16 +var valid = require('../lib/validators')
17 +var apidoc = require('../lib/apidocs').plugins
18 +
19 +module.exports = {
20 + name: 'plugins',
21 + version: '1.0.0',
22 + manifest: mdm.manifest(apidoc),
23 + permissions: {
24 + master: {allow: ['install', 'uninstall', 'enable', 'disable']}
25 + },
26 + init: function (server, config) {
27 + var installPath = config.path
28 + config.plugins = config.plugins || {}
29 + mkdirp.sync(path.join(installPath, 'node_modules'))
30 +
31 + // helper to enable/disable plugins
32 + function configPluginEnabled (b) {
33 + return function (pluginName, cb) {
34 + checkInstalled(pluginName, function (err) {
35 + if (err) return cb(err)
36 +
37 + config.plugins[pluginName] = b
38 + writePluginConfig(pluginName, b)
39 + if (b)
40 + cb(null, '\''+pluginName+'\' has been enabled. Restart ssb-server to use the plugin.')
41 + else
42 + cb(null, '\''+pluginName+'\' has been disabled. Restart ssb-server to stop using the plugin.')
43 + })
44 + }
45 + }
46 +
47 + // helper to check if a plugin is installed
48 + function checkInstalled (pluginName, cb) {
49 + if (!pluginName || typeof pluginName !== 'string')
50 + return cb(new Error('plugin name is required'))
51 + var modulePath = path.join(installPath, 'node_modules', pluginName)
52 + fs.stat(modulePath, function (err) {
53 + if (err)
54 + cb(new Error('Plugin "'+pluginName+'" is not installed.'))
55 + else
56 + cb()
57 + })
58 + }
59 +
60 + // write the plugin config to ~/.ssb/config
61 + function writePluginConfig (pluginName, value) {
62 + var cfgPath = path.join(config.path, 'config')
63 + // load ~/.ssb/config
64 + let existingConfig
65 + fs.readFile(cfgPath, 'utf-8', (err, data) => {
66 + if (err) {
67 + if (err.code === 'ENOENT') {
68 + // only catch "file not found"
69 + existingConfig = {}
70 + } else {
71 + throw err
72 + }
73 + } else {
74 + existingConfig = JSON.parse(data)
75 + }
76 +
77 +
78 + // update the plugins config
79 + existingConfig.plugins = existingConfig.plugins || {}
80 + existingConfig.plugins[pluginName] = value
81 +
82 + // write to disc
83 + fs.writeFileSync(cfgPath, JSON.stringify(existingConfig, null, 2), 'utf-8')
84 + })
85 +
86 + }
87 +
88 + return {
89 + install: valid.source(function (pluginName, opts) {
90 + var p = pushable()
91 + var dryRun = opts && opts['dry-run']
92 + var from = opts && opts.from
93 +
94 + if (!pluginName || typeof pluginName !== 'string')
95 + return pull.error(new Error('plugin name is required'))
96 +
97 + // pull out the version, if given
98 + if (pluginName.indexOf('@') !== -1) {
99 + var pluginNameSplitted = pluginName.split('@')
100 + pluginName = pluginNameSplitted[0]
101 + var version = pluginNameSplitted[1]
102 +
103 + if (version && !from)
104 + from = pluginName + '@' + version
105 + }
106 +
107 + if (!validatePluginName(pluginName))
108 + return pull.error(new Error('invalid plugin name: "'+pluginName+'"'))
109 +
110 + // create a tmp directory to install into
111 + var tmpInstallPath = path.join(osenv.tmpdir(), pluginName)
112 + rimraf.sync(tmpInstallPath); mkdirp.sync(tmpInstallPath)
113 +
114 + // build args
115 + // --global-style: dont dedup at the top level, gives proper isolation between each plugin
116 + // --loglevel error: dont output warnings, because npm just whines about the lack of a package.json in ~/.ssb
117 + var args = ['install', from||pluginName, '--global-style', '--loglevel', 'error']
118 + if (dryRun)
119 + args.push('--dry-run')
120 +
121 + // exec npm
122 + var child = spawn('npm', args, { cwd: tmpInstallPath })
123 + .on('close', function (code) {
124 + if (code == 0 && !dryRun) {
125 + var tmpInstallNMPath = path.join(tmpInstallPath, 'node_modules')
126 + var finalInstallNMPath = path.join(installPath, 'node_modules')
127 +
128 + // delete plugin, if it's already there
129 + rimraf.sync(path.join(finalInstallNMPath, pluginName))
130 +
131 + // move the plugin from the tmpdir into our install path
132 + // ...using our given plugin name
133 + var dirs = fs.readdirSync(tmpInstallNMPath)
134 + .filter(function (name) { return name.charAt(0) !== '.' }) // filter out dot dirs, like '.bin'
135 + mv(
136 + path.join(tmpInstallNMPath, dirs[0]),
137 + path.join(finalInstallNMPath, pluginName),
138 + function (err) {
139 + if (err)
140 + return p.end(explain(err, '"'+pluginName+'" failed to install. See log output above.'))
141 +
142 + // enable the plugin
143 + // - use basename(), because plugins can be installed from the FS, in which case pluginName is a path
144 + var name = path.basename(pluginName)
145 + config.plugins[name] = true
146 + writePluginConfig(name, true)
147 + p.push(Buffer.from('"'+pluginName+'" has been installed. Restart ssb-server to enable the plugin.\n', 'utf-8'))
148 + p.end()
149 + }
150 + )
151 + } else
152 + p.end(new Error('"'+pluginName+'" failed to install. See log output above.'))
153 + })
154 + return cat([
155 + pull.values([Buffer.from('Installing "'+pluginName+'"...\n', 'utf-8')]),
156 + many([toPull(child.stdout), toPull(child.stderr)]),
157 + p
158 + ])
159 + }, 'string', 'object?'),
160 + uninstall: valid.source(function (pluginName, opts) {
161 + var p = pushable()
162 + if (!pluginName || typeof pluginName !== 'string')
163 + return pull.error(new Error('plugin name is required'))
164 +
165 + var modulePath = path.join(installPath, 'node_modules', pluginName)
166 +
167 + rimraf(modulePath, function (err) {
168 + if (!err) {
169 + writePluginConfig(pluginName, false)
170 + p.push(Buffer.from('"'+pluginName+'" has been uninstalled. Restart ssb-server to disable the plugin.\n', 'utf-8'))
171 + p.end()
172 + } else
173 + p.end(err)
174 + })
175 + return p
176 + }, 'string', 'object?'),
177 + enable: valid.async(configPluginEnabled(true), 'string'),
178 + disable: valid.async(configPluginEnabled(false), 'string')
179 + }
180 + }
181 +}
182 +
183 +module.exports.loadUserPlugins = function (createSsbServer, config) {
184 + // iterate all modules
185 + var nodeModulesPath = path.join(config.path, 'node_modules')
186 + //instead of testing all plugins, only load things explicitly
187 + //enabled in the config
188 + for(var module_name in config.plugins) {
189 + if(config.plugins[module_name]) {
190 + var name = config.plugins[module_name]
191 + if(name === true)
192 + name = /^ssb-/.test(module_name) ? module_name.substring(4) : module_name
193 +
194 + if (createSsbServer.plugins.some(plug => plug.name === name))
195 + throw new Error('already loaded plugin named:'+name)
196 + var plugin = require(path.join(nodeModulesPath, module_name))
197 + if(!plugin || plugin.name !== name)
198 + throw new Error('plugin at:'+module_name+' expected name:'+name+' but had:'+(plugin||{}).name)
199 + assertSsbServerPlugin(plugin)
200 + createSsbServer.use(plugin)
201 + }
202 + }
203 +}
204 +
205 +// predictate to check if an object appears to be a ssbServer plugin
206 +function assertSsbServerPlugin (obj) {
207 + // function signature:
208 + if (typeof obj == 'function')
209 + return
210 +
211 + // object signature:
212 + assert(obj && typeof obj == 'object', 'module.exports must be an object')
213 + assert(typeof obj.name == 'string', 'module.exports.name must be a string')
214 + assert(typeof obj.version == 'string', 'module.exports.version must be a string')
215 + assert(obj.manifest &&
216 + typeof obj.manifest == 'object', 'module.exports.manifest must be an object')
217 + assert(typeof obj.init == 'function', 'module.exports.init must be a function')
218 +}
219 +
220 +function validatePluginName (name) {
221 + if (/^[._]/.test(name))
222 + return false
223 + // from npm-validate-package-name:
224 + if (encodeURIComponent(name) !== name)
225 + return false
226 + return true
227 +}
228 +
229 +
plugins/plugins.mdView
@@ -1,0 +1,68 @@
1 +# ssb-server plugins plugin
2 +
3 +Install and manage third-party plugins.
4 +
5 +
6 +
7 +## install: source
8 +
9 +Install a plugin to ssb-server.
10 +
11 +```bash
12 +install {nodeModule} [--from path]
13 +```
14 +```js
15 +install(nodeModule, { from: })
16 +```
17 +
18 +Calls out to npm to install a package into `~/.ssb/node_modules`.
19 +
20 + - nodeModule (string): The name of the plugin to install. Uses npm's module package-name rules.
21 + - from (string): A location to install from (directory path, url, or any location that npm accepts for its install command).
22 +
23 +
24 +
25 +## uninstall: source
26 +
27 +Uninstall a plugin from ssb-server.
28 +
29 +```bash
30 +uninstall {nodeModule}
31 +```
32 +```js
33 +uninstall(nodeModule)
34 +```
35 +
36 +Calls out to npm to uninstall a package into `~/.ssb/node_modules`.
37 +
38 + - nodeModule (string): The name of the plugin to uninstall.
39 +
40 +
41 +
42 +## enable: async
43 +
44 +Update the config to enable a plugin.
45 +
46 +```bash
47 +enable {nodeModule}
48 +```
49 +```js
50 +enable(nodeModule, cb)
51 +```
52 +
53 + - nodeModule (string): The name of the plugin to enable.
54 +
55 +
56 +
57 +## disable: async
58 +
59 +Update the config to disable a plugin.
60 +
61 +```bash
62 +disable {nodeModule}
63 +```
64 +```js
65 +disable(nodeModule, cb)
66 +```
67 +
68 + - nodeModule (string): The name of the plugin to disable.
plugins/replicate.mdView
@@ -1,0 +1,32 @@
1 +# ssb-server replicate plugin
2 +
3 +Sync feeds between peers.
4 +
5 +
6 +## changes: source
7 +
8 +Listen to replicate events.
9 +
10 +```bash
11 +changes
12 +```
13 +
14 +```js
15 +changes()
16 +```
17 +
18 +Emits events of the following form:
19 +
20 +```
21 +{ type: 'progress', peerid:, total:, progress:, feeds:, sync: }
22 +```
23 +
24 +## upto: source
25 +
26 +returns {} of feeds to replicate, with sequences
27 +
28 +## request: sync
29 +
30 +request a given feed, either as request(id) to replicate that feed,
31 +or request(id, false) to disable replication.
32 +
plugins/replicate/index.jsView
@@ -1,0 +1,34 @@
1 +'use strict'
2 +
3 +var Legacy = require('./legacy')
4 +var mdm = require('mdmanifest')
5 +var apidoc = require('../../lib/apidocs').replicate
6 +var Notify = require('pull-notify')
7 +var pull = require('pull-stream')
8 +
9 +module.exports = {
10 + name: 'replicate',
11 + version: '2.0.0',
12 + manifest: mdm.manifest(apidoc),
13 + //replicate: replicate,
14 + init: function (ssbServer, config) {
15 + var notify = Notify(), upto
16 + if(!config.replicate || config.replicate.legacy !== false) {
17 + var replicate = Legacy.call(this, ssbServer, notify, config)
18 +
19 + // replication policy is set by calling
20 + // ssbServer.replicate.request(id)
21 + // or by cancelling replication
22 + // ssbServer.replicate.request(id, false)
23 + // this is currently performed from the ssb-friends plugin
24 +
25 + return replicate
26 + }
27 + else
28 + return {
29 + request: function () {},
30 + changes: function () { return function (abort, cb) { cb(true) } }
31 + }
32 + }
33 +}
34 +
plugins/replicate/legacy.jsView
@@ -1,0 +1,354 @@
1 +var pull = require('pull-stream')
2 +var pullNext = require('pull-next')
3 +var para = require('pull-paramap')
4 +var Notify = require('pull-notify')
5 +var Cat = require('pull-cat')
6 +var Debounce = require('observ-debounce')
7 +var deepEqual = require('deep-equal')
8 +var Obv = require('obv')
9 +var isFeed = require('ssb-ref').isFeed
10 +var Pushable = require('pull-pushable')
11 +var detectSync = require('../../lib/detect-sync')
12 +
13 +// compatibility function for old implementations of `latestSequence`
14 +function toSeq (s) {
15 + return 'number' === typeof s ? s : s.sequence
16 +}
17 +
18 +function last (a) { return a[a.length - 1] }
19 +
20 +// if one of these shows up in a replication stream, the stream is dead
21 +var streamErrors = {
22 + 'unexpected end of parent stream': true, // stream closed okay
23 + 'unexpected hangup': true, // stream closed probably okay
24 + 'read EHOSTUNREACH': true,
25 + 'read ECONNRESET': true,
26 + 'read ENETDOWN': true,
27 + 'read ETIMEDOUT': true,
28 + 'write ECONNRESET': true,
29 + 'write EPIPE': true,
30 + 'stream is closed': true, // rpc method called after stream ended
31 +}
32 +
33 +module.exports = function (ssbServer, notify, config) {
34 + var debounce = Debounce(200)
35 + var listeners = {}
36 + var newPeers = Notify()
37 +
38 + var start = null
39 + var count = 0
40 + var rate = 0
41 + var toSend = {}
42 + var peerHas = {}
43 + var pendingFeedsForPeer = {}
44 + var lastProgress = null
45 +
46 + var replicate = {}
47 +
48 + function request (id, unfollow) {
49 + if(unfollow === false) {
50 + if(replicate[id]) {
51 + delete replicate[id]
52 + newPeers({id:id, sequence: -1})
53 + }
54 + }
55 + else if(!replicate[id]) {
56 + replicate[id] = true
57 + newPeers({id:id, sequence: toSend[id] || 0})
58 + }
59 + }
60 +
61 + ssbServer.getVectorClock(function (err, clock) {
62 + if(err) throw err
63 + toSend = clock
64 + })
65 +
66 + ssbServer.post(function (msg) {
67 + //this should be part of ssb.getVectorClock
68 + toSend[msg.value.author] = msg.value.sequence
69 + debounce.set()
70 + })
71 +
72 + debounce(function () {
73 + // only list loaded feeds once we know about all of them!
74 + var feeds = Object.keys(toSend).length
75 + var legacyProgress = 0
76 + var legacyTotal = 0
77 +
78 + var pendingFeeds = new Set()
79 + var pendingPeers = {}
80 + var legacyToRecv = {}
81 +
82 + Object.keys(pendingFeedsForPeer).forEach(function (peerId) {
83 + if (pendingFeedsForPeer[peerId] && pendingFeedsForPeer[peerId].size) {
84 + Object.keys(toSend).forEach(function (feedId) {
85 + if (peerHas[peerId] && peerHas[peerId][feedId]) {
86 + if (peerHas[peerId][feedId] > toSend[feedId]) {
87 + pendingFeeds.add(feedId)
88 + }
89 + }
90 + })
91 + pendingPeers[peerId] = pendingFeedsForPeer[peerId].size
92 + }
93 + })
94 +
95 + for (var k in toSend) {
96 + legacyProgress += toSend[k]
97 + }
98 +
99 + for (var id in peerHas) {
100 + for (var k in peerHas[id]) {
101 + legacyToRecv[k] = Math.max(peerHas[id][k], legacyToRecv[k] || 0)
102 + }
103 + }
104 +
105 + for (var k in legacyToRecv) {
106 + if (toSend[k] !== null) {
107 + legacyTotal += legacyToRecv[k]
108 + }
109 + }
110 +
111 + var progress = {
112 + id: ssbServer.id,
113 + rate, // rate of messages written to ssbServer
114 + feeds, // total number of feeds we want to replicate
115 + pendingPeers, // number of pending feeds per peer
116 + incompleteFeeds: pendingFeeds.size, // number of feeds with pending messages to download
117 +
118 + // LEGACY: Preserving old api. Needed for test/random.js to pass
119 + progress: legacyProgress,
120 + total: legacyTotal
121 + }
122 +
123 + if (!deepEqual(progress, lastProgress)) {
124 + lastProgress = progress
125 + notify(progress)
126 + }
127 + })
128 +
129 + pull(
130 + ssbServer.createLogStream({old: false, live: true, sync: false, keys: false}),
131 + pull.drain(function (e) {
132 + //track writes per second, mainly used for developing initial sync.
133 + if(!start) start = Date.now()
134 + var time = (Date.now() - start)/1000
135 + if(time >= 1) {
136 + rate = count / time
137 + start = Date.now()
138 + count = 0
139 + }
140 + var pushable = listeners[e.author]
141 +
142 + if(pushable && pushable.sequence == e.sequence) {
143 + pushable.sequence ++
144 + pushable.forEach(function (p) {
145 + p.push(e)
146 + })
147 + }
148 + count ++
149 + })
150 + )
151 +
152 + var chs = ssbServer.createHistoryStream
153 +
154 + ssbServer.createHistoryStream.hook(function (fn, args) {
155 + var upto = args[0] || {}
156 + var seq = upto.sequence || upto.seq
157 + if(this._emit) this._emit('call:createHistoryStream', args[0])
158 +
159 + //if we are calling this locally, skip cleverness
160 + if(this===ssbServer) return fn.call(this, upto)
161 +
162 + // keep track of each requested value, per feed / per peer.
163 + peerHas[this.id] = peerHas[this.id] || {}
164 + peerHas[this.id][upto.id] = seq - 1 // peer requests +1 from actual last seq
165 +
166 + debounce.set()
167 +
168 + //handle creating lots of history streams efficiently.
169 + //maybe this could be optimized in map-filter-reduce queries instead?
170 + if(toSend[upto.id] == null || (seq > toSend[upto.id])) {
171 + upto.old = false
172 + if(!upto.live) return pull.empty()
173 + var pushable = listeners[upto.id] = listeners[upto.id] || []
174 + var p = Pushable(function () {
175 + var i = pushable.indexOf(p)
176 + pushable.splice(i, 1)
177 + })
178 + pushable.push(p)
179 + pushable.sequence = seq
180 + return p
181 + }
182 + return fn.call(this, upto)
183 + })
184 +
185 + // collect the IDs of feeds we want to request
186 + var opts = config.replication || {}
187 + opts.hops = opts.hops || 3
188 + opts.dunbar = opts.dunbar || 150
189 + opts.live = true
190 + opts.meta = true
191 +
192 + //XXX policy about replicating specific peers should be outside
193 + //of this plugin.
194 + function localPeers () {
195 + if(!ssbServer.gossip) return
196 + ssbServer.gossip.peers().forEach(function (e) {
197 + if (e.source === 'local')
198 + request(e.key)
199 + })
200 + }
201 +
202 + //also request local peers.
203 + if (ssbServer.gossip) {
204 + // if we have the gossip plugin active, then include new local peers
205 + // so that you can put a name to someone on your local network.
206 + var int = setInterval(localPeers, 1000)
207 + if(int.unref) int.unref()
208 + localPeers()
209 + }
210 + //XXX ^
211 +
212 + function upto (opts) {
213 + opts = opts || {}
214 + var ary = Object.keys(replicate).map(function (k) {
215 + return { id: k, sequence: toSend[k]||0 }
216 + })
217 + if(opts.live)
218 + return Cat([
219 + pull.values(ary),
220 + pull.once({sync: true}),
221 + newPeers.listen()
222 + ])
223 +
224 + return pull.values(ary)
225 + }
226 +
227 + ssbServer.on('rpc:connect', function(rpc) {
228 + // this is the cli client, just ignore.
229 + if(rpc.id === ssbServer.id) return
230 + if (!ssbServer.ready()) return
231 +
232 + var errorsSeen = {}
233 + //check for local peers, or manual connections.
234 + localPeers()
235 +
236 + var drain
237 +
238 + function replicate(upto, cb) {
239 + pendingFeedsForPeer[rpc.id] = pendingFeedsForPeer[rpc.id] || new Set()
240 + pendingFeedsForPeer[rpc.id].add(upto.id)
241 +
242 + debounce.set()
243 +
244 + var sync = false
245 +
246 + pull(
247 + rpc.createHistoryStream({
248 + id: upto.id,
249 + seq: (upto.sequence || upto.seq || 0) + 1,
250 + live: true,
251 + keys: false
252 + }),
253 +
254 + pull.through(detectSync(rpc.id, upto, toSend, peerHas, function () {
255 + sync = true
256 + if (pendingFeedsForPeer[rpc.id]) {
257 + // this peer has finished syncing, remove from progress
258 + pendingFeedsForPeer[rpc.id].delete(upto.id)
259 + debounce.set()
260 + }
261 + })),
262 +
263 + ssbServer.createWriteStream(function (err) {
264 + if(err && !(err.message in errorsSeen)) {
265 + errorsSeen[err.message] = true
266 + if(err.message in streamErrors) {
267 + cb && cb(err)
268 + if(err.message === 'unexpected end of parent stream') {
269 + if (err instanceof Error) {
270 + // stream closed okay locally
271 + } else {
272 + // pre-emptively destroy the stream, assuming the other
273 + // end is packet-stream 2.0.0 sending end messages.
274 + rpc.close(err)
275 + }
276 + }
277 + } else {
278 + console.error(
279 + 'Error replicating with ' + rpc.id + ':\n ',
280 + err.stack
281 + )
282 + }
283 + }
284 +
285 + // if stream closes, remove from pending progress
286 + if (pendingFeedsForPeer[rpc.id]) {
287 + pendingFeedsForPeer[rpc.id].delete(upto.id)
288 + debounce.set()
289 + }
290 + })
291 + )
292 + }
293 +
294 + var replicate_self = false
295 + //if replicate.fallback is enabled
296 + //then wait for the fallback event before
297 + //starting to replicate by this strategy.
298 + if(config.replicate && config.replicate.fallback)
299 + rpc.once('fallback:replicate', fallback)
300 + else
301 + fallback()
302 +
303 + function fallback () {
304 + //if we are not configured to use EBT, then fallback to createHistoryStream
305 + if(replicate_self) return
306 + replicate_self = true
307 + replicate({id: ssbServer.id, sequence: toSend[ssbServer.id] || 0})
308 + }
309 +
310 + //trigger this if ebt.replicate fails...
311 + rpc.once('call:createHistoryStream', next)
312 +
313 + var started = false
314 + function next () {
315 + if(started) return
316 + started = true
317 + ssbServer.emit('replicate:start', rpc)
318 +
319 + rpc.on('closed', function () {
320 + ssbServer.emit('replicate:finish', toSend)
321 +
322 + // if we disconnect from a peer, remove it from sync progress
323 + delete pendingFeedsForPeer[rpc.id]
324 + debounce.set()
325 + })
326 +
327 + //make sure we wait until the clock is loaded
328 + pull(
329 + upto({live: opts.live}),
330 + drain = pull.drain(function (upto) {
331 + if(upto.sync) return
332 + if(!isFeed(upto.id)) throw new Error('expected feed!')
333 + if(!Number.isInteger(upto.sequence)) throw new Error('expected sequence!')
334 +
335 + if(upto.id == ssbServer.id && replicate_self) return replicate_self = true
336 + replicate(upto, function (err) {
337 + drain.abort()
338 + })
339 + }, function (err) {
340 + if(err && err !== true)
341 + ssbServer.emit('log:error', ['replication', rpc.id, 'error', err])
342 + })
343 + )
344 +
345 + }
346 + })
347 +
348 + return {
349 + request: request,
350 + upto: upto,
351 + changes: notify.listen
352 + }
353 +}
354 +
plugins/unix-socket.jsView
@@ -1,0 +1,12 @@
1 +
2 +exports.name = 'unix-socket'
3 +exports.version = '1.0.0'
4 +exports.init = function (ssk, config) {
5 + var Unix = require('multiserver/plugins/unix-socket')
6 + ssk.multiserver.transport({
7 + name: 'unix',
8 + create: function (conf) {
9 + return Unix(Object.assign(Object.assign({}, conf), config))
10 + }
11 + })
12 +}

Built with git-ssb-web