123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278 |
- const pull = require('../server/node_modules/pull-stream');
- const ssbClientGUI = require("../client/gui");
- const coolerInstance = ssbClientGUI({ offline: require('../server/ssb_config').offline });
- const models = require("../models/main_models");
- const { about, friend } = models({
- cooler: coolerInstance,
- isPublic: require('../server/ssb_config').public,
- });
- const { getConfig } = require('../configs/config-manager.js');
- const logLimit = getConfig().ssbLogStream?.limit || 1000;
- module.exports = ({ cooler }) => {
- let ssb;
- const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
- return {
- async listInhabitants(options = {}) {
- const { filter = 'all', search = '', location = '', language = '', skills = '' } = options;
- const ssbClient = await openSsb();
- const userId = ssbClient.id;
- const timeoutPromise = (timeout) => new Promise((_, reject) => setTimeout(() => reject('Timeout'), timeout));
- const fetchUserImage = (feedId) => {
- return Promise.race([
- about.image(feedId),
- timeoutPromise(5000)
- ]).catch(() => '/assets/images/default-avatar.png');
- };
- if (filter === 'GALLERY') {
- const feedIds = await new Promise((res, rej) => {
- pull(
- ssbClient.createLogStream({ limit: logLimit }),
- pull.filter(msg => {
- const c = msg.value?.content;
- const a = msg.value?.author;
- return c &&
- c.type === 'about' &&
- c.type !== 'tombstone' &&
- typeof c.name === 'string' &&
- typeof c.about === 'string' &&
- c.about === a;
- }),
- pull.collect((err, msgs) => err ? rej(err) : res(msgs))
- );
- });
- const uniqueFeedIds = Array.from(new Set(feedIds.map(r => r.value.author).filter(Boolean)));
- const users = await Promise.all(
- uniqueFeedIds.map(async (feedId) => {
- const name = await about.name(feedId);
- const description = await about.description(feedId);
- const image = await fetchUserImage(feedId);
- const photo =
- typeof image === 'string'
- ? `/image/256/${encodeURIComponent(image)}`
- : '/assets/images/default-avatar.png';
- return { id: feedId, name, description, photo };
- })
- );
- return users;
- }
- if (filter === 'all') {
- const feedIds = await new Promise((res, rej) => {
- pull(
- ssbClient.createLogStream({ limit: logLimit }),
- pull.filter(msg => {
- const c = msg.value?.content;
- const a = msg.value?.author;
- return c &&
- c.type === 'about' &&
- c.type !== 'tombstone' &&
- typeof c.name === 'string' &&
- typeof c.about === 'string' &&
- c.about === a;
- }),
- pull.collect((err, msgs) => err ? rej(err) : res(msgs))
- );
- });
- const uniqueFeedIds = Array.from(new Set(feedIds.map(r => r.value.author).filter(Boolean)));
- const users = await Promise.all(
- uniqueFeedIds.map(async (feedId) => {
- const name = await about.name(feedId);
- const description = await about.description(feedId);
- const image = await fetchUserImage(feedId);
- const photo =
- typeof image === 'string'
- ? `/image/256/${encodeURIComponent(image)}`
- : '/assets/images/default-avatar.png';
- return { id: feedId, name, description, photo };
- })
- );
- const deduplicated = Array.from(new Map(users.filter(u => u && u.id).map(u => [u.id, u])).values());
- let filtered = deduplicated;
- if (search) {
- const q = search.toLowerCase();
- filtered = filtered.filter(u =>
- u.name?.toLowerCase().includes(q) ||
- u.description?.toLowerCase().includes(q) ||
- u.id?.toLowerCase().includes(q)
- );
- }
- return filtered;
- }
- if (filter === 'contacts') {
- const all = await this.listInhabitants({ filter: 'all' });
- const result = [];
- for (const user of all) {
- const rel = await friend.getRelationship(user.id);
- if (rel.following) result.push(user);
- }
- return Array.from(new Map(result.map(u => [u.id, u])).values());
- }
- if (filter === 'blocked') {
- const all = await this.listInhabitants({ filter: 'all' });
- const result = [];
- for (const user of all) {
- const rel = await friend.getRelationship(user.id);
- if (rel.blocking) result.push({ ...user, isBlocked: true });
- }
- return Array.from(new Map(result.map(u => [u.id, u])).values());
- }
- if (filter === 'SUGGESTED') {
- const all = await this.listInhabitants({ filter: 'all' });
- const result = [];
- for (const user of all) {
- if (user.id === userId) continue;
- const rel = await friend.getRelationship(user.id);
- if (!rel.following && !rel.blocking && rel.followsMe) {
- const cv = await this.getCVByUserId(user.id);
- if (cv) result.push({ ...this._normalizeCurriculum(cv), mutualCount: 1 });
- }
- }
- return Array.from(new Map(result.map(u => [u.id, u])).values())
- .sort((a, b) => (b.mutualCount || 0) - (a.mutualCount || 0));
- }
- if (filter === 'CVs' || filter === 'MATCHSKILLS') {
- const records = await new Promise((res, rej) => {
- pull(
- ssbClient.createLogStream({ limit: logLimit }),
- pull.filter(msg =>
- msg.value.content?.type === 'curriculum' &&
- msg.value.content?.type !== 'tombstone'
- ),
- pull.collect((err, msgs) => err ? rej(err) : res(msgs))
- );
- });
- let cvs = records.map(r => this._normalizeCurriculum(r.value.content));
- cvs = Array.from(new Map(cvs.map(u => [u.id, u])).values());
- if (filter === 'CVs') {
- if (search) {
- const q = search.toLowerCase();
- cvs = cvs.filter(u =>
- u.name.toLowerCase().includes(q) ||
- u.description.toLowerCase().includes(q) ||
- u.skills.some(s => s.toLowerCase().includes(q))
- );
- }
- if (location) {
- cvs = cvs.filter(u => u.location?.toLowerCase() === location.toLowerCase());
- }
- if (language) {
- cvs = cvs.filter(u => u.languages.map(l => l.toLowerCase()).includes(language.toLowerCase()));
- }
- if (skills) {
- const skillList = skills.split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
- cvs = cvs.filter(u => skillList.every(s => u.skills.map(k => k.toLowerCase()).includes(s)));
- }
- return cvs;
- }
- if (filter === 'MATCHSKILLS') {
- const cv = await this.getCVByUserId();
- const userSkills = cv
- ? [
- ...cv.personalSkills,
- ...cv.oasisSkills,
- ...cv.educationalSkills,
- ...cv.professionalSkills
- ].map(s => s.toLowerCase())
- : [];
- if (!userSkills.length) return [];
- const matches = cvs.map(c => {
- if (c.id === userId) return null;
- const common = c.skills.map(s => s.toLowerCase()).filter(s => userSkills.includes(s));
- if (!common.length) return null;
- const matchScore = common.length / userSkills.length;
- return { ...c, commonSkills: common, matchScore };
- }).filter(Boolean);
- return matches.sort((a, b) => b.matchScore - a.matchScore);
- }
- }
- return [];
- },
- _normalizeCurriculum(c) {
- const photo =
- typeof c.photo === 'string'
- ? `/image/256/${encodeURIComponent(c.photo)}`
- : '/assets/images/default-avatar.png';
- return {
- id: c.author,
- name: c.name,
- description: c.description,
- photo,
- skills: [
- ...c.personalSkills,
- ...c.oasisSkills,
- ...c.educationalSkills,
- ...c.professionalSkills
- ],
- location: c.location,
- languages: typeof c.languages === 'string'
- ? c.languages.split(',').map(x => x.trim())
- : Array.isArray(c.languages) ? c.languages : [],
- createdAt: c.createdAt
- };
- },
-
- async getLatestAboutById(id) {
- const ssbClient = await openSsb();
- const records = await new Promise((res, rej) => {
- pull(
- ssbClient.createUserStream({ id }),
- pull.filter(msg =>
- msg.value.content?.type === 'about' &&
- msg.value.content?.type !== 'tombstone'
- ),
- pull.collect((err, msgs) => err ? rej(err) : res(msgs))
- );
- });
- if (!records.length) return null;
- const latest = records.sort((a, b) => b.value.timestamp - a.value.timestamp)[0];
- return latest.value.content;
- },
-
- async getFeedByUserId(id) {
- const ssbClient = await openSsb();
- const targetId = id || ssbClient.id;
- const records = await new Promise((res, rej) => {
- pull(
- ssbClient.createUserStream({ id: targetId }),
- pull.filter(msg =>
- msg.value &&
- msg.value.content &&
- typeof msg.value.content.text === 'string' &&
- msg.value.content?.type !== 'tombstone'
- ),
- pull.collect((err, msgs) => err ? rej(err) : res(msgs))
- );
- });
- return records
- .filter(m => typeof m.value.content.text === 'string')
- .sort((a, b) => b.value.timestamp - a.value.timestamp)
- .slice(0, 10);
- },
- async getCVByUserId(id) {
- const ssbClient = await openSsb();
- const targetId = id || ssbClient.id;
- const records = await new Promise((res, rej) => {
- pull(
- ssbClient.createUserStream({ id: targetId }),
- pull.filter(msg =>
- msg.value.content?.type === 'curriculum' &&
- msg.value.content?.type !== 'tombstone'
- ),
- pull.collect((err, msgs) => err ? rej(err) : res(msgs))
- );
- });
- return records.length ? records[records.length - 1].value.content : null;
- }
- };
- };
|