inhabitants_model.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. const pull = require('../server/node_modules/pull-stream');
  2. const ssbClientGUI = require("../client/gui");
  3. const coolerInstance = ssbClientGUI({ offline: require('../server/ssb_config').offline });
  4. const models = require("../models/main_models");
  5. const { about, friend } = models({
  6. cooler: coolerInstance,
  7. isPublic: require('../server/ssb_config').public,
  8. });
  9. const { getConfig } = require('../configs/config-manager.js');
  10. const logLimit = getConfig().ssbLogStream?.limit || 1000;
  11. module.exports = ({ cooler }) => {
  12. let ssb;
  13. const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
  14. async function getLastKarmaScore(feedId) {
  15. const ssbClient = await openSsb();
  16. return new Promise(resolve => {
  17. const src = ssbClient.messagesByType
  18. ? ssbClient.messagesByType({ type: "karmaScore", reverse: true })
  19. : ssbClient.createLogStream && ssbClient.createLogStream({ reverse: true });
  20. if (!src) return resolve(0);
  21. pull(
  22. src,
  23. pull.filter(msg => {
  24. const v = msg.value || msg;
  25. const c = v.content || {};
  26. return v.author === feedId && c.type === "karmaScore" && typeof c.karmaScore !== "undefined";
  27. }),
  28. pull.take(1),
  29. pull.collect((err, arr) => {
  30. if (err || !arr || !arr.length) return resolve(0);
  31. const v = arr[0].value || arr[0];
  32. resolve(v.content.karmaScore || 0);
  33. })
  34. );
  35. });
  36. }
  37. return {
  38. async listInhabitants(options = {}) {
  39. const { filter = 'all', search = '', location = '', language = '', skills = '' } = options;
  40. const ssbClient = await openSsb();
  41. const userId = ssbClient.id;
  42. const timeoutPromise = (timeout) => new Promise((_, reject) => setTimeout(() => reject('Timeout'), timeout));
  43. const fetchUserImage = (feedId) => {
  44. return Promise.race([
  45. about.image(feedId),
  46. timeoutPromise(5000)
  47. ]).catch(() => '/assets/images/default-avatar.png');
  48. };
  49. if (filter === 'GALLERY') {
  50. const feedIds = await new Promise((res, rej) => {
  51. pull(
  52. ssbClient.createLogStream({ limit: logLimit }),
  53. pull.filter(msg => {
  54. const c = msg.value?.content;
  55. const a = msg.value?.author;
  56. return c &&
  57. c.type === 'about' &&
  58. c.type !== 'tombstone' &&
  59. typeof c.name === 'string' &&
  60. typeof c.about === 'string' &&
  61. c.about === a;
  62. }),
  63. pull.collect((err, msgs) => err ? rej(err) : res(msgs))
  64. );
  65. });
  66. const uniqueFeedIds = Array.from(new Set(feedIds.map(r => r.value.author).filter(Boolean)));
  67. const users = await Promise.all(
  68. uniqueFeedIds.map(async (feedId) => {
  69. const name = await about.name(feedId);
  70. const description = await about.description(feedId);
  71. const image = await fetchUserImage(feedId);
  72. const photo =
  73. typeof image === 'string'
  74. ? `/image/256/${encodeURIComponent(image)}`
  75. : '/assets/images/default-avatar.png';
  76. return { id: feedId, name, description, photo };
  77. })
  78. );
  79. return users;
  80. }
  81. if (filter === 'all' || filter === 'TOP KARMA') {
  82. const feedIds = await new Promise((res, rej) => {
  83. pull(
  84. ssbClient.createLogStream({ limit: logLimit }),
  85. pull.filter(msg => {
  86. const c = msg.value?.content;
  87. const a = msg.value?.author;
  88. return c &&
  89. c.type === 'about' &&
  90. c.type !== 'tombstone' &&
  91. typeof c.name === 'string' &&
  92. typeof c.about === 'string' &&
  93. c.about === a;
  94. }),
  95. pull.collect((err, msgs) => err ? rej(err) : res(msgs))
  96. );
  97. });
  98. const uniqueFeedIds = Array.from(new Set(feedIds.map(r => r.value.author).filter(Boolean)));
  99. let users = await Promise.all(
  100. uniqueFeedIds.map(async (feedId) => {
  101. const name = await about.name(feedId);
  102. const description = await about.description(feedId);
  103. const image = await fetchUserImage(feedId);
  104. const photo =
  105. typeof image === 'string'
  106. ? `/image/256/${encodeURIComponent(image)}`
  107. : '/assets/images/default-avatar.png';
  108. return { id: feedId, name, description, photo };
  109. })
  110. );
  111. users = Array.from(new Map(users.filter(u => u && u.id).map(u => [u.id, u])).values());
  112. if (search) {
  113. const q = search.toLowerCase();
  114. users = users.filter(u =>
  115. u.name?.toLowerCase().includes(q) ||
  116. u.description?.toLowerCase().includes(q) ||
  117. u.id?.toLowerCase().includes(q)
  118. );
  119. }
  120. const withKarma = await Promise.all(users.map(async u => {
  121. const karmaScore = await getLastKarmaScore(u.id);
  122. return { ...u, karmaScore };
  123. }));
  124. if (filter === 'TOP KARMA') {
  125. return withKarma.sort((a, b) => (b.karmaScore || 0) - (a.karmaScore || 0));
  126. }
  127. return withKarma;
  128. }
  129. if (filter === 'contacts') {
  130. const all = await this.listInhabitants({ filter: 'all' });
  131. const result = [];
  132. for (const user of all) {
  133. const rel = await friend.getRelationship(user.id);
  134. if (rel.following) result.push(user);
  135. }
  136. return Array.from(new Map(result.map(u => [u.id, u])).values());
  137. }
  138. if (filter === 'blocked') {
  139. const all = await this.listInhabitants({ filter: 'all' });
  140. const result = [];
  141. for (const user of all) {
  142. const rel = await friend.getRelationship(user.id);
  143. if (rel.blocking) result.push({ ...user, isBlocked: true });
  144. }
  145. return Array.from(new Map(result.map(u => [u.id, u])).values());
  146. }
  147. if (filter === 'SUGGESTED') {
  148. const all = await this.listInhabitants({ filter: 'all' });
  149. const result = [];
  150. for (const user of all) {
  151. if (user.id === userId) continue;
  152. const rel = await friend.getRelationship(user.id);
  153. if (!rel.following && !rel.blocking && rel.followsMe) {
  154. const cv = await this.getCVByUserId(user.id);
  155. if (cv) result.push({ ...this._normalizeCurriculum(cv), mutualCount: 1 });
  156. }
  157. }
  158. return Array.from(new Map(result.map(u => [u.id, u])).values())
  159. .sort((a, b) => (b.mutualCount || 0) - (a.mutualCount || 0));
  160. }
  161. if (filter === 'CVs' || filter === 'MATCHSKILLS') {
  162. const records = await new Promise((res, rej) => {
  163. pull(
  164. ssbClient.createLogStream({ limit: logLimit }),
  165. pull.filter(msg =>
  166. msg.value.content?.type === 'curriculum' &&
  167. msg.value.content?.type !== 'tombstone'
  168. ),
  169. pull.collect((err, msgs) => err ? rej(err) : res(msgs))
  170. );
  171. });
  172. let cvs = records.map(r => this._normalizeCurriculum(r.value.content));
  173. cvs = Array.from(new Map(cvs.map(u => [u.id, u])).values());
  174. if (filter === 'CVs') {
  175. if (search) {
  176. const q = search.toLowerCase();
  177. cvs = cvs.filter(u =>
  178. u.name.toLowerCase().includes(q) ||
  179. u.description.toLowerCase().includes(q) ||
  180. u.skills.some(s => s.toLowerCase().includes(q))
  181. );
  182. }
  183. if (location) {
  184. cvs = cvs.filter(u => u.location?.toLowerCase() === location.toLowerCase());
  185. }
  186. if (language) {
  187. cvs = cvs.filter(u => u.languages.map(l => l.toLowerCase()).includes(language.toLowerCase()));
  188. }
  189. if (skills) {
  190. const skillList = skills.split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
  191. cvs = cvs.filter(u => skillList.every(s => u.skills.map(k => k.toLowerCase()).includes(s)));
  192. }
  193. return cvs;
  194. }
  195. if (filter === 'MATCHSKILLS') {
  196. const cv = await this.getCVByUserId();
  197. const userSkills = cv
  198. ? [
  199. ...cv.personalSkills,
  200. ...cv.oasisSkills,
  201. ...cv.educationalSkills,
  202. ...cv.professionalSkills
  203. ].map(s => s.toLowerCase())
  204. : [];
  205. if (!userSkills.length) return [];
  206. const matches = cvs.map(c => {
  207. if (c.id === userId) return null;
  208. const common = c.skills.map(s => s.toLowerCase()).filter(s => userSkills.includes(s));
  209. if (!common.length) return null;
  210. const matchScore = common.length / userSkills.length;
  211. return { ...c, commonSkills: common, matchScore };
  212. }).filter(Boolean);
  213. return matches.sort((a, b) => b.matchScore - a.matchScore);
  214. }
  215. }
  216. return [];
  217. },
  218. _normalizeCurriculum(c) {
  219. const photo =
  220. typeof c.photo === 'string'
  221. ? `/image/256/${encodeURIComponent(c.photo)}`
  222. : '/assets/images/default-avatar.png';
  223. return {
  224. id: c.author,
  225. name: c.name,
  226. description: c.description,
  227. photo,
  228. skills: [
  229. ...c.personalSkills,
  230. ...c.oasisSkills,
  231. ...c.educationalSkills,
  232. ...c.professionalSkills
  233. ],
  234. location: c.location,
  235. languages: typeof c.languages === 'string'
  236. ? c.languages.split(',').map(x => x.trim())
  237. : Array.isArray(c.languages) ? c.languages : [],
  238. createdAt: c.createdAt
  239. };
  240. },
  241. async getLatestAboutById(id) {
  242. const ssbClient = await openSsb();
  243. const records = await new Promise((res, rej) => {
  244. pull(
  245. ssbClient.createUserStream({ id }),
  246. pull.filter(msg =>
  247. msg.value.content?.type === 'about' &&
  248. msg.value.content?.type !== 'tombstone'
  249. ),
  250. pull.collect((err, msgs) => err ? rej(err) : res(msgs))
  251. );
  252. });
  253. if (!records.length) return null;
  254. const latest = records.sort((a, b) => b.value.timestamp - a.value.timestamp)[0];
  255. return latest.value.content;
  256. },
  257. async getFeedByUserId(id) {
  258. const ssbClient = await openSsb();
  259. const targetId = id || ssbClient.id;
  260. const records = await new Promise((res, rej) => {
  261. pull(
  262. ssbClient.createUserStream({ id: targetId }),
  263. pull.filter(msg =>
  264. msg.value &&
  265. msg.value.content &&
  266. typeof msg.value.content.text === 'string' &&
  267. msg.value.content?.type !== 'tombstone'
  268. ),
  269. pull.collect((err, msgs) => err ? rej(err) : res(msgs))
  270. );
  271. });
  272. return records
  273. .filter(m => typeof m.value.content.text === 'string')
  274. .sort((a, b) => b.value.timestamp - a.value.timestamp)
  275. .slice(0, 10);
  276. },
  277. async getCVByUserId(id) {
  278. const ssbClient = await openSsb();
  279. const targetId = id || ssbClient.id;
  280. const records = await new Promise((res, rej) => {
  281. pull(
  282. ssbClient.createUserStream({ id: targetId }),
  283. pull.filter(msg =>
  284. msg.value.content?.type === 'curriculum' &&
  285. msg.value.content?.type !== 'tombstone'
  286. ),
  287. pull.collect((err, msgs) => err ? rej(err) : res(msgs))
  288. );
  289. });
  290. return records.length ? records[records.length - 1].value.content : null;
  291. }
  292. };
  293. };