inhabitants_model.js 10 KB

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