Merge pull request #89 from shocknet/feat/walls

Feat/walls
This commit is contained in:
Daniel Lugo 2020-06-26 20:58:42 -04:00 committed by GitHub
commit 9c8d36e380
8 changed files with 480 additions and 3 deletions

View file

@ -7,6 +7,8 @@ const logger = require('winston')
Gun.log = () => {}
// @ts-ignore
require('gun/lib/open')
// @ts-ignore
require('gun/lib/load')
const debounce = require('lodash/debounce')
const Encryption = require('../../../utils/encryptionStore')

View file

@ -50,6 +50,7 @@ export interface GUNNodeBase {
once(this: GUNNode, cb?: Listener): GUNNode
open(this: GUNNode, cb?: OpenListener): GUNNode
load(this: GUNNode, cb?: OpenListener): GUNNode
load(this: GUNNode, cb?: LoadListener): GUNNode

View file

@ -3,7 +3,8 @@
*/
const uuidv1 = require('uuid/v1')
const logger = require('winston')
const { Constants, Schema } = require('shock-common')
const Common = require('shock-common')
const { Constants, Schema } = Common
const { ErrorCode } = Constants
@ -1133,7 +1134,21 @@ const setBio = (bio, user) =>
resolve()
}
})
})
}).then(
() =>
new Promise((resolve, reject) => {
user
.get(Key.PROFILE)
.get(Key.BIO)
.put(bio, ack => {
if (ack.err) {
reject(new Error(ack.err))
} else {
resolve()
}
})
})
)
/**
* @param {string[]} mnemonicPhrase
@ -1216,8 +1231,229 @@ const setLastSeenApp = () =>
res()
}
})
}).then(
() =>
new Promise((res, rej) => {
require('../Mediator')
.getUser()
.get(Key.PROFILE)
.get(Key.LAST_SEEN_APP)
.put(Date.now(), ack => {
if (ack.err) {
rej(new Error(ack.err))
} else {
res()
}
})
})
)
/**
* @param {string[]} tags
* @param {string} title
* @param {Common.Schema.ContentItem[]} content
* @returns {Promise<Common.Schema.Post>}
*/
const createPost = async (tags, title, content) => {
if (content.length === 0) {
throw new Error(`A post must contain at least one paragraph/image/video`)
}
const numOfPages = await (async () => {
const maybeNumOfPages = await Utils.tryAndWait(
(_, user) =>
user
.get(Key.WALL)
.get(Key.NUM_OF_PAGES)
.then(),
v => typeof v !== 'number'
)
return typeof maybeNumOfPages === 'number' ? maybeNumOfPages : 0
})()
let pageIdx = Math.max(0, numOfPages - 1).toString()
const count = await (async () => {
if (numOfPages === 0) {
return 0
}
const maybeCount = await Utils.tryAndWait(
(_, user) =>
user
.get(Key.WALL)
.get(Key.PAGES)
.get(pageIdx)
.get(Key.COUNT)
.then(),
v => typeof v !== 'number'
)
return typeof maybeCount === 'number' ? maybeCount : 0
})()
const shouldBeNewPage =
count >= Common.Constants.Misc.NUM_OF_POSTS_PER_WALL_PAGE
if (shouldBeNewPage) {
pageIdx = Number(pageIdx + 1).toString()
}
await new Promise((res, rej) => {
require('../Mediator')
.getUser()
.get(Key.WALL)
.get(Key.PAGES)
.get(pageIdx)
.put(
{
[Key.COUNT]: shouldBeNewPage ? 1 : count + 1,
posts: {
unused: null
}
},
ack => {
if (ack.err) {
rej(new Error(ack.err))
}
res()
}
)
})
/** @type {string} */
const postID = await new Promise((res, rej) => {
const _n = require('../Mediator')
.getUser()
.get(Key.WALL)
.get(Key.PAGES)
.get(pageIdx)
.get(Key.POSTS)
.set(
{
date: Date.now(),
status: 'publish',
tags: tags.join('-'),
title
},
ack => {
if (ack.err) {
rej(new Error(ack.err))
} else {
res(_n._.get)
}
}
)
})
if (shouldBeNewPage || numOfPages === 0) {
await new Promise(res => {
require('../Mediator')
.getUser()
.get(Key.WALL)
.get(Key.NUM_OF_PAGES)
.put(numOfPages + 1, ack => {
if (ack.err) {
throw new Error(ack.err)
}
res()
})
})
}
const contentItems = require('../Mediator')
.getUser()
.get(Key.WALL)
.get(Key.PAGES)
.get(pageIdx)
.get(Key.POSTS)
.get(postID)
.get(Key.CONTENT_ITEMS)
try {
await Promise.all(
content.map(
ci =>
new Promise(res => {
// @ts-ignore
contentItems.set(ci, ack => {
if (ack.err) {
throw new Error(ack.err)
}
res()
})
})
)
)
} catch (e) {
await new Promise(res => {
require('../Mediator')
.getUser()
.get(Key.WALL)
.get(Key.PAGES)
.get(pageIdx)
.get(Key.POSTS)
.get(postID)
.put(null, ack => {
if (ack.err) {
throw new Error(ack.err)
}
res()
})
})
throw e
}
const loadedPost = await new Promise(res => {
require('../Mediator')
.getUser()
.get(Key.WALL)
.get(Key.PAGES)
.get(pageIdx)
.get(Key.POSTS)
.get(postID)
.load(data => {
res(data)
})
})
/** @type {Common.Schema.User} */
const userForPost = await Getters.getMyUser()
/** @type {Common.Schema.Post} */
const completePost = {
...loadedPost,
author: userForPost,
id: postID
}
if (!Common.Schema.isPost(completePost)) {
throw new Error(
`completePost not a Post inside Actions.createPost(): ${JSON.stringify(
createPost
)}`
)
}
return completePost
}
/**
* @param {string} postId
* @returns {Promise<void>}
*/
const deletePost = async postId => {
await new Promise(res => {
res(postId)
})
}
/**
* @param {string} publicKey
* @param {boolean} isPrivate Will overwrite previous private status.
@ -1285,6 +1521,8 @@ module.exports = {
saveChannelsBackup,
disconnect,
setLastSeenApp,
createPost,
deletePost,
follow,
unfollow
}

View file

@ -1,9 +1,13 @@
/**
* @format
*/
const Common = require('shock-common')
const Key = require('../key')
const Utils = require('../utils')
const Wall = require('./wall')
/**
* @param {string} pub
* @returns {Promise<string>}
@ -39,4 +43,54 @@ exports.userToIncomingID = async pub => {
return null
}
/**
* @returns {Promise<Common.SchemaTypes.User>}
*/
const getMyUser = async () => {
const oldProfile = await Utils.tryAndWait(
(_, user) => new Promise(res => user.get(Key.PROFILE).load(res)),
v => typeof v !== 'object'
)
const bio = await Utils.tryAndWait(
(_, user) => user.get(Key.BIO).then(),
v => typeof v !== 'string'
)
const lastSeenApp = await Utils.tryAndWait(
(_, user) => user.get(Key.LAST_SEEN_APP).then(),
v => typeof v !== 'number'
)
const lastSeenNode = await Utils.tryAndWait(
(_, user) => user.get(Key.LAST_SEEN_NODE).then(),
v => typeof v !== 'number'
)
const publicKey = await Utils.tryAndWait(
(_, user) => Promise.resolve(user.is && user.is.pub),
v => typeof v !== 'string'
)
/** @type {Common.SchemaTypes.User} */
const u = {
avatar: oldProfile.avatar,
// @ts-ignore
bio,
displayName: oldProfile.displayName,
// @ts-ignore
lastSeenApp,
// @ts-ignore
lastSeenNode,
// @ts-ignore
publicKey
}
return u
}
module.exports.getMyUser = getMyUser
module.exports.Follows = require('./follows')
module.exports.getWallPage = Wall.getWallPage
module.exports.getWallTotalPages = Wall.getWallTotalPages

