Commit 1c2359030ac279f1fdfef805d7938f3003372c24
minimal stats page
mix irving committed on 4/17/2018, 12:16:28 AMParent: 856796486d48c7b494769019a74b9b843471ee41
Files changed
app/html/header.js | changed |
app/index.js | changed |
app/page/statsShow.js | added |
background-process.js | changed |
blog/sync/isBlog.js | changed |
package-lock.json | changed |
package.json | changed |
router/sync/routes.js | changed |
ssb-server-blog-stats.js | added |
app/html/header.js | ||
---|---|---|
@@ -38,8 +38,11 @@ | ||
38 | 38 | }), |
39 | 39 | h('img.settings', { |
40 | 40 | src: when(isSettings, assetPath('settings_on.png'), assetPath('settings.png')), |
41 | 41 | 'ev-click': () => push({page: 'settings'}) |
42 | + }), | |
43 | + h('i.fa.fa-bell', { | |
44 | + 'ev-click': () => push({page: 'statsShow'}) | |
42 | 45 | }) |
43 | 46 | ]) |
44 | 47 | ]) |
45 | 48 | }) |
app/index.js | ||
---|---|---|
@@ -46,8 +46,9 @@ | ||
46 | 46 | userEdit: require('./page/userEdit'), |
47 | 47 | // userFind: require('./page/userFind'), |
48 | 48 | userShow: require('./page/userShow'), |
49 | 49 | splash: require('./page/splash'), |
50 | + statsShow: require('./page/statsShow'), | |
50 | 51 | threadNew: require('./page/threadNew'), |
51 | 52 | threadShow: require('./page/threadShow') |
52 | 53 | }, |
53 | 54 | sync: { |
app/page/statsShow.js | ||
---|---|---|
@@ -1,0 +1,90 @@ | ||
1 | +const nest = require('depnest') | |
2 | +const { h, Array: MutantArray, Dict, onceTrue, map } = require('mutant') | |
3 | +const pull = require('pull-stream') | |
4 | + | |
5 | +exports.gives = nest('app.page.statsShow') | |
6 | + | |
7 | +exports.needs = nest({ | |
8 | + 'sbot.obs.connection': 'first' | |
9 | +}) | |
10 | + | |
11 | +exports.create = (api) => { | |
12 | + return nest('app.page.statsShow', statsShow) | |
13 | + | |
14 | + function statsShow (location) { | |
15 | + var blogs = MutantArray([]) | |
16 | + var comments = Dict() | |
17 | + var likes = Dict() | |
18 | + // comments(console.log) | |
19 | + | |
20 | + onceTrue(api.sbot.obs.connection, server => { | |
21 | + // console.log(Object.keys(server.blogStats)) | |
22 | + | |
23 | + fetchBlogs() | |
24 | + | |
25 | + function fetchBlogs () { | |
26 | + pull( | |
27 | + server.blogStats.readBlogs({ reverse: false }), | |
28 | + pull.drain(blog => { | |
29 | + blogs.push(blog) | |
30 | + fetchComments(blog) | |
31 | + fetchLikes(blog) | |
32 | + }) | |
33 | + ) | |
34 | + } | |
35 | + | |
36 | + function fetchComments (blog) { | |
37 | + if (!comments.has(blog.key)) comments.put(blog.key, MutantArray()) | |
38 | + | |
39 | + pull( | |
40 | + server.blogStats.readComments({ blog }), | |
41 | + pull.drain(comment => { | |
42 | + comments.get(blog.key).push(comment) | |
43 | + }) | |
44 | + ) | |
45 | + } | |
46 | + | |
47 | + function fetchLikes (blog) { | |
48 | + if (!likes.has(blog.key)) likes.put(blog.key, MutantArray()) | |
49 | + | |
50 | + pull( | |
51 | + server.blogStats.readLikes({ blog }), | |
52 | + pull.drain(comment => { | |
53 | + likes.get(blog.key).push(comment) | |
54 | + }) | |
55 | + ) | |
56 | + } | |
57 | + | |
58 | + // ///// test code ///// | |
59 | + var blogKey = '%3JeEg7voZF4aplk9xCEAfhFOx+zocbKhgstzvfD3G8w=.sha256' | |
60 | + // console.log('fetching comments', blogKey) // has 2 comments, 1 like | |
61 | + | |
62 | + pull( | |
63 | + server.blogStats.read({ | |
64 | + gt: ['L', blogKey, null], | |
65 | + lt: ['L', blogKey+'~', undefined], | |
66 | + // gt: ['L', blogKey, null], | |
67 | + // lte: ['L', blogKey+'~', undefined], | |
68 | + // limit: 100, | |
69 | + keys: true, | |
70 | + values: true, | |
71 | + seqs: false, | |
72 | + reverse: true | |
73 | + }), | |
74 | + // pull.filter(o => o.key[1] === blogKey), | |
75 | + pull.log(() => console.log('DONE')) | |
76 | + ) | |
77 | + /// ///// test code ///// | |
78 | + }) | |
79 | + | |
80 | + return h('Page -statsShow', [ | |
81 | + h('pre', map(blogs, blog => { | |
82 | + return h('div', [ | |
83 | + h('b', blog.value.content.title), | |
84 | + h('div', map(comments.get(blog.key), msg => 'C')), | |
85 | + h('div', map(likes.get(blog.key), msg => 'L')) | |
86 | + ]) | |
87 | + })) | |
88 | + ]) | |
89 | + } | |
90 | +} |
background-process.js | ||
---|---|---|
@@ -21,23 +21,21 @@ | ||
21 | 21 | .use(require('ssb-about')) |
22 | 22 | // .use(require('ssb-ebt')) |
23 | 23 | .use(require('ssb-ws')) |
24 | 24 | .use(require('ssb-server-channel')) |
25 | + .use(require('./ssb-server-blog-stats')) | |
25 | 26 | |
26 | 27 | Client(config.keys, config, (err, ssbServer) => { |
27 | - if (ssbServer === undefined) { | |
28 | + if (err) { | |
28 | 29 | console.log('> starting sbot') |
29 | 30 | var sbot = createSbot(config) |
30 | 31 | |
31 | 32 | console.log(' > updating updating manifest.json') |
32 | 33 | var manifest = sbot.getManifest() |
33 | 34 | fs.writeFileSync(Path.join(config.path, 'manifest.json'), JSON.stringify(manifest)) |
34 | 35 | electron.ipcRenderer.send('server-started') |
35 | - } | |
36 | - else { | |
36 | + } else { | |
37 | 37 | console.log('> sbot running elsewhere') |
38 | 38 | electron.ipcRenderer.send('server-started') |
39 | 39 | // TODO send some warning to the client side |
40 | 40 | } |
41 | 41 | }) |
42 | - | |
43 | - |
blog/sync/isBlog.js | ||
---|---|---|
@@ -1,6 +1,7 @@ | ||
1 | 1 | const nest = require('depnest') |
2 | 2 | const get = require('lodash/get') |
3 | +const isBlog = require('scuttle-blog/isBlog') | |
3 | 4 | |
4 | 5 | exports.gives = nest({ |
5 | 6 | 'blog.sync.isBlog': true, |
6 | 7 | }) |
@@ -8,14 +9,15 @@ | ||
8 | 9 | const MIN_LENGTH_FOR_BLOG_POST = 800 |
9 | 10 | |
10 | 11 | exports.create = function (api) { |
11 | 12 | return nest({ |
12 | - 'blog.sync.isBlog': isBlog | |
13 | + 'blog.sync.isBlog': isBloggy | |
13 | 14 | }) |
14 | 15 | |
15 | - function isBlog (msg) { | |
16 | + function isBloggy (msg) { | |
17 | + if (isBlog(msg)) return true | |
18 | + | |
16 | 19 | const type = msg.value.content.type |
17 | - if (type === 'blog') return true | |
18 | 20 | if (type === 'post' && get(msg, 'value.content.text', '').length > MIN_LENGTH_FOR_BLOG_POST) return true |
19 | 21 | return false |
20 | 22 | } |
21 | 23 | } |
package-lock.json | ||
---|---|---|
The diff is too large to show. Use a local git client to view these changes. Old file size: 238763 bytes New file size: 248886 bytes |
package.json | ||
---|---|---|
@@ -25,8 +25,9 @@ | ||
25 | 25 | "depject": "^4.1.1", |
26 | 26 | "depnest": "^1.3.0", |
27 | 27 | "electron-default-menu": "^1.0.1", |
28 | 28 | "electron-window-state": "^4.1.1", |
29 | + "flumeview-level": "^3.0.2", | |
29 | 30 | "font-awesome": "^4.7.0", |
30 | 31 | "html-escape": "^2.0.0", |
31 | 32 | "human-time": "0.0.1", |
32 | 33 | "hyper-nav": "^2.0.0", |
@@ -52,8 +53,9 @@ | ||
52 | 53 | "pull-obv": "^1.3.0", |
53 | 54 | "pull-stream": "^3.6.0", |
54 | 55 | "read-directory": "^2.1.0", |
55 | 56 | "require-style": "^1.0.1", |
57 | + "scuttle-blog": "^1.0.0", | |
56 | 58 | "scuttlebot": "10.4.10", |
57 | 59 | "secret-stack": "4.0.1", |
58 | 60 | "setimmediate": "^1.0.5", |
59 | 61 | "ssb-about": "^0.1.0", |
router/sync/routes.js | ||
---|---|---|
@@ -23,8 +23,9 @@ | ||
23 | 23 | 'app.page.userEdit': 'first', |
24 | 24 | // 'app.page.userFind': 'first', |
25 | 25 | 'app.page.userShow': 'first', |
26 | 26 | 'app.page.splash': 'first', |
27 | + 'app.page.statsShow': 'first', | |
27 | 28 | 'app.page.threadNew': 'first', |
28 | 29 | 'app.page.threadShow': 'first', |
29 | 30 | // 'app.page.image': 'first', |
30 | 31 | 'blob.sync.url': 'first' |
@@ -50,12 +51,15 @@ | ||
50 | 51 | !get(location, 'value.private') // treats public posts as 'blogs' |
51 | 52 | }, pages.blogShow ], |
52 | 53 | |
53 | 54 | // Channel related pages |
54 | - [ location => location.page === 'channelSubscriptions', pages.channelSubscriptions], | |
55 | + [ location => location.page === 'channelSubscriptions', pages.channelSubscriptions ], | |
55 | 56 | [ location => location.page === 'channelShow', pages.channelShow ], |
56 | 57 | [ location => location.channel, pages.channelShow ], |
57 | 58 | |
59 | + // Stats pages | |
60 | + [ location => location.page === 'statsShow', pages.statsShow ], | |
61 | + | |
58 | 62 | // AddressBook pages |
59 | 63 | [ location => location.page === 'addressBook', pages.addressBook ], |
60 | 64 | |
61 | 65 | // Private Thread pages |
ssb-server-blog-stats.js | ||
---|---|---|
@@ -1,0 +1,148 @@ | ||
1 | +const FlumeView = require('flumeview-level') | |
2 | +const get = require('lodash/get') | |
3 | +const pull = require('pull-stream') | |
4 | +const isBlog = require('scuttle-blog/isBlog') | |
5 | +const { isMsg: isMsgRef } = require('ssb-ref') | |
6 | + | |
7 | +const getType = (msg) => get(msg, 'value.content.type') | |
8 | +const getAuthor = (msg) => get(msg, 'value.author') | |
9 | +const getCommentRoot = (msg) => get(msg, 'value.content.root') | |
10 | +const getLikeRoot = (msg) => get(msg, 'value.content.vote.link') | |
11 | +const getTimestamp = (msg) => get(msg, 'value.timestamp') | |
12 | + | |
13 | +const FLUME_VIEW_VERSION = 5 | |
14 | + | |
15 | +module.exports = { | |
16 | + name: 'blogStats', | |
17 | + version: 1, | |
18 | + manifest: { | |
19 | + get: 'async', | |
20 | + read: 'source', | |
21 | + readBlogs: 'source', | |
22 | + getBlogs: 'async', | |
23 | + readComments: 'source', | |
24 | + readLikes: 'source' | |
25 | + }, | |
26 | + init: (server, config) => { | |
27 | + console.log('initialising blog-stats plugin') | |
28 | + const myKey = server.keys.id | |
29 | + | |
30 | + const view = server._flumeUse( | |
31 | + 'internalblogStats', | |
32 | + FlumeView(FLUME_VIEW_VERSION, map) | |
33 | + ) | |
34 | + | |
35 | + return { | |
36 | + get: view.get, | |
37 | + read: view.read, | |
38 | + readBlogs, | |
39 | + getBlogs, | |
40 | + readComments, | |
41 | + readLikes, | |
42 | + // getLikes | |
43 | + // getComments | |
44 | + } | |
45 | + | |
46 | + function map (msg, seq) { | |
47 | + var root | |
48 | + | |
49 | + switch (getType(msg)) { | |
50 | + case 'blog': | |
51 | + if (isBlog(msg) && myBlog(msg)) return [['B', msg.key, getTimestamp(msg)]] | |
52 | + else return [] | |
53 | + | |
54 | + case 'vote': | |
55 | + // process.stdout.write('L') | |
56 | + root = getLikeRoot(msg) | |
57 | + // TODO figure out how to only store likes I care about | |
58 | + if (root) return [['L', root, getTimestamp(msg)]] | |
59 | + else return [] | |
60 | + | |
61 | + // Note this catches: | |
62 | + // - all likes, on all things D: | |
63 | + // - likes AND unlikes | |
64 | + | |
65 | + case 'post': | |
66 | + // process.stdout.write('C') | |
67 | + root = getCommentRoot(msg) | |
68 | + // TODO figure out how to only store likes I care about | |
69 | + if (root) return [['C', root, getTimestamp(msg)]] | |
70 | + else return [] | |
71 | + | |
72 | + // Note this catches: | |
73 | + // - all comments, on all things D: | |
74 | + | |
75 | + default: | |
76 | + return [] | |
77 | + } | |
78 | + } | |
79 | + | |
80 | + function readBlogs (options = {}) { | |
81 | + const query = Object.assign({}, { | |
82 | + gte: ['B', null, null], | |
83 | + // null is the 'minimum' structure in bytewise ordering | |
84 | + lte: ['B~', undefined, undefined], | |
85 | + reverse: true, | |
86 | + values: true, | |
87 | + keys: false, | |
88 | + seqs: false | |
89 | + }, options) | |
90 | + | |
91 | + return view.read(query) | |
92 | + } | |
93 | + | |
94 | + function getBlogs (options, cb) { | |
95 | + pull( | |
96 | + readBlogs(options), | |
97 | + pull.collect(cb) | |
98 | + ) | |
99 | + } | |
100 | + | |
101 | + function readComments (options = {}) { | |
102 | + var key | |
103 | + if (!options.blog) key = null | |
104 | + else if (isMsgRef(options.blog)) key = options.blog | |
105 | + else if (isMsgRef(options.blog.key) && isBlog(options.blog)) key = options.blog.key | |
106 | + | |
107 | + const query = Object.assign({}, { | |
108 | + gt: ['C', key, null], | |
109 | + lt: ['C', key, undefined], | |
110 | + // undefined is the 'maximum' structure in bytewise ordering https://www.npmjs.com/package/bytewise#order-of-supported-structures | |
111 | + reverse: true, | |
112 | + values: true, | |
113 | + keys: false, | |
114 | + seqs: false | |
115 | + }, options) | |
116 | + | |
117 | + delete query.blog | |
118 | + | |
119 | + return view.read(query) | |
120 | + } | |
121 | + | |
122 | + function readLikes (options = {}) { | |
123 | + var key | |
124 | + if (!options.blog) key = null | |
125 | + else if (isMsgRef(options.blog)) key = options.blog | |
126 | + else if (isMsgRef(options.blog.key) && isBlog(options.blog)) key = options.blog.key | |
127 | + | |
128 | + const query = Object.assign({}, { | |
129 | + // gt: ['L', key, null], | |
130 | + // lt: ['L', key, undefined], // why doesn't this work? | |
131 | + gt: ['L', key, null], // null is minimum in bytewise ordering | |
132 | + lt: ['L', key + '~', undefined], // undefinted in maximum in bytewise ordering | |
133 | + reverse: true, | |
134 | + values: true, | |
135 | + keys: false, | |
136 | + seqs: false | |
137 | + }, options) | |
138 | + | |
139 | + delete query.blog | |
140 | + | |
141 | + return view.read(query) | |
142 | + } | |
143 | + | |
144 | + function myBlog (msg) { | |
145 | + return getAuthor(msg) === myKey | |
146 | + } | |
147 | + } | |
148 | +} |
Built with git-ssb-web