You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
452 lines
15 KiB
452 lines
15 KiB
import {
|
|
FastifyInstance,
|
|
Plugin,
|
|
DefaultQuery,
|
|
DefaultParams,
|
|
DefaultHeaders,
|
|
DefaultBody,
|
|
RouteShorthandOptions,
|
|
} from 'fastify'
|
|
|
|
import { Server, IncomingMessage, ServerResponse } from 'http'
|
|
|
|
import { SHORT_TEXT_LENGTH, SUBSCRIBER_MAX_SIZE } from '../../constants'
|
|
import { userSchema, postSchema, errorSchema } from '../../schemas'
|
|
import { unauthorizedError, serverError, badRequestError, badRequestFormError, notFoundError } from '../../lib/errors'
|
|
import { trimContent, createPostId } from '../../lib/util'
|
|
import { getUsers, getApprovedSubscriptions, getUserBlocks } from '../../lib/collections'
|
|
import { containerFor, createQuerySpec, queryItems, getItem, normalize } from '../../lib/database'
|
|
|
|
import {
|
|
Post,
|
|
PostAttachment,
|
|
User,
|
|
UserPost,
|
|
UserSubscription,
|
|
UserTimelinePost,
|
|
GroupBlock,
|
|
PostRelationship,
|
|
Status,
|
|
} from '../../types/collections'
|
|
|
|
import { PluginOptions } from '../../types'
|
|
|
|
function doPostRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
|
|
interface Body {
|
|
text?: string
|
|
cover?: string
|
|
visible: boolean
|
|
status?: Status
|
|
attachments: PostAttachment[]
|
|
parent: string
|
|
}
|
|
|
|
const options: RouteShorthandOptions = {
|
|
schema: {
|
|
body: {
|
|
type: 'object',
|
|
required: ['visible'],
|
|
properties: {
|
|
text: { type: 'string' },
|
|
cover: {
|
|
type: 'string',
|
|
maxLength: SHORT_TEXT_LENGTH,
|
|
},
|
|
visible: { type: 'boolean' },
|
|
status: {
|
|
type: 'object',
|
|
required: ['date'],
|
|
properties: {
|
|
imageUrl: { type: 'string' },
|
|
text: {
|
|
type: 'string',
|
|
maxLength: SHORT_TEXT_LENGTH,
|
|
},
|
|
date: { type: 'number' },
|
|
},
|
|
},
|
|
attachments: {
|
|
type: 'array',
|
|
items: {
|
|
type: 'object',
|
|
required: ['imageUrl'],
|
|
properties: {
|
|
imageUrl: { type: 'string' },
|
|
caption: {
|
|
type: 'string',
|
|
maxLength: SHORT_TEXT_LENGTH,
|
|
},
|
|
cover: {
|
|
type: 'string',
|
|
maxLength: SHORT_TEXT_LENGTH,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
parent: { type: 'string' },
|
|
},
|
|
},
|
|
response: {
|
|
200: {
|
|
type: 'object',
|
|
properties: {
|
|
id: { type: 'string' },
|
|
},
|
|
},
|
|
400: errorSchema,
|
|
},
|
|
},
|
|
}
|
|
|
|
server.post<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/api/post', options, async (request, reply) => {
|
|
if (!server.database) return serverError(reply)
|
|
if (!request.viewer) return unauthorizedError(reply)
|
|
|
|
let newPostRelationship: PostRelationship | undefined
|
|
|
|
const postContainer = containerFor(server.database.client, 'Posts')
|
|
const ancestryContainer = containerFor(server.database.client, 'Ancestry')
|
|
const userContainer = containerFor(server.database.client, 'Users')
|
|
|
|
const viewer = await getItem<User>({
|
|
container: userContainer,
|
|
id: request.viewer.id,
|
|
logger: request.log
|
|
})
|
|
|
|
if (!viewer) return serverError(reply)
|
|
if (viewer.pending) return badRequestError(reply, 'User requires approval')
|
|
if (!viewer.group) return badRequestError(reply, 'User must belong to a group')
|
|
|
|
const postId = createPostId()
|
|
|
|
if (request.body.parent) {
|
|
const parent = await getItem<Post>({
|
|
container: postContainer,
|
|
id: request.body.parent,
|
|
logger: request.log
|
|
})
|
|
|
|
if (!parent) return badRequestFormError(reply, 'parent', 'Invalid parent')
|
|
|
|
const parentRelationship = await getItem<PostRelationship>({
|
|
container: ancestryContainer,
|
|
id: request.body.parent,
|
|
partitionKey: parent.root,
|
|
logger: request.log
|
|
})
|
|
|
|
const parents = parentRelationship ? parentRelationship.parents : []
|
|
|
|
newPostRelationship = {
|
|
id: postId,
|
|
pk: parent.root,
|
|
parents: [
|
|
...parents,
|
|
parent.id,
|
|
]
|
|
}
|
|
}
|
|
|
|
const post: Post = {
|
|
id: postId,
|
|
pk: postId,
|
|
t: 'post',
|
|
userId: request.viewer.id,
|
|
root: newPostRelationship ? newPostRelationship.pk : postId,
|
|
parents: newPostRelationship ? newPostRelationship.parents : [],
|
|
text: trimContent(request.body.text, 1000),
|
|
cover: trimContent(request.body.cover),
|
|
visible: request.body.visible,
|
|
attachments: [],
|
|
awards: 0,
|
|
latestAwards: [],
|
|
created: Date.now(),
|
|
}
|
|
|
|
const userPost: UserPost = {
|
|
postId,
|
|
pk: request.viewer.id,
|
|
t: 'post',
|
|
created: Date.now(),
|
|
}
|
|
|
|
await postContainer.items.create<Post>(post)
|
|
await userContainer.items.create<UserPost>(userPost)
|
|
|
|
if (newPostRelationship) await ancestryContainer.items.create<PostRelationship>(newPostRelationship)
|
|
|
|
const query = createQuerySpec(`SELECT u.id FROM Users u WHERE u.pk = @pk AND u.type = 'subscription'`, { pk: request.viewer.id })
|
|
const subscribers = await queryItems<UserSubscription>({
|
|
container: userContainer,
|
|
query,
|
|
logger: request.log
|
|
})
|
|
|
|
if (subscribers.length < SUBSCRIBER_MAX_SIZE) {
|
|
for (const subscriber of subscribers) {
|
|
await userContainer.items.create<UserTimelinePost>({
|
|
postId,
|
|
pk: subscriber.id!,
|
|
t: 'timeline',
|
|
created: Date.now(),
|
|
})
|
|
}
|
|
}
|
|
|
|
return {
|
|
id: postId,
|
|
}
|
|
})
|
|
}
|
|
|
|
function postsByUserRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
|
|
interface Params {
|
|
id: string
|
|
}
|
|
|
|
const options: RouteShorthandOptions = {
|
|
schema: {
|
|
params: {
|
|
type: 'object',
|
|
properties: {
|
|
id: { type: 'string' },
|
|
},
|
|
},
|
|
response: {
|
|
200: {
|
|
type: 'object',
|
|
properties: {
|
|
user: userSchema,
|
|
posts: {
|
|
type: 'array',
|
|
items: postSchema,
|
|
},
|
|
},
|
|
},
|
|
400: errorSchema,
|
|
},
|
|
},
|
|
}
|
|
|
|
server.get<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/api/user/:id/posts', options, async (request, reply) => {
|
|
if (!server.database) return serverError(reply)
|
|
|
|
const id = normalize(request.params.id)
|
|
const userContainer = containerFor(server.database.client, 'Users')
|
|
const user = await getItem<User>({
|
|
container: userContainer,
|
|
id,
|
|
logger: request.log
|
|
})
|
|
|
|
if (!user) return notFoundError(reply)
|
|
if (!user.group) return notFoundError(reply)
|
|
|
|
switch (user.privacy) {
|
|
case 'private':
|
|
return unauthorizedError(reply)
|
|
case 'subscribers': {
|
|
if (!request.viewer) return unauthorizedError(reply)
|
|
|
|
const subscriptions = await getApprovedSubscriptions(server.database.client, user.id, request.viewer.id, request.log)
|
|
if (subscriptions.length === 0) return unauthorizedError(reply)
|
|
|
|
break
|
|
}
|
|
case 'group': {
|
|
if (!request.viewer) return unauthorizedError(reply)
|
|
|
|
const viewer = await getItem<User>({
|
|
container: userContainer,
|
|
id: request.viewer.id,
|
|
logger: request.log
|
|
})
|
|
|
|
if (!viewer) return serverError(reply)
|
|
if (!viewer.group) return unauthorizedError(reply)
|
|
|
|
const subscriptions = await getApprovedSubscriptions(server.database.client, user.id, request.viewer.id, request.log)
|
|
if (viewer.group.id !== user.group.id && subscriptions.length === 0) return unauthorizedError(reply)
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
if (request.viewer) {
|
|
const viewer = await getItem<User>({
|
|
container: userContainer,
|
|
id: request.viewer.id,
|
|
logger: request.log
|
|
})
|
|
|
|
if (!viewer) return serverError(reply)
|
|
if (!viewer.group) return unauthorizedError(reply)
|
|
|
|
const blocks = await getUserBlocks(server.database.client, user.id, [viewer.id, viewer.group.id], request.log)
|
|
if (blocks.length > 0) return unauthorizedError(reply)
|
|
}
|
|
|
|
const userPostsQuery = createQuerySpec(`SELECT p.id FROM Users p WHERE p.pk = @user AND p.type = 'post'`, { user: id })
|
|
const userPosts = await queryItems<UserPost>({
|
|
container: userContainer,
|
|
query: userPostsQuery,
|
|
logger: request.log
|
|
})
|
|
|
|
const posts = await queryItems<Post>({
|
|
container: containerFor(server.database.client, 'Posts'),
|
|
query: createQuerySpec('SELECT * FROM Posts p WHERE ARRAY_CONTAINS(@posts, p.id)', {
|
|
posts: userPosts.map(p => p.id!),
|
|
}),
|
|
logger: request.log
|
|
})
|
|
|
|
return {
|
|
user,
|
|
posts,
|
|
}
|
|
})
|
|
}
|
|
|
|
function postRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
|
|
interface Params {
|
|
id: string
|
|
}
|
|
|
|
const options: RouteShorthandOptions = {
|
|
schema: {
|
|
params: {
|
|
type: 'object',
|
|
properties: {
|
|
id: { type: 'string' },
|
|
},
|
|
},
|
|
response: {
|
|
200: {
|
|
type: 'object',
|
|
properties: {
|
|
post: postSchema,
|
|
descendants: {
|
|
type: 'array',
|
|
items: postSchema,
|
|
},
|
|
ancestors: {
|
|
type: 'array',
|
|
items: postSchema,
|
|
},
|
|
users: {
|
|
type: 'array',
|
|
items: userSchema,
|
|
}
|
|
},
|
|
},
|
|
400: errorSchema,
|
|
},
|
|
},
|
|
}
|
|
|
|
server.get<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/api/post/:id', options, async (request, reply) => {
|
|
if (!server.database) return serverError(reply)
|
|
|
|
const postContainer = containerFor(server.database.client, 'Posts')
|
|
const post = await getItem<Post>({
|
|
container: postContainer,
|
|
id: request.params.id,
|
|
logger: request.log
|
|
})
|
|
|
|
if (!post) return notFoundError(reply)
|
|
|
|
const query = createQuerySpec('SELECT * FROM Ancestry a WHERE a.pk = @pk AND ARRAY_CONTAINS(a.parents, @id)', {
|
|
pk: post.root,
|
|
id: post.id,
|
|
})
|
|
|
|
const descendantRelationships = await queryItems<PostRelationship>({
|
|
container: containerFor(server.database.client, 'Ancestry'),
|
|
query,
|
|
logger: request.log
|
|
})
|
|
|
|
const descendants = await queryItems<Post>({
|
|
container: postContainer,
|
|
query: createQuerySpec('SELECT * FROM Posts p WHERE ARRAY_CONTAINS(@descendants, p.id)', {
|
|
descendants: descendantRelationships.map(r => r.id),
|
|
}),
|
|
logger: request.log
|
|
})
|
|
|
|
const ancestors = await queryItems<Post>({
|
|
container: postContainer,
|
|
query: createQuerySpec('SELECT * FROM Posts p WHERE ARRAY_CONTAINS(@parents, p.id)', {
|
|
parents: post.parents,
|
|
}),
|
|
logger: request.log
|
|
})
|
|
|
|
const getUserId = (post: Post) => post.userId
|
|
|
|
const userIds = [
|
|
...descendants.map(getUserId),
|
|
...ancestors.map(getUserId),
|
|
getUserId(post),
|
|
]
|
|
|
|
const users = await getUsers(server.database.client, userIds, request.log)
|
|
|
|
if (request.viewer) {
|
|
const viewer = await getItem<User>({
|
|
container: containerFor(server.database.client, 'Users'),
|
|
id: request.viewer.id,
|
|
logger: request.log
|
|
})
|
|
|
|
if (!viewer) return serverError(reply)
|
|
if (!viewer.group) return unauthorizedError(reply)
|
|
|
|
const blockQuery = createQuerySpec(`
|
|
SELECT g.userId FROM Groups g WHERE
|
|
g.pk = @viewerGroup AND
|
|
g.t = 'block' AND
|
|
(g.blockedId = @viewer OR g.blockedId = @viewerGroup)
|
|
ARRAY_CONTAINS(@ids, g.userId)
|
|
`, {
|
|
viewer: viewer.id,
|
|
viewerGroup: viewer.group.id,
|
|
ids: userIds,
|
|
})
|
|
|
|
const blocks = await queryItems<GroupBlock>({
|
|
container: containerFor(server.database.client, 'Groups'),
|
|
query: blockQuery,
|
|
logger: request.log
|
|
})
|
|
const blockedUserIds = blocks.map(b => b.userId)
|
|
|
|
if (blockedUserIds.includes(post.userId)) return unauthorizedError(reply)
|
|
|
|
return {
|
|
post,
|
|
descendants: descendants.filter(p => !blockedUserIds.includes(p.userId)),
|
|
ancestors: ancestors.filter(p => !blockedUserIds.includes(p.userId)),
|
|
users: users.filter(u => !blockedUserIds.includes(u.id)),
|
|
}
|
|
}
|
|
|
|
return {
|
|
post,
|
|
descendants,
|
|
ancestors,
|
|
users,
|
|
}
|
|
})
|
|
}
|
|
|
|
const plugin: Plugin<Server, IncomingMessage, ServerResponse, PluginOptions> = async server => {
|
|
doPostRoute(server)
|
|
postsByUserRoute(server)
|
|
postRoute(server)
|
|
}
|
|
|
|
export default plugin
|