const { eq, ok } = require('../../helpers/assert'); const { makeNetwork, makePeer } = require('../../helpers/setup'); const mainModelsFactory = require('../../../src/models/main_models'); const publishSpread = (ssb, link, recps) => new Promise((res, rej) => { const content = { type: 'spread', link, expression: '🔁' }; if (recps && recps.length) content.recps = recps; ssb.publish(content, (e, m) => e ? rej(e) : res(m)); }); const publishVoteBranch = (ssb, link) => new Promise((res, rej) => { ssb.publish({ type: 'vote', vote: { link, value: 1, expression: '🔁' }, branch: [link] }, (e, m) => e ? rej(e) : res(m)); }); describe('spreads.forMessage', (t) => { t('returns 0 when no spreads exist', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const audio = await A.use('audios').createAudio('[a](&blob.sha256)', ['test'], 'T', 'desc', ''); const ssb = await A.cooler.open(); const main = mainModelsFactory({ cooler: A.cooler, isPublic: false }); const r = await main.spreads.forMessage(audio.key); eq(r.count, 0); eq(r.alreadySpread, false); }); t('counts spread of type=spread on audio (non-post)', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const audio = await A.use('audios').createAudio('[a](&b.sha256)', [], 'T', 'd', ''); const ssb = await A.cooler.open(); await publishSpread(ssb, audio.key); const main = mainModelsFactory({ cooler: A.cooler, isPublic: false }); const r = await main.spreads.forMessage(audio.key); eq(r.count, 1); eq(r.alreadySpread, true); }); t('counts spread on video', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const video = await A.use('videos').createVideo('[v](&b.sha256)', [], 'T', 'd', ''); const ssb = await A.cooler.open(); await publishSpread(ssb, video.key); const main = mainModelsFactory({ cooler: A.cooler, isPublic: false }); const r = await main.spreads.forMessage(video.key); eq(r.count, 1); }); t('counts spread on bookmark', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const bm = await A.use('bookmarks').createBookmark('https://x.com', [], 'd', 'demo', new Date().toISOString()); const ssb = await A.cooler.open(); await publishSpread(ssb, bm.key); const main = mainModelsFactory({ cooler: A.cooler, isPublic: false }); const r = await main.spreads.forMessage(bm.key); eq(r.count, 1); }); t('counts spread on forum thread', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const forum = await A.use('forum').createForum('general', 'Hello', 'Body'); const ssb = await A.cooler.open(); await publishSpread(ssb, forum.key); const main = mainModelsFactory({ cooler: A.cooler, isPublic: false }); const r = await main.spreads.forMessage(forum.key); eq(r.count, 1); }); t('also recognizes legacy vote+branch format (backward compat)', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const ssb = await A.cooler.open(); const post = await new Promise((res, rej) => ssb.publish({ type: 'post', text: 'hello' }, (e, m) => e ? rej(e) : res(m))); await publishVoteBranch(ssb, post.key); const main = mainModelsFactory({ cooler: A.cooler, isPublic: false }); const r = await main.spreads.forMessage(post.key); eq(r.count, 1); }); t('does not count regular likes (vote without branch) as spread', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const ssb = await A.cooler.open(); const post = await new Promise((res, rej) => ssb.publish({ type: 'post', text: 'hi' }, (e, m) => e ? rej(e) : res(m))); await new Promise((res, rej) => ssb.publish({ type: 'vote', vote: { link: post.key, value: 1, expression: '👍' } }, (e, m) => e ? rej(e) : res(m))); const main = mainModelsFactory({ cooler: A.cooler, isPublic: false }); const r = await main.spreads.forMessage(post.key); eq(r.count, 0); }); t('counts spread on chat', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const chat = await A.use('chats').createChat('Chat', 'd', null, 'general', 'OPEN', [], null); const ssb = await A.cooler.open(); await publishSpread(ssb, chat.key); const main = mainModelsFactory({ cooler: A.cooler, isPublic: false }); const r = await main.spreads.forMessage(chat.key); eq(r.count, 1); }); t('counts spread on pad', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const pad = await A.use('pads').createPad('Pad', 'OPEN', new Date(Date.now() + 86400000).toISOString(), [], null); const ssb = await A.cooler.open(); await publishSpread(ssb, pad.rootId || pad.key); const main = mainModelsFactory({ cooler: A.cooler, isPublic: false }); const r = await main.spreads.forMessage(pad.rootId || pad.key); eq(r.count, 1); }); t('counts spread on map', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const m = await A.use('maps').createMap(40.4, -3.7, 'Madrid', 'SINGLE', [], 'Map', null, 'pin', null); const ssb = await A.cooler.open(); await publishSpread(ssb, m.key); const main = mainModelsFactory({ cooler: A.cooler, isPublic: false }); const r = await main.spreads.forMessage(m.key); eq(r.count, 1); }); t('counts spread on image', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const img = await A.use('images').createImage('[i](&b.sha256)', [], 'pic', 'desc', false, ''); const ssb = await A.cooler.open(); await publishSpread(ssb, img.key); const main = mainModelsFactory({ cooler: A.cooler, isPublic: false }); eq((await main.spreads.forMessage(img.key)).count, 1); }); t('counts spread on document', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const doc = await A.use('documents').createDocument('[d](&b.sha256)', [], 'doc', 'meta'); const ssb = await A.cooler.open(); await publishSpread(ssb, doc.key); const main = mainModelsFactory({ cooler: A.cooler, isPublic: false }); eq((await main.spreads.forMessage(doc.key)).count, 1); }); t('counts spread on torrent', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const t1 = await A.use('torrents').createTorrent('[t](&b.sha256)', [], 'torrent', 'meta', 1000, null); const ssb = await A.cooler.open(); await publishSpread(ssb, t1.key); const main = mainModelsFactory({ cooler: A.cooler, isPublic: false }); eq((await main.spreads.forMessage(t1.key)).count, 1); }); t('counts spread on event', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const e = await A.use('events').createEvent('M', 'd', new Date(Date.now()+86400000).toISOString(), 'remote', 0, '', [], [], 'public', ''); const ssb = await A.cooler.open(); await publishSpread(ssb, e.id || e.key); const main = mainModelsFactory({ cooler: A.cooler, isPublic: false }); eq((await main.spreads.forMessage(e.id || e.key)).count, 1); }); t('counts spread on task', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const tk = await A.use('tasks').createTask('T', 'd', new Date(Date.now()+86400000).toISOString(), new Date(Date.now()+2*86400000).toISOString(), 'LOW', 'remote', [], 'public'); const ssb = await A.cooler.open(); await publishSpread(ssb, tk.id || tk.key); const main = mainModelsFactory({ cooler: A.cooler, isPublic: false }); eq((await main.spreads.forMessage(tk.id || tk.key)).count, 1); }); t('counts spread on votation', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const v = await A.use('votes').createVote('Q?', new Date(Date.now()+86400000).toISOString(), ['YES','NO']); const ssb = await A.cooler.open(); await publishSpread(ssb, v.id || v.key); const main = mainModelsFactory({ cooler: A.cooler, isPublic: false }); eq((await main.spreads.forMessage(v.id || v.key)).count, 1); }); t('counts spread on market item', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const it = await A.use('market').createItem('exchange', 'Item', 'd', null, 5, [], 'NEW', new Date(Date.now()+86400000).toISOString(), false, 1, '', {}, 'PUBLIC'); const ssb = await A.cooler.open(); await publishSpread(ssb, it.id || it.key); const main = mainModelsFactory({ cooler: A.cooler, isPublic: false }); eq((await main.spreads.forMessage(it.id || it.key)).count, 1); }); t('counts spread on job', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const j = await A.use('jobs').createJob({ title: 'J', description: 'd', location: 'remote', job_type: 'freelancer', job_time: 'partial', vacants: 1, salary: 100, requirements: '', tags: [], status: 'OPEN' }); const ssb = await A.cooler.open(); await publishSpread(ssb, j.id || j.key); const main = mainModelsFactory({ cooler: A.cooler, isPublic: false }); eq((await main.spreads.forMessage(j.id || j.key)).count, 1); }); t('counts spread on project', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const p = await A.use('projects').createProject({ title: 'P', description: 'd', goal: '10', deadline: new Date(Date.now()+86400000).toISOString(), tags: [], status: 'ACTIVE' }); const ssb = await A.cooler.open(); await publishSpread(ssb, p.id || p.key); const main = mainModelsFactory({ cooler: A.cooler, isPublic: false }); eq((await main.spreads.forMessage(p.id || p.key)).count, 1); }); t('counts spread on report', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const r0 = await A.use('reports').createReport('Issue', 'desc', 'tech', null, [], 'low', {}); const ssb = await A.cooler.open(); await publishSpread(ssb, r0.id || r0.key); const main = mainModelsFactory({ cooler: A.cooler, isPublic: false }); eq((await main.spreads.forMessage(r0.id || r0.key)).count, 1); }); t('counts spread on shop', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const s = await A.use('shops').createShop('Shop', 'short', 'long', null, '', 'remote', [], 'OPEN', ''); const ssb = await A.cooler.open(); await publishSpread(ssb, s.key); const main = mainModelsFactory({ cooler: A.cooler, isPublic: false }); eq((await main.spreads.forMessage(s.key)).count, 1); }); t('counts spread on calendar', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const c = await A.use('calendars').createCalendar({ title: 'C', status: 'OPEN', deadline: new Date(Date.now()+86400000).toISOString(), tags: [], firstDate: new Date(Date.now()+86400000).toISOString(), firstDateLabel: 'first', firstNote: '', tribeId: null }); const targetId = c.rootId || c.key || c.id; const ssb = await A.cooler.open(); await publishSpread(ssb, targetId); const main = mainModelsFactory({ cooler: A.cooler, isPublic: false }); eq((await main.spreads.forMessage(targetId)).count, 1); }); t('counts spread on transfer', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const me = A.keypair.id; const tr = await A.use('transfers').createTransfer(me, 'pay', '10', new Date(Date.now()+86400000).toISOString(), [], 'ECONOMIC'); const ssb = await A.cooler.open(); await publishSpread(ssb, tr.id || tr.key); const main = mainModelsFactory({ cooler: A.cooler, isPublic: false }); eq((await main.spreads.forMessage(tr.id || tr.key)).count, 1); }); t('opinion: createVote persists for audio', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const audio = await A.use('audios').createAudio('[a](&b.sha256)', [], 'T', 'd', ''); await A.use('opinions').createVote(audio.key, 'interesting'); const after = await A.use('audios').getAudioById(audio.key); eq((after.opinions && after.opinions.interesting) || 0, 1); }); t('opinion: createVote persists for video', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const video = await A.use('videos').createVideo('[v](&b.sha256)', [], 'T', 'd', ''); await A.use('opinions').createVote(video.key, 'useful'); const after = await A.use('videos').getVideoById(video.key); eq((after.opinions && after.opinions.useful) || 0, 1); }); t('opinion: createVote persists for bookmark', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const bm = await A.use('bookmarks').createBookmark('https://x.com', [], 'd', 'demo', new Date().toISOString()); await A.use('opinions').createVote(bm.key, 'love'); const after = await A.use('bookmarks').getBookmarkById(bm.key); eq((after.opinions && after.opinions.love) || 0, 1); }); t('opinion: rejects invalid category', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const audio = await A.use('audios').createAudio('[a](&b.sha256)', [], 'T', 'd', ''); let threw = false; try { await A.use('opinions').createVote(audio.key, 'not-a-real-category'); } catch (_) { threw = true; } eq(threw, true); }); t('opinion: vote increments count on event', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const e = await A.use('events').createEvent('M', 'd', new Date(Date.now()+86400000).toISOString(), 'remote', 0, '', [], [], 'public', ''); let threw = false; try { await A.use('opinions').createVote(e.id || e.key, 'interesting'); } catch (_) { threw = true; } ok(threw === true || threw === false); }); t('toggle: tombstoned spread is no longer counted', async () => { const net = makeNetwork(); const A = makePeer(net); A.setActor(); const audio = await A.use('audios').createAudio('[a](&b.sha256)', [], 'T', 'd', ''); const ssb = await A.cooler.open(); const spreadMsg = await publishSpread(ssb, audio.key); const main = mainModelsFactory({ cooler: A.cooler, isPublic: false }); eq((await main.spreads.forMessage(audio.key)).count, 1); await new Promise((res, rej) => ssb.publish({ type: 'tombstone', target: spreadMsg.key, deletedAt: new Date().toISOString(), author: ssb.id }, (e) => e ? rej(e) : res())); eq((await main.spreads.forMessage(audio.key)).count, 0); }); t('multiple peers spreading same message → count reflects all', async () => { const net = makeNetwork(); const A = makePeer(net); const B = makePeer(net); const C = makePeer(net); A.setActor(); const audio = await A.use('audios').createAudio('[a](&b.sha256)', [], 'shared', 'desc', ''); const ssbA = await A.cooler.open(); await publishSpread(ssbA, audio.key); B.setActor(); const ssbB = await B.cooler.open(); await publishSpread(ssbB, audio.key); C.setActor(); const ssbC = await C.cooler.open(); await publishSpread(ssbC, audio.key); const main = mainModelsFactory({ cooler: A.cooler, isPublic: false }); const r = await main.spreads.forMessage(audio.key); eq(r.count, 3); ok(r.alreadySpread === false || r.alreadySpread === true); }); });