Commit d2b5693b01842e3376dae3c3cf5c579e03e490c4
added initial localhost landing page
austinfrey committed on 4/9/2019, 2:09:38 AMParent: bea0d6f86662e5db7348deb37bef697f1226edae
Files changed
bin.js | ||
---|---|---|
@@ -1,5 +1,6 @@ | ||
1 | 1 … | var fs = require('fs') |
2 … | +var file = require('pull-file') | |
2 | 3 … | var path = require('path') |
3 | 4 … | var ssbKeys = require('ssb-keys') |
4 | 5 … | var stringify = require('pull-stringify') |
5 | 6 … | var open = require('open') |
@@ -7,8 +8,9 @@ | ||
7 | 8 … | var nonPrivate = require('non-private-ip') |
8 | 9 … | var muxrpcli = require('muxrpcli') |
9 | 10 … | var {pull, values, once} = require('pull-stream') |
10 | 11 … | var toPull = require('stream-to-pull-stream') |
12 … | +var through = require('pull-through') | |
11 | 13 … | const webresolve = require('ssb-web-resolver') |
12 | 14 … | |
13 | 15 … | var SEC = 1e3 |
14 | 16 … | var MIN = 60*SEC |
@@ -19,9 +21,8 @@ | ||
19 | 21 … | var urlIdRegex = /^(?:\/(([%&@]|%25|%26|%40)(?:[A-Za-z0-9\/+]|%2[Ff]|%2[Bb]){43}(?:=|%3[Dd])\.(?:sha256|ed25519))(?:\.([^?]*))?|(\/.*?))(?:\?(.*))?$/ |
20 | 22 … | |
21 | 23 … | config.keys = ssbKeys.loadOrCreateSync(path.join(config.path, 'secret')) |
22 | 24 … | |
23 | -var mvdClient = fs.readFileSync(path.join('./build/index.html')) | |
24 | 25 … | var favicon = fs.readFileSync(path.join('./public/favicon.ico')) |
25 | 26 … | |
26 | 27 … | var manifestFile = path.join(config.path, 'manifest.json') |
27 | 28 … | |
@@ -31,54 +32,61 @@ | ||
31 | 32 … | argv = ~i ? argv.slice(0, i) : argv |
32 | 33 … | |
33 | 34 … | if (argv[0] == 'start') { |
34 | 35 … | |
35 | - var createSbot = require('ssb-server') | |
36 … | + var createSbot = require('./') | |
36 | 37 … | |
37 | 38 … | createSbot |
38 | - .use(require('ssb-server/plugins/master')) | |
39 … | + .use(require('./plugins/master')) | |
40 … | + .use(require('./plugins/local')) | |
39 | 41 … | .use(require('ssb-replicate')) |
42 … | + .use(require('ssb-invite')) | |
40 | 43 … | .use(require('ssb-friends')) |
41 | 44 … | .use(require('ssb-gossip')) |
42 | 45 … | .use(require('ssb-blobs')) |
43 | 46 … | .use(require('ssb-backlinks')) |
44 | 47 … | .use(require('ssb-query')) |
45 | 48 … | .use(require('ssb-links')) |
46 | 49 … | .use(require('ssb-ebt')) |
47 | 50 … | .use(require('ssb-search')) |
48 | - .use(require('ssb-server/plugins/local')) | |
49 | 51 … | .use(require('ssb-ws')) |
50 | 52 … | .use({ |
51 | 53 … | name: 'serve', |
52 | 54 … | version: '1.0.0', |
53 | 55 … | init: function (sbot) { |
54 | - console.log(sbot.getAddress()) | |
55 | 56 … | sbot.ws.use(function (req, res, next) { |
56 | 57 … | var send = config |
57 | 58 … | |
58 | 59 … | delete send.keys // very important to keep this, as it removes the server keys from the config before broadcast |
59 | 60 … | |
60 | 61 … | send.address = 'ws://100.115.92.2:8989~shs:VelntasZy86CuIihzSpkzPvIOYgyu3FO3NZww/UOirk=' |
61 | 62 … | |
62 | - //sbot.invite.create({modern: true}, function (err, cb) { | |
63 | - // send.invite = cb | |
64 | - //}) | |
65 | - | |
66 | 63 … | var m = urlIdRegex.exec(req.url) |
67 | 64 … | |
65 … | + function onError(err) { | |
66 … | + if (err) console.error('[viewer]', err) | |
67 … | + } | |
68 … | + | |
68 | 69 … | 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 … | + ) | |
71 | 81 … | } |
72 | 82 … | if(req.url.startsWith('/web/')) { |
73 | 83 … | return serveWeb(req, res, m[4]) |
74 | 84 … | } |
75 | 85 … | if(req.url == '/get-config') { |
76 | - console.log('/get-config') | |
77 | 86 … | return res.end(JSON.stringify(send)) |
78 | 87 … | } |
79 | 88 … | if(req.url == '/favicon.ico') { |
80 | - console.log('/favicon') | |
81 | 89 … | return res.end(favicon) |
82 | 90 … | } else next() |
83 | 91 … | |
84 | 92 … | function respond(res, status, message) { |
@@ -95,23 +103,19 @@ | ||
95 | 103 … | if (components[0] === 'web') components.shift() |
96 | 104 … | components[0] = decodeURIComponent(components[0]) |
97 | 105 … | |
98 | 106 … | webresolve(sbot, components, function (err, data) { |
99 | - console.log(err) | |
100 | 107 … | if (err) return respond(res, 404, 'ERROR: ' + err) |
101 | 108 … | |
102 | - function onError(err) { | |
103 | - if (err) console.error('[viewer]', err) | |
104 | - } | |
105 | 109 … | |
106 | 110 … | return pull(once(data), toPull(res)) |
107 | 111 … | }) |
108 | 112 … | } |
109 | 113 … | }) |
110 | 114 … | } |
111 | 115 … | }) |
112 | 116 … | |
113 | - // open('http://localhost:' + config.ws.port, {wait: false}) | |
117 … | + open('http://100.115.92.2:' + config.ws.port, {wait: false}) | |
114 | 118 … | |
115 | 119 … | // start server |
116 | 120 … | var server = createSbot(config) |
117 | 121 … |
package-lock.json | ||
---|---|---|
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.json | ||
---|---|---|
@@ -7,17 +7,19 @@ | ||
7 | 7 … | "start": "node bin server", |
8 | 8 … | "decent": "node bin server --appname=decent", |
9 | 9 … | "ssb": "node bin server --appname=ssb", |
10 | 10 … | "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" | |
12 | 13 … | }, |
13 | 14 … | "devDependencies": { |
14 | 15 … | "browserify": "^16.2.2", |
15 | 16 … | "indexhtmlify": "^1.3.1" |
16 | 17 … | }, |
17 | 18 … | "author": "Ev Bogue <ev@evbogue.com>", |
18 | 19 … | "license": "MIT", |
19 | 20 … | "dependencies": { |
21 … | + "broadcast-stream": "^0.2.2", | |
20 | 22 … | "chloride": "^2.2.14", |
21 | 23 … | "dataurl-": "^0.1.0", |
22 | 24 … | "deep-extend": "^0.6.0", |
23 | 25 … | "diff": "^3.5.0", |
@@ -26,26 +28,33 @@ | ||
26 | 28 … | "hyperfile": "^2.0.0", |
27 | 29 … | "hyperloadmore": "^1.1.0", |
28 | 30 … | "hyperscript": "^2.0.2", |
29 | 31 … | "hyperscroll": "^1.0.0", |
32 … | + "inu-engine": "^1.0.0-pre.0", | |
30 | 33 … | "multiblob-http": "^0.4.2", |
31 | 34 … | "muxrpcli": "^1.1.0", |
35 … | + "nanomorph": "^5.4.0", | |
32 | 36 … | "non-private-ip": "^1.4.3", |
33 | 37 … | "open": "^6.1.0", |
34 | 38 … | "os-homedir": "^1.0.2", |
35 | 39 … | "patch-package": "^6.1.0", |
40 … | + "pull-file": "^1.1.0", | |
36 | 41 … | "pull-more": "^1.1.0", |
37 | 42 … | "pull-next-query": "^1.0.0", |
38 | 43 … | "pull-reconnect": "0.0.3", |
39 | 44 … | "pull-stream": "^3.6.9", |
40 | 45 … | "pull-stringify": "^2.0.0", |
46 … | + "pull-through": "^1.0.18", | |
41 | 47 … | "rc": "^1.2.7", |
48 … | + "secret-stack": "^6.0.3", | |
49 … | + "sheetify": "^7.3.3", | |
42 | 50 … | "simple-mime": "^0.1.0", |
43 | 51 … | "split-buffer": "^1.0.0", |
44 | 52 … | "ssb-avatar": "^0.2.0", |
45 | 53 … | "ssb-backlinks": "^0.7.1", |
46 | 54 … | "ssb-blobs": "^1.1.5", |
47 | 55 … | "ssb-client": "^4.5.7", |
56 … | + "ssb-db": "^19.1.1", | |
48 | 57 … | "ssb-ebt": "^5.1.5", |
49 | 58 … | "ssb-feed": "^2.3.0", |
50 | 59 … | "ssb-friends": "^3.1.6", |
51 | 60 … | "ssb-gossip": "^1.0.6", |
@@ -57,13 +66,12 @@ | ||
57 | 66 … | "ssb-query": "^2.3.0", |
58 | 67 … | "ssb-ref": "^2.11.1", |
59 | 68 … | "ssb-replicate": "^1.2.3", |
60 | 69 … | "ssb-search": "^1.0.1", |
61 | - "ssb-server": "^13.6.3", | |
62 | - "ssb-viewer": "^1.0.0", | |
63 | 70 … | "ssb-web-resolver": "^1.1.2", |
64 | 71 … | "ssb-ws": "^6.0.0", |
65 | 72 … | "stack": "^0.1.0", |
66 | 73 … | "stream-to-pull-stream": "^1.7.3", |
74 … | + "tachyons": "^4.11.1", | |
67 | 75 … | "visualize-buffer": "0.0.1" |
68 | 76 … | } |
69 | 77 … | } |
config.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.css | ||
---|---|---|
@@ -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.json | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.css | ||
---|---|---|
@@ -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.json | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.md | ||
---|---|---|
@@ -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.md | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.md | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.md | ||
---|---|---|
@@ -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.md | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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