View file

@ -0,0 +1,114 @@
/**
* @format
*/
const Common = require('shock-common')
const Utils = require('../utils')
const Key = require('../key')
/**
* @returns {Promise<number>}
*/
const getWallTotalPages = async () => {
const totalPages = await Utils.tryAndWait(
(_, user) =>
user
.get(Key.WALL)
.get(Key.NUM_OF_PAGES)
.then(),
v => typeof v !== 'number'
)
return typeof totalPages === 'number' ? totalPages : 0
}
/**
* @param {number} page
* @throws {TypeError}
* @throws {RangeError}
* @returns {Promise<Common.SchemaTypes.WallPage>}
*/
const getWallPage = async page => {
const totalPages = await getWallTotalPages()
if (page === 0 || totalPages === 0) {
return {
count: 0,
posts: {}
}
}
const actualPageIdx = page < 0 ? totalPages + page : page - 1
if (actualPageIdx > totalPages - 1) {
throw new RangeError(`Requested a page out of bounds`)
}
/**
* @type {Common.SchemaTypes.WallPage}
*/
const thePage = await Utils.tryAndWait(
(_, user) =>
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
}
const clean = {
...maybePage
}
// @ts-ignore
for (const [key, post] of Object.entries(clean.posts)) {
// delete unsuccessful writes
if (post === null) {
// @ts-ignore
delete clean.posts[key]
} else {
post.id = key
}
}
// .load() sometimes doesn't load all data on first call
// @ts-ignore
if (Object.keys(clean.posts).length === 0) {
return true
}
return !Common.Schema.isWallPage(clean)
}
)
const clean = {
...thePage
}
for (const [key, post] of Object.entries(clean.posts)) {
// delete unsuccessful writes
if (post === null) {
delete clean.posts[key]
} else {
post.id = key
}
}
if (!Common.Schema.isWallPage(clean)) {
throw new Error(
`Fetched page not a wall page, instead got: ${JSON.stringify(clean)}`
)
}
return clean
}
module.exports = {
getWallTotalPages,
getWallPage
}

