git ssb

1+

Daan Patchwork / patchwork



Tree: aabb27408791b10bbb6e9ba87c00240141ddec4a

Files: aabb27408791b10bbb6e9ba87c00240141ddec4a / lib / plugins / public-feed.js

8352 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 return false
100 }),
101
102 // MAP ROOT ITEMS
103 pull.map(item => {
104 const root = item.root || item
105 return root
106 }),
107
108 // RESOLVE ROOTS WITH ABOUTS
109 ResolveAbouts({ ssb }),
110
111 // ADD THREAD SUMMARY
112 Paramap((item, cb) => {
113 threadSummary(item.key, {
114 pullFilter: pull(
115 FilterBlocked([item.value && item.value.author, ssb.id], { isBlocking: ssb.patchwork.contacts.isBlocking }),
116 ApplyFilterResult({ ssb })
117 ),
118 recentLimit: 3,
119 readThread: ssb.patchwork.thread.read,
120 bumpFilter: bumpFilter
121 }, (err, summary) => {
122 if (err) return cb(err)
123 cb(null, extend(item, summary, {
124 filterResult: undefined,
125 rootBump: bumpFilter
126 }))
127 })
128 }, 20)
129 )
130 }))
131 })
132 })
133 }
134 }
135
136 function shouldShow (filterResult) {
137 return !!filterResult
138 }
139}
140
141function FilterPrivateRoots () {
142 return pull.filter(msg => {
143 return !msg.root || (msg.root.value && !msg.root.value.private)
144 })
145}
146
147function ApplyRootFilterResult ({ ssb }) {
148 return Paramap((item, cb) => {
149 if (item.root) {
150 getFilterResult(item.root, { ssb }, (err, rootFilterResult) => {
151 if (err) return cb(err)
152 if (item.filterResult && checkReplyForcesDisplay(item)) { // include this item if it has matching tags or the author is you
153 item.root.filterResult = extend(item.filterResult, { forced: true })
154 } else {
155 item.root.filterResult = rootFilterResult
156 }
157 cb(null, item)
158 })
159 } else {
160 cb(null, item)
161 }
162 })
163}
164
165function ApplyFilterResult ({ ssb }) {
166 return Paramap((item, cb) => {
167 getFilterResult(item, { ssb }, (err, filterResult) => {
168 if (err) return cb(err)
169 item.filterResult = filterResult
170 cb(null, item)
171 })
172 }, 10)
173}
174
175function getFilterResult (msg, { ssb }, cb) {
176 ssb.patchwork.contacts.isFollowing({ source: ssb.id, dest: msg.value.author }, (err, following) => {
177 if (err) return cb(err)
178 ssb.patchwork.subscriptions2.get({ id: ssb.id }, (err, subscriptions) => {
179 if (err) return cb(err)
180 const type = msg.value.content.type
181 if (type === 'vote' || type === 'tag') return cb() // filter out likes and tags
182 const hasChannel = !!msg.value.content.channel
183 const matchesChannel = (type !== 'channel' && checkChannel(subscriptions, msg.value.content.channel))
184 const matchingTags = getMatchingTags(subscriptions, msg.value.content.mentions)
185 const isYours = msg.value.author === ssb.id
186 const mentionsYou = getMentionsYou([ssb.id], msg.value.content.mentions)
187 if (isYours || matchesChannel || matchingTags.length || following || mentionsYou) {
188 cb(null, {
189 matchingTags, matchesChannel, isYours, following, mentionsYou, hasChannel
190 })
191 } else {
192 cb()
193 }
194 })
195 })
196}
197
198function getMatchingTags (lookup, mentions) {
199 if (Array.isArray(mentions)) {
200 return mentions.reduce((result, mention) => {
201 if (mention && typeof mention.link === 'string' && mention.link.startsWith('#')) {
202 if (checkChannel(lookup, mention.link.slice(1))) {
203 result.push(normalizeChannel(mention.link.slice(1)))
204 }
205 }
206 return result
207 }, [])
208 }
209 return []
210}
211
212function getMentionsYou (ids, mentions) {
213 if (Array.isArray(mentions)) {
214 return mentions.some((mention) =>
215 mention && typeof mention.link === 'string' && ids.includes(mention.link)
216 )
217 }
218}
219
220function checkReplyForcesDisplay (item) {
221 const filterResult = item.filterResult || {}
222 const matchesTags = filterResult.matchingTags && !!filterResult.matchingTags.length
223 return matchesTags || filterResult.isYours
224}
225
226function checkChannel (lookup, channel) {
227 if (!lookup) return false
228 channel = normalizeChannel(channel)
229 if (channel) {
230 return lookup[channel] && lookup[channel].subscribed
231 }
232}
233
234function bumpFilter (msg) {
235 const filterResult = msg.filterResult
236 if (filterResult) {
237 if (isAttendee(msg)) {
238 return 'attending'
239 } else if (filterResult.following || filterResult.isYours) {
240 if (msg.value.content.type === 'post') {
241 if (getRoot(msg)) {
242 return 'reply'
243 } else {
244 return 'post'
245 }
246 } else {
247 return 'updated'
248 }
249 } else if (filterResult.matchesChannel || filterResult.matchingTags.length) {
250 const channels = new Set()
251 if (filterResult.matchesChannel) channels.add(msg.value.content.channel)
252 if (Array.isArray(filterResult.matchingTags)) filterResult.matchingTags.forEach(x => channels.add(x))
253 return { type: 'matches-channel', channels: Array.from(channels) }
254 }
255 }
256}
257
258function isAttendee (msg) {
259 const content = msg.value && msg.value.content
260 return (content && content.type === 'about' && content.attendee && !content.attendee.remove)
261}
262

Built with git-ssb-web