diff --git a/package.json b/package.json index 10e7a609..68b03834 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "lodash": "^4.17.15", "method-override": "^2.3.7", "promise": "^8.0.1", + "ramda": "^0.27.0", "request": "^2.87.0", "request-promise": "^4.2.2", "response-time": "^2.3.2", @@ -61,6 +62,7 @@ "@types/jest": "^24.0.18", "@types/jsonwebtoken": "^8.3.7", "@types/lodash": "^4.14.141", + "@types/ramda": "types/npm-ramda#dist", "@types/socket.io": "^2.1.4", "@types/uuid": "^3.4.5", "babel-eslint": "^10.0.3", diff --git a/services/gunDB/contact-api/getters/feed.js b/services/gunDB/contact-api/getters/feed.js new file mode 100644 index 00000000..e0cce941 --- /dev/null +++ b/services/gunDB/contact-api/getters/feed.js @@ -0,0 +1,79 @@ +/** + * @format + */ +const Common = require('shock-common') +const isFinite = require('lodash/isFinite') +const shuffle = require('lodash/shuffle') +const R = require('ramda') + +const Follows = require('./follows') +const Wall = require('./wall') + +/** + * @param {number} numberOfPublicKeyGroups + * @param {number} pageRequested + * @returns {[ number , number ]} + */ +const calculateFeedPage = (numberOfPublicKeyGroups, pageRequested) => { + // thanks to sebassdc + + return [ + (pageRequested - 1) % numberOfPublicKeyGroups, + Math.ceil(pageRequested / numberOfPublicKeyGroups) + ] +} + +/** + * @param {number} page + * @throws {TypeError} + * @throws {RangeError} + * @returns {Promise} + */ +const getFeedPage = async page => { + if (!isFinite(page)) { + throw new TypeError(`Please provide an actual number for [page]`) + } + + if (page <= 0) { + throw new RangeError(`Please provide only positive numbers for [page]`) + } + + const subbedPublicKeys = Object.values(await Follows.currentFollows()).map( + f => f.user + ) + + if (subbedPublicKeys.length === 0) { + return [] + } + + // say there are 20 public keys total + // page 1: page 1 from first 10 public keys + // page 2: page 1 from second 10 public keys + // page 3: page 2 from first 10 public keys + // page 4: page 2 from first 10 public keys + // etc + // thanks to sebassdc (github) + + const pagedPublicKeys = R.splitEvery(10, shuffle(subbedPublicKeys)) + + const [publicKeyGroupIdx, pageToRequest] = calculateFeedPage( + pagedPublicKeys.length, + page + ) + + const publicKeys = pagedPublicKeys[publicKeyGroupIdx] + + const fetchedPages = await Promise.all( + publicKeys.map(pk => Wall.getWallPage(pageToRequest, pk)) + ) + + const fetchedPostsGroups = fetchedPages.map(wp => Object.values(wp.posts)) + const fetchedPosts = R.flatten(fetchedPostsGroups) + const sortered = R.sort((a, b) => b.date - a.date, fetchedPosts) + + return sortered +} + +module.exports = { + getFeedPage +} diff --git a/services/gunDB/contact-api/getters/index.js b/services/gunDB/contact-api/getters/index.js index 64b5a531..3a82e410 100644 --- a/services/gunDB/contact-api/getters/index.js +++ b/services/gunDB/contact-api/getters/index.js @@ -7,6 +7,7 @@ const Key = require('../key') const Utils = require('../utils') const Wall = require('./wall') +const Feed = require('./feed') const User = require('./user') /** @@ -96,4 +97,5 @@ module.exports.Follows = require('./follows') module.exports.getWallPage = Wall.getWallPage module.exports.getWallTotalPages = Wall.getWallTotalPages -module.exports.getUser = User.getAnUser +module.exports.getFeedPage = Feed.getFeedPage +module.exports.getAnUser = User.getAnUser diff --git a/services/gunDB/contact-api/getters/wall.js b/services/gunDB/contact-api/getters/wall.js index 0c0b19d7..f639780e 100644 --- a/services/gunDB/contact-api/getters/wall.js +++ b/services/gunDB/contact-api/getters/wall.js @@ -9,15 +9,19 @@ const Key = require('../key') const Wall = require('./user') /** + * @param {string=} publicKey * @returns {Promise} */ -const getWallTotalPages = async () => { +const getWallTotalPages = async publicKey => { const totalPages = await Utils.tryAndWait( - (_, user) => - user + (gun, u) => { + const user = publicKey ? gun.get(`~${publicKey}`) : u + + return user .get(Key.WALL) .get(Key.NUM_OF_PAGES) - .then(), + .then() + }, v => typeof v !== 'number' ) @@ -26,12 +30,13 @@ const getWallTotalPages = async () => { /** * @param {number} page + * @param {string=} publicKey * @throws {TypeError} * @throws {RangeError} * @returns {Promise} */ -const getWallPage = async page => { - const totalPages = await getWallTotalPages() +const getWallPage = async (page, publicKey) => { + const totalPages = await getWallTotalPages(publicKey) if (page === 0 || totalPages === 0) { return { @@ -50,15 +55,18 @@ const getWallPage = async page => { * @type {Common.SchemaTypes.WallPage} */ const thePage = await Utils.tryAndWait( - (_, user) => - new Promise(res => { + (g, u) => { + const user = publicKey ? g.get(`~${publicKey}`) : u + + return new Promise(res => { user .get(Key.WALL) .get(Key.PAGES) .get(actualPageIdx.toString()) // @ts-ignore .load(res) - }), + }) + }, maybePage => { if (typeof maybePage !== 'object' || maybePage === null) { return true @@ -97,9 +105,13 @@ const getWallPage = async page => { // delete unsuccessful writes if (post === null) { delete clean.posts[key] + clean.count-- } else { - // eslint-disable-next-line no-await-in-loop - post.author = await Wall.getMyUser() + post.author = publicKey + ? // eslint-disable-next-line no-await-in-loop + await Wall.getAnUser(publicKey) + : // eslint-disable-next-line no-await-in-loop + await Wall.getMyUser() post.id = key } } diff --git a/src/routes.js b/src/routes.js index d925f631..35914618 100644 --- a/src/routes.js +++ b/src/routes.js @@ -1842,9 +1842,10 @@ module.exports = async ( }) //////////////////////////////////////////////////////////////////////////////// - app.get(`/api/gun/wall`, async (req, res) => { + app.get(`/api/gun/wall/:publicKey?`, async (req, res) => { try { const { page } = req.query; + const {publicKey} = req.params const pageNum = Number(page) @@ -1855,8 +1856,8 @@ module.exports = async ( }) } - const totalPages = await GunGetters.getWallTotalPages() - const fetchedPage = await GunGetters.getWallPage(pageNum) + const totalPages = await GunGetters.getWallTotalPages(publicKey) + const fetchedPage = await GunGetters.getWallPage(pageNum, publicKey) return res.status(200).json({ ...fetchedPage, @@ -1969,6 +1970,40 @@ module.exports = async ( ap.put(`/api/gun/follows/:publicKey`,apiGunFollowsPut) ap.delete(`/api/gun/follows/:publicKey`, apiGunFollowsDelete) + /** + * @type {RequestHandler<{}>} + */ + const apiGunFeedGet = async (req, res) => { + try { + const { page: pageStr } = req.query; + const page = Number(pageStr) + + if (!isARealUsableNumber(page)) { + return res.status(400).json({ + field: 'page', + errorMessage: 'page must be a number' + }) + } + + if (page < 1) { + return res.status(400).json({ + field: page, + errorMessage: 'page must be a positive number' + }) + } + + return res.status(200).json({ + posts: await GunGetters.getFeedPage(page) + }) + } catch (err) { + return res.status(500).json({ + errorMessage: err.message || 'Unknown error inside /api/gun/follows/' + }) + } + } + + ap.get(`/api/gun/feed`, apiGunFeedGet) + /** * Return app so that it can be used by express. */ diff --git a/yarn.lock b/yarn.lock index 64648074..f9da858b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -633,6 +633,10 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== +"@types/ramda@types/npm-ramda#dist": + version "0.25.0" + resolved "https://codeload.github.com/types/npm-ramda/tar.gz/9529aa3c8ff70ff84afcbc0be83443c00f30ea90" + "@types/range-parser@*": version "1.2.3" resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" @@ -5388,6 +5392,11 @@ ramda@^0.26.1: resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06" integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ== +ramda@^0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.27.0.tgz#915dc29865c0800bf3f69b8fd6c279898b59de43" + integrity sha512-pVzZdDpWwWqEVVLshWUHjNwuVP7SfcmPraYuqocJp1yo2U1R7P+5QAfDhdItkuoGqIBnBYrtPp7rEPqDn9HlZA== + random-bytes@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b"