git ssb

1+

Daan Patchwork / patchwork



Tree: 58ab0241031aa549a35cce1e678c27065ae66221

Files: 58ab0241031aa549a35cce1e678c27065ae66221 / lib / plugins / public-feed.js

8353 bytesRaw
1'use strict'
2const pull = require('pull-stream')
3const HLRU = require('hashlru')
4const extend = require('xtend')
5const normalizeChannel = require('ssb-ref').normalizeChannel
6const pullResume = require('../pull-resume')
7const threadSummary = require('../thread-summary')
8const LookupRoots = require('../lookup-roots')
9const ResolveAbouts = require('../resolve-abouts')
10const Paramap = require('pull-paramap')
11const getRoot = require('../get-root')
12const FilterBlocked = require('../filter-blocked')
13const PullCont = require('pull-cont/source')
14
15exports.manifest = {
16 latest: 'source',
17 roots: 'source'
18}
19
20exports.init = function (ssb) {
21 // cache mostly just to avoid reading the same roots over and over again
22 // not really big enough for multiple refresh cycles
23 const cache = HLRU(100)
24
25 return {
26 latest: function () {
27 return pull(
28 ssb.createFeedStream({ live: true, old: false, awaitReady: false }),
29
30 ApplyFilterResult({ ssb }),
31 pull.filter(msg => !!msg.filterResult),
32
33 LookupRoots({ ssb, cache }),
34
35 FilterPrivateRoots(),
36 FilterBlocked([ssb.id], {
37 isBlocking: ssb.patchwork.contacts.isBlocking,
38 useRootAuthorBlocks: true,
39 checkRoot: true
40 }),
41
42 ApplyRootFilterResult({ ssb }),
43 pull.filter(msg => {
44 const root = msg.root || msg
45 return root.filterResult
46 })
47 )
48 },
49 roots: function ({ reverse, limit, resume }) {
50 const seen = new Set()
51 const included = new Set()
52
53 // use resume option if specified
54 const opts = { reverse, old: true, awaitReady: false }
55 if (resume) {
56 opts[reverse ? 'lt' : 'gt'] = resume
57 }
58
59 return PullCont(cb => {
60 // wait until contacts have resolved before reading
61 ssb.patchwork.contacts.raw.get(() => {
62 cb(null, pullResume.source(ssb.createFeedStream(opts), {
63 limit,
64 getResume: (item) => {
65 return item && item.rts
66 },
67 filterMap: pull(
68 ApplyFilterResult({ ssb }),
69 pull.filter(msg => !!msg.filterResult),
70
71 LookupRoots({ ssb, cache }),
72
73 FilterPrivateRoots(),
74
75 FilterBlocked([ssb.id], {
76 isBlocking: ssb.patchwork.contacts.isBlocking,
77 useRootAuthorBlocks: true,
78 checkRoot: true
79 }),
80
81 ApplyRootFilterResult({ ssb }),
82
83 // FILTER ROOTS
84 pull.filter(item => {
85 const root = item.root || item
86 if (!included.has(root.key) && root && root.value && root.filterResult) {
87 if (root.filterResult.forced) {
88 // force include the root when a reply has matching tags or the author is you
89 included.add(root.key)
90 return true
91 } else if (!seen.has(root.key)) {
92 seen.add(root.key)
93 if (shouldShow(root.filterResult)) {
94 included.add(root.key)
95 return true
96 }
97 }
98 }
99 }),
100
101 // MAP ROOT ITEMS
102 pull.map(item => {
103 const root = item.root || item
104 return root
105 }),
106
107 // RESOLVE ROOTS WITH ABOUTS
108 ResolveAbouts({ ssb }),
109
110 // ADD THREAD SUMMARY
111 Paramap((item, cb) => {
112 threadSummary(item.key, {
113 pullFilter: pull(
114 FilterBlocked([item.value && item.value.author, ssb.id], { isBlocking: ssb.patchwork.contacts.isBlocking }),
115 ApplyFilterResult({ ssb })
116 ),
117 recentLimit: 3,
118 readThread: ssb.patchwork.thread.read,
119 bumpFilter: bumpFilter
120 }, (err, summary) => {
121 if (err) return cb(err)
122 cb(null, extend(item, summary, {
123 filterResult: undefined,
124 rootBump: bumpFilter
125 }))
126 })
127 }, 20)
128 )
129 }))
130 })
131 })
132 }
133 }
134
135 function shouldShow (filterResult) {
136 return !!filterResult
137 }
138}
139
140function FilterPrivateRoots () {
141 return pull.filter(msg => {
142 return !msg.root || (msg.root.value && !msg.root.value.private)
143 })
144}
145
146function ApplyRootFilterResult ({ ssb }) {
147 return Paramap((item, cb) => {
148 if (item.root) {
149 getFilterResult(item.root, { ssb }, (err, rootFilterResult) => {
150 if (err) return cb(err)
151 if (item.filterResult && checkReplyForcesDisplay(item)) { // include this item if it has matching tags or the author is you
152 item.root.filterResult = extend(item.filterResult, { forced: true })
153 } else {
154 item.root.filterResult = rootFilterResult
155 }
156 cb(null, item)
157 })
158 } else {
159 cb(null, item)
160 }
161 })
162}
163
164function ApplyFilterResult ({ ssb }) {
165 return Paramap((item, cb) => {
166 getFilterResult(item, { ssb }, (err, filterResult) => {
167 if (err) return cb(err)
168 item.filterResult = filterResult
169 cb(null, item)
170 })
171 }, 10)
172}
173
174function getFilterResult (msg, { ssb }, cb) {
175 ssb.patchwork.contacts.isFollowing({ source: ssb.id, dest: msg.value.author }, (err, following) => {
176 if (err) return cb(err)
177 ssb.patchwork.subscriptions2.get({ id: ssb.id }, (err, subscriptions) => {
178 if (err) return cb(err)
179 const type = msg.value.content.type
180 if (type === 'vote' || type === 'tag') return cb() // filter out likes and tags
181 const hasChannel = !!msg.value.content.channel
182 const matchesChannel = (type !== 'channel' && checkChannel(subscriptions, msg.value.content.channel))
183 const matchingTags = getMatchingTags(subscriptions, msg.value.content.mentions)
184 const isYours = msg.value.author === ssb.id
185 const mentionsYou = getMentionsYou([ssb.id], msg.value.content.mentions)
186 if (isYours || matchesChannel || matchingTags.length || following || mentionsYou) {
187 cb(null, {
188 matchingTags, matchesChannel, isYours, following, mentionsYou, hasChannel
189 })
190 } else {
191 cb()
192 }
193 })
194 })
195}
196
197function getMatchingTags (lookup, mentions) {
198 if (Array.isArray(mentions)) {
199 return mentions.reduce((result, mention) => {
200 if (mention && typeof mention.link === 'string' && mention.link.startsWith('#')) {
201 if (checkChannel(lookup, mention.link.slice(1))) {
202 result.push(normalizeChannel(mention.link.slice(1)))
203 }
204 }
205 return result
206 }, [])
207 }
208 return []
209}
210
211function getMentionsYou (ids, mentions) {
212 if (Array.isArray(mentions)) {
213 return mentions.some((mention) => {
214 if (mention && typeof mention.link === 'string') {
215 return ids.includes(mention.link)
216 }
217 })
218 }
219}
220
221function checkReplyForcesDisplay (item) {
222 const filterResult = item.filterResult || {}
223 const matchesTags = filterResult.matchingTags && !!filterResult.matchingTags.length
224 return matchesTags || filterResult.isYours
225}
226
227function checkChannel (lookup, channel) {
228 if (!lookup) return false
229 channel = normalizeChannel(channel)
230 if (channel) {
231 return lookup[channel] && lookup[channel].subscribed
232 }
233}
234
235function bumpFilter (msg) {
236 const filterResult = msg.filterResult
237 if (filterResult) {
238 if (isAttendee(msg)) {
239 return 'attending'
240 } else if (filterResult.following || filterResult.isYours) {
241 if (msg.value.content.type === 'post') {
242 if (getRoot(msg)) {
243 return 'reply'
244 } else {
245 return 'post'
246 }
247 } else {
248 return 'updated'
249 }
250 } else if (filterResult.matchesChannel || filterResult.matchingTags.length) {
251 const channels = new Set()
252 if (filterResult.matchesChannel) channels.add(msg.value.content.channel)
253 if (Array.isArray(filterResult.matchingTags)) filterResult.matchingTags.forEach(x => channels.add(x))
254 return { type: 'matches-channel', channels: Array.from(channels) }
255 }
256 }
257}
258
259function isAttendee (msg) {
260 const content = msg.value && msg.value.content
261 return (content && content.type === 'about' && content.attendee && !content.attendee.remove)
262}
263

Built with git-ssb-web