View file

@ -37,6 +37,15 @@ const lastSeenNode = user => {
logger.error(`Error inside lastSeenNode job: ${ack.err}`)
}
})
user
.get(Key.PROFILE)
.get(Key.LAST_SEEN_NODE)
.put(Date.now(), ack => {
if (ack.err) {
logger.error(`Error inside lastSeenNode job: ${ack.err}`)
}
})
}
}, LAST_SEEN_NODE_INTERVAL)
}

View file

@ -43,4 +43,15 @@ exports.LAST_SEEN_APP = 'lastSeenApp'
exports.LAST_SEEN_NODE = 'lastSeenNode'
exports.WALL = 'wall'
exports.NUM_OF_PAGES = 'numOfPages'
exports.PAGES = 'pages'
exports.COUNT = 'count'
exports.CONTENT_ITEMS = 'contentItems'
exports.FOLLOWS = 'follows'
exports.POSTS = 'posts'

View file

@ -12,6 +12,7 @@ const httpsAgent = require("https");
const responseTime = require("response-time");
const uuid = require("uuid/v4");
const Common = require('shock-common')
const isARealUsableNumber = require('lodash/isFinite')
const getListPage = require("../utils/paginate");
const auth = require("../services/auth/auth");
@ -1689,7 +1690,7 @@ module.exports = async (
const GunEvent = Common.Constants.Event
const Key = require('../services/gunDB/contact-api/key')
app.get("/api/gun/lndchanbackups", async (req,res) => {
try{
const user = require('../services/gunDB/Mediator').getUser()
@ -1839,7 +1840,54 @@ module.exports = async (
})
}
})
////////////////////////////////////////////////////////////////////////////////
app.get(`/api/gun/wall`, async (req, res) => {
try {
const { page } = req.query;
const pageNum = Number(page)
if (!isARealUsableNumber(pageNum)) {
return res.status(400).json({
field: 'page',
errorMessage: 'Not a number'
})
}
const totalPages = await GunGetters.getWallTotalPages()
const fetchedPage = await GunGetters.getWallPage(pageNum)
return res.status(200).json({
...fetchedPage,
totalPages,
})
} catch (err) {
return res.status(500).json({
errorMessage: err.message
})
}
})
app.post(`/api/gun/wall/`, async (req,res) => {
try{
const {tags,title,contentItems} = req.body
return res.status(200).json(await GunActions.createPost(
tags,
title,
contentItems
))
} catch(e) {
return res.status(500).json({
errorMessage: (typeof e === 'string' ? e : e.message)
|| 'Unknown error.'
})
}
})
app.delete(`/api/gun/wall/:postID`, (_, res) => res.status(200).json({
ok: 'true'
}))
/////////////////////////////////
/**
* @template P