pixelia_model.js 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. const pull = require('../server/node_modules/pull-stream');
  2. const { getConfig } = require('../configs/config-manager.js');
  3. const logLimit = getConfig().ssbLogStream?.limit || 1000;
  4. module.exports = ({ cooler }) => {
  5. let ssb;
  6. const openSsb = async () => {
  7. if (!ssb) ssb = await cooler.open();
  8. return ssb;
  9. };
  10. const getPixelByCoordinate = async (coordinateKey) => {
  11. const ssbClient = await openSsb();
  12. const messages = await new Promise((res, rej) => {
  13. pull(
  14. ssbClient.createLogStream({ limit: logLimit }),
  15. pull.collect((err, msgs) => err ? rej(err) : res(msgs))
  16. );
  17. });
  18. const tombstoned = new Set(
  19. messages
  20. .filter(m => m.value?.content?.type === 'tombstone' && m.value?.content?.target)
  21. .map(m => m.value.content.target)
  22. );
  23. const replaces = new Map();
  24. const byId = new Map();
  25. for (const m of messages) {
  26. const c = m.value?.content;
  27. const k = m.key;
  28. if (!c || c.type !== 'pixelia' || c.coordinateKey !== coordinateKey) continue;
  29. if (tombstoned.has(k)) continue;
  30. if (c.replaces) replaces.set(c.replaces, k);
  31. byId.set(k, m);
  32. }
  33. for (const r of replaces.keys()) {
  34. byId.delete(r);
  35. }
  36. return [...byId.values()][0] || null;
  37. };
  38. const paintPixel = async (x, y, color) => {
  39. if (x < 1 || x > 50 || y < 1 || y > 200) {
  40. throw new Error('Coordinates out of bounds. Please use x (1-50) and y (1-200)');
  41. }
  42. const ssbClient = await openSsb();
  43. const userId = ssbClient.id;
  44. const coordinateKey = `${x}:${y}`;
  45. const existingPixel = await getPixelByCoordinate(coordinateKey);
  46. if (existingPixel) {
  47. const tombstone = {
  48. type: 'tombstone',
  49. target: existingPixel.key,
  50. deletedAt: new Date().toISOString()
  51. };
  52. await new Promise((resolve, reject) =>
  53. ssbClient.publish(tombstone, err => err ? reject(err) : resolve())
  54. );
  55. }
  56. const contributors = existingPixel?.value?.content?.contributors_inhabitants || [];
  57. const contributors_inhabitants = contributors.includes(userId)
  58. ? contributors
  59. : [...contributors, userId];
  60. const content = {
  61. type: 'pixelia',
  62. x: x - 1,
  63. y: y - 1,
  64. color,
  65. author: userId,
  66. contributors_inhabitants,
  67. timestamp: Date.now(),
  68. coordinateKey,
  69. replaces: existingPixel?.key || null
  70. };
  71. await new Promise((resolve, reject) => {
  72. ssbClient.publish(content, (err) => err ? reject(err) : resolve());
  73. });
  74. };
  75. const listPixels = async () => {
  76. const ssbClient = await openSsb();
  77. const messages = await new Promise((res, rej) => {
  78. pull(
  79. ssbClient.createLogStream({ limit: logLimit }),
  80. pull.collect((err, msgs) => err ? rej(err) : res(msgs))
  81. );
  82. });
  83. const tombstoned = new Set();
  84. const replaces = new Map();
  85. const byKey = new Map();
  86. for (const m of messages) {
  87. const c = m.value?.content;
  88. const k = m.key;
  89. if (!c) continue;
  90. if (c.type === 'tombstone' && c.target) {
  91. tombstoned.add(c.target);
  92. continue;
  93. }
  94. if (c.type === 'pixelia') {
  95. if (tombstoned.has(k)) continue;
  96. if (c.replaces) replaces.set(c.replaces, k);
  97. byKey.set(k, m);
  98. }
  99. }
  100. for (const replaced of replaces.keys()) {
  101. byKey.delete(replaced);
  102. }
  103. return Array.from(byKey.values()).map(m => ({
  104. x: m.value.content.x + 1,
  105. y: m.value.content.y + 1,
  106. color: m.value.content.color,
  107. author: m.value.content.author,
  108. contributors_inhabitants: m.value.content.contributors_inhabitants || [],
  109. timestamp: m.value.timestamp
  110. }));
  111. };
  112. return {
  113. paintPixel,
  114. listPixels
  115. };
  116. };