| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333 |
- const pull = require('../server/node_modules/pull-stream');
- const moment = require('../server/node_modules/moment');
- const { getConfig } = require('../configs/config-manager.js');
- const logLimit = getConfig().ssbLogStream?.limit || 1000;
- module.exports = ({ cooler, padsModel }) => {
- let ssb;
- const openSsb = async () => {
- if (!ssb) ssb = await cooler.open();
- return ssb;
- };
- const searchableTypes = [
- 'post', 'about', 'curriculum', 'tribe', 'transfer', 'feed',
- 'votes', 'report', 'task', 'event', 'bookmark', 'document',
- 'image', 'audio', 'video', 'torrent', 'market', 'bankWallet', 'bankClaim',
- 'project', 'job', 'forum', 'vote', 'contact', 'pub', 'map', 'shop', 'shopProduct', 'chat', 'pad'
- ];
- const getRelevantFields = (type, content) => {
- switch (type) {
- case 'post':
- return [content?.text, content?.contentWarning, ...(content?.tags || [])];
- case 'about':
- return [content?.about, content?.name, content?.description];
- case 'feed':
- return [content?.text, content?.author, content?.createdAt, ...(content?.tags || []), content?.refeeds];
- case 'event':
- return [content?.title, content?.description, content?.date, content?.location, content?.price, content?.eventUrl, ...(content?.tags || []), content?.attendees, content?.organizer, content?.status, content?.isPublic];
- case 'votes':
- return [content?.question, content?.deadline, content?.status, ...(Object.values(content?.votes || {})), content?.totalVotes];
- case 'tribe':
- return [content?.title, content?.description, content?.image, content?.location, ...(content?.tags || []), content?.isLARP, content?.isAnonymous, content?.members?.length, content?.createdAt, content?.author];
- case 'audio':
- return [content?.url, content?.mimeType, content?.title, content?.description, ...(content?.tags || [])];
- case 'image':
- return [content?.url, content?.title, content?.description, ...(content?.tags || []), content?.meme];
- case 'video':
- return [content?.url, content?.mimeType, content?.title, content?.description, ...(content?.tags || [])];
- case 'document':
- return [content?.url, content?.title, content?.description, ...(content?.tags || []), content?.key];
- case 'torrent':
- return [content?.title, content?.description, ...(content?.tags || []), content?.url];
- case 'market':
- return [content?.item_type, content?.title, content?.description, content?.price, ...(content?.tags || []), content?.status, content?.item_status, content?.deadline, content?.includesShipping, content?.seller, content?.image, content?.auctions_poll, content?.stock];
- case 'bookmark':
- return [content?.author, content?.url, ...(content?.tags || []), content?.description, content?.category, content?.lastVisit];
- case 'task':
- return [content?.title, content?.description, content?.startTime, content?.endTime, content?.priority, content?.location, ...(content?.tags || []), content?.isPublic, content?.assignees?.length, content?.status, content?.author];
- case 'report':
- return [content?.title, content?.description, content?.category, content?.createdAt, content?.author, content?.image, ...(content?.tags || []), content?.confirmations, content?.severity, content?.status, content?.isAnonymous];
- case 'transfer':
- return [content?.from, content?.to, content?.concept, content?.amount, content?.deadline, content?.status, ...(content?.tags || []), content?.confirmedBy?.length];
- case 'curriculum':
- return [content?.author, content?.name, content?.description, content?.photo, ...(content?.personalSkills || []), ...(content?.personalExperiences || []), ...(content?.oasisExperiences || []), ...(content?.oasisSkills || []), ...(content?.educationExperiences || []), ...(content?.educationalSkills || []), ...(content?.languages || []), ...(content?.professionalExperiences || []), ...(content?.professionalSkills || []), content?.location, content?.status, content?.preferences, content?.createdAt];
- case 'bankWallet':
- return [content?.address];
- case 'bankClaim':
- return [content?.amount, content?.epochId, content?.allocationId, content?.txid];
- case 'project':
- return [content?.title, content?.status, content?.progress, content?.goal, content?.pledged, content?.deadline, (content?.followers || []).length, (content?.backers || []).length, (content?.milestones || []).length, content?.bounty, content?.bountyAmount, content?.bounty_currency, content?.activity?.kind, content?.activityActor];
- case 'job':
- return [content?.title, content?.job_type, ...(content?.tasks || []), content?.location, content?.vacants, content?.salary, content?.status, (content?.subscribers || []).length];
- case 'forum':
- return [content?.root, content?.category, content?.title, content?.text, content?.key];
- case 'vote':
- return [content?.vote?.link];
- case 'contact':
- return [content?.contact];
- case 'pub':
- return [content?.address?.host, content?.address?.key];
- case 'map':
- return [content?.title, content?.description, content?.mapType, ...(content?.tags || []), content?.lat, content?.lng];
- case 'shop':
- return [content?.title, content?.shortDescription, content?.description, content?.location, ...(content?.tags || []), content?.visibility, content?.url];
- case 'shopProduct':
- return [content?.title, content?.description, content?.price, ...(content?.tags || []), content?.shopId];
- case 'chat':
- return [content?.title, content?.description, content?.category, ...(content?.tags || []), content?.status, content?.author];
- case 'pad':
- return [content?.title, content?.status, content?.deadline, ...(content?.tags || []), content?.author];
- case 'gameScore':
- return [content?.game, content?.player];
- default:
- return [];
- }
- };
- const norm = (v) => String(v == null ? '' : v).trim().toLowerCase();
- const getDedupeKey = (msg) => {
- const c = msg?.value?.content || {};
- const t = c?.type || 'unknown';
- const author = c.author || msg?.value?.author || '';
- if (t === 'post') return `post:${msg.key}`;
- if (t === 'about') return `about:${c.about || author || msg.key}`;
- if (t === 'curriculum') return `curriculum:${c.author || msg?.value?.author || msg.key}`;
- if (t === 'contact') return `contact:${c.contact || msg.key}`;
- if (t === 'vote') return `vote:${c?.vote?.link || msg.key}`;
- if (t === 'pub') return `pub:${c?.address?.key || c?.address?.host || msg.key}`;
- if (t === 'bankWallet') return `bankWallet:${c?.address || msg.key}`;
- if (t === 'bankClaim') return `bankClaim:${c?.txid || `${c?.epochId || ''}:${c?.allocationId || ''}:${c?.amount || ''}` || msg.key}`;
- if (t === 'document') return `document:${c.key || c.url || `${author}|${norm(c.title)}` || msg.key}`;
- if (t === 'image') return `image:${c.url || `${author}|${norm(c.title)}|${norm(c.description)}` || msg.key}`;
- if (t === 'audio') return `audio:${c.url || `${author}|${norm(c.title)}|${norm(c.description)}` || msg.key}`;
- if (t === 'video') return `video:${c.url || `${author}|${norm(c.title)}|${norm(c.description)}` || msg.key}`;
- if (t === 'torrent') return `torrent:${c.url || `${author}|${norm(c.title)}|${norm(c.description)}` || msg.key}`;
- if (t === 'bookmark') return `bookmark:${author}|${c.url || norm(c.description) || msg.key}`;
- if (t === 'tribe') {
- return [
- 'tribe',
- author,
- norm(c.title),
- norm(c.location),
- norm(c.image)
- ].join('|');
- }
- if (t === 'event') {
- return [
- 'event',
- c.organizer || author,
- norm(c.title),
- norm(c.date),
- norm(c.location)
- ].join('|');
- }
- if (t === 'task') {
- return [
- 'task',
- c.author || author,
- norm(c.title),
- norm(c.startTime),
- norm(c.endTime),
- norm(c.location)
- ].join('|');
- }
- if (t === 'report') {
- return [
- 'report',
- c.author || author,
- norm(c.title),
- norm(c.category),
- norm(c.severity)
- ].join('|');
- }
- if (t === 'votes') {
- return [
- 'votes',
- c.createdBy || author,
- norm(c.question),
- norm(c.deadline)
- ].join('|');
- }
- if (t === 'market') {
- return [
- 'market',
- c.seller || author,
- norm(c.title),
- norm(c.deadline),
- norm(c.item_type),
- norm(c.image)
- ].join('|');
- }
- if (t === 'transfer') {
- const txid = c.txid || c.transactionId || c.id;
- if (txid) return `transfer:${txid}`;
- return [
- 'transfer',
- norm(c.from),
- norm(c.to),
- norm(c.amount),
- norm(c.concept),
- norm(c.deadline)
- ].join('|');
- }
- if (t === 'feed') {
- return [
- 'feed',
- c.author || author,
- norm(c.text)
- ].join('|');
- }
- if (t === 'project') {
- return [
- 'project',
- c.activityActor || author,
- norm(c.title),
- norm(c.deadline),
- norm(c.goal)
- ].join('|');
- }
- if (t === 'job') {
- return [
- 'job',
- author,
- norm(c.title),
- norm(c.location),
- norm(c.salary),
- norm(c.job_type)
- ].join('|');
- }
- if (t === 'forum') {
- return `forum:${c.key || c.root || `${author}|${norm(c.title)}` || msg.key}`;
- }
- if (t === 'map') {
- return ['map', author, norm(c.title), norm(c.description), norm(c.lat), norm(c.lng)].join('|');
- }
- if (t === 'shop') {
- return ['shop', author, norm(c.title), norm(c.location)].join('|');
- }
- if (t === 'shopProduct') {
- return ['shopProduct', author, norm(c.title), norm(c.shopId)].join('|');
- }
- if (t === 'pad') {
- return ['pad', author, norm(c.title), norm(c.deadline)].join('|');
- }
- return `${t}:${msg.key}`;
- };
- const dedupeKeepLatest = (msgs) => {
- const map = new Map();
- for (const msg of msgs) {
- const k = getDedupeKey(msg);
- const prev = map.get(k);
- const ts = msg?.value?.timestamp || 0;
- const pts = prev?.value?.timestamp || 0;
- if (!prev || ts > pts) map.set(k, msg);
- }
- return Array.from(map.values());
- };
- const search = async ({ query, types = [], resultsPerPage = "10" }) => {
- const ssbClient = await openSsb();
- const queryLower = String(query || '').toLowerCase();
- const messages = await new Promise((res, rej) => {
- pull(
- ssbClient.createLogStream({ limit: logLimit }),
- pull.collect((err, msgs) => err ? rej(err) : res(msgs))
- );
- });
- const tombstoned = new Set(messages.filter(m => m.value?.content?.type === 'tombstone').map(m => m.value.content.target));
- const replacesMap = new Map();
- const latestByKey = new Map();
- for (const msg of messages) {
- const k = msg.key;
- const c = msg?.value?.content;
- const t = c?.type;
- if (!t || !searchableTypes.includes(t)) continue;
- if (tombstoned.has(k)) continue;
- if (c.replaces) replacesMap.set(c.replaces, k);
- latestByKey.set(k, msg);
- }
- for (const oldId of replacesMap.keys()) {
- latestByKey.delete(oldId);
- }
- if (padsModel) {
- for (const msg of latestByKey.values()) {
- const c = msg?.value?.content;
- if (c?.type === 'pad') {
- const rootId = c.replaces ? msg.key : msg.key;
- const decrypted = padsModel.decryptContent(c, rootId);
- c.title = decrypted.title || c.title;
- c.deadline = decrypted.deadline || c.deadline;
- c.tags = decrypted.tags.length ? decrypted.tags : c.tags;
- }
- }
- }
- let filtered = Array.from(latestByKey.values()).filter(msg => {
- const c = msg?.value?.content;
- const t = c?.type;
- if (!t || (types.length > 0 && !types.includes(t))) return false;
- if (t === 'market') {
- if (c.stock === 0 && c.status !== 'SOLD') return false;
- }
- if (!queryLower) return true;
- if (queryLower.startsWith('@') && queryLower.length > 1) return (t === 'about' && c?.about === query);
- const fields = getRelevantFields(t, c);
- if (queryLower.startsWith('#') && queryLower.length > 1) {
- const tag = queryLower.substring(1);
- const tagArr = Array.isArray(c?.tags) ? c.tags : (typeof c?.tags === 'string' ? c.tags.split(',').map(s => s.trim()).filter(Boolean) : []);
- return tagArr.some(x => String(x).toLowerCase() === tag);
- }
- return fields.filter(Boolean).map(String).some(field => field.toLowerCase().includes(queryLower));
- });
- filtered = dedupeKeepLatest(filtered);
- filtered.sort((a, b) => (b?.value?.timestamp || 0) - (a?.value?.timestamp || 0));
- const grouped = filtered.reduce((acc, msg) => {
- const t = msg?.value?.content?.type || 'unknown';
- if (!acc[t]) acc[t] = [];
- acc[t].push(msg);
- return acc;
- }, {});
- if (resultsPerPage !== "all") {
- const limit = parseInt(resultsPerPage, 10);
- for (const key in grouped) grouped[key] = grouped[key].slice(0, limit);
- }
- return grouped;
- };
- return { search };
- };
|