Dwayne Harris 5 years ago
parent
commit
de4610ff62
  1. 330
      src/plugins/api/posts.ts

330
src/plugins/api/posts.ts

@ -6,11 +6,15 @@ import {
DefaultHeaders,
DefaultBody,
RouteShorthandOptions,
FastifyReply,
Logger,
} from 'fastify'
import { Server, IncomingMessage, ServerResponse } from 'http'
import { createHmac } from 'crypto'
import { CosmosClient } from '@azure/cosmos'
import { SHORT_TEXT_LENGTH, SUBSCRIBER_MAX_SIZE } from '../../constants'
import { SHORT_TEXT_LENGTH, SUBSCRIBER_MAX_SIZE, INSTALLATION_PARTITION_KEY, APP_PARTITION_KEY } from '../../constants'
import { userSchema, postSchema, errorSchema } from '../../schemas'
import { unauthorizedError, serverError, badRequestError, badRequestFormError, notFoundError } from '../../lib/errors'
import { trimContent, createPostId } from '../../lib/utils'
@ -31,65 +35,158 @@ import {
UserItemType,
UserPrivacyType,
GroupItemType,
Installation,
App,
Group,
} 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
}
interface PostBody {
text?: string
cover?: string
visible: boolean
status?: Status
attachments: PostAttachment[]
parent: string
}
const options: RouteShorthandOptions = {
schema: {
body: {
const postBodySchema = {
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: ['visible'],
required: ['imageUrl'],
properties: {
text: { type: 'string' },
cover: {
imageUrl: { type: 'string' },
caption: {
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,
},
},
},
cover: {
type: 'string',
maxLength: SHORT_TEXT_LENGTH,
},
parent: { type: 'string' },
},
},
},
parent: { type: 'string' },
},
}
async function createPost(client: CosmosClient, userId: string, body: PostBody, reply: FastifyReply<ServerResponse>, logger: Logger) {
let newPostRelationship: PostRelationship | undefined
const postContainer = containerFor(client, 'Posts')
const ancestryContainer = containerFor(client, 'Ancestry')
const userContainer = containerFor(client, 'Users')
const viewer = await getItem<User>({ container: userContainer, id: userId })
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 (body.parent) {
const parent = await getItem<Post>({ container: postContainer, id: body.parent })
if (!parent) return badRequestFormError(reply, 'parent', 'Invalid parent')
const parentRelationship = await getItem<PostRelationship>({
container: ancestryContainer,
id: body.parent,
partitionKey: parent.root,
})
const parents = parentRelationship ? parentRelationship.parents : []
newPostRelationship = {
id: postId,
pk: parent.root,
parents: [
...parents,
parent.id,
]
}
}
const post: Post = {
id: postId,
pk: postId,
t: PostItemType.Post,
userId,
root: newPostRelationship ? newPostRelationship.pk : postId,
parents: newPostRelationship ? newPostRelationship.parents : [],
text: trimContent(body.text, 1000),
cover: trimContent(body.cover),
visible: body.visible,
attachments: [],
awards: 0,
latestAwards: [],
created: Date.now(),
}
const userPost: UserPost = {
postId,
pk: userId,
t: UserItemType.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.t = @type`, { pk: userId, type: UserItemType.Subscription })
const subscribers = await queryItems<UserSubscription>({
container: userContainer,
query,
logger,
})
if (subscribers.length < SUBSCRIBER_MAX_SIZE) {
for (const subscriber of subscribers) {
await userContainer.items.create<UserTimelinePost>({
postId,
pk: subscriber.id!,
t: UserItemType.Timeline,
created: Date.now(),
})
}
}
return {
id: postId,
}
}
function createPostRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
const options: RouteShorthandOptions = {
schema: {
body: postBodySchema,
response: {
200: {
type: 'object',
@ -102,95 +199,75 @@ function doPostRoute(server: FastifyInstance<Server, IncomingMessage, ServerResp
},
}
server.post<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/api/post', options, async (request, reply) => {
server.post<DefaultQuery, DefaultParams, DefaultHeaders, PostBody>('/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 })
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 })
if (!parent) return badRequestFormError(reply, 'parent', 'Invalid parent')
return await createPost(server.database.client, request.viewer.id, request.body, reply, request.log)
})
}
const parentRelationship = await getItem<PostRelationship>({
container: ancestryContainer,
id: request.body.parent,
partitionKey: parent.root,
})
function createAppPostRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
interface Headers {
installation: string
timestamp: string
signature: string
}
const parents = parentRelationship ? parentRelationship.parents : []
const options: RouteShorthandOptions = {
schema: {
headers: {
type: 'object',
properties: {
installation: { type: 'string' },
timestamp: { type: 'string' },
signature: { type: 'string' },
},
},
body: postBodySchema,
response: {
200: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
400: errorSchema,
},
},
}
newPostRelationship = {
id: postId,
pk: parent.root,
parents: [
...parents,
parent.id,
]
}
}
server.post<DefaultQuery, DefaultParams, Headers, PostBody>('/api/app/post', options, async (request, reply) => {
if (!server.database) return serverError(reply)
const post: Post = {
id: postId,
pk: postId,
t: PostItemType.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 { installation: installationId, timestamp, signature } = request.headers
const userPost: UserPost = {
postId,
pk: request.viewer.id,
t: UserItemType.Post,
created: Date.now(),
}
if (!installationId) return badRequestError(reply, '"installation" header required')
if (!timestamp) return badRequestError(reply, '"timestamp" header required')
if (!signature) return badRequestError(reply, '"signature" header required')
await postContainer.items.create<Post>(post)
await userContainer.items.create<UserPost>(userPost)
const container = containerFor(server.database.client, 'Apps')
const installation = await getItem<Installation>({
container,
id: installationId,
partitionKey: INSTALLATION_PARTITION_KEY,
})
if (newPostRelationship) await ancestryContainer.items.create<PostRelationship>(newPostRelationship)
if (!installation) return badRequestError(reply, 'Installation not found')
const query = createQuerySpec(`SELECT u.id FROM Users u WHERE u.pk = @pk AND u.t = @type`, { pk: request.viewer.id, type: UserItemType.Subscription })
const subscribers = await queryItems<UserSubscription>({
container: userContainer,
query,
logger: request.log
const app = await getItem<App>({
container,
id: installation.appId,
partitionKey: APP_PARTITION_KEY,
})
if (subscribers.length < SUBSCRIBER_MAX_SIZE) {
for (const subscriber of subscribers) {
await userContainer.items.create<UserTimelinePost>({
postId,
pk: subscriber.id!,
t: UserItemType.Timeline,
created: Date.now(),
})
}
}
if (!app) return serverError(reply)
return {
id: postId,
if (createHmac('sha256', app.privateKey).update(installationId + timestamp).digest('hex') !== signature) {
return badRequestError(reply, 'Invalid signature')
}
return await createPost(server.database.client, installation.userId, request.body, reply, request.log)
})
}
@ -276,14 +353,19 @@ function postsByUserRoute(server: FastifyInstance<Server, IncomingMessage, Serve
const posts = await queryItems<Post>({
container: containerFor(server.database.client, 'Posts'),
query: createQuerySpec('SELECT * FROM Posts p WHERE ARRAY_CONTAINS(@posts, p.id)', {
query: createQuerySpec('SELECT * FROM Posts p WHERE ARRAY_CONTAINS(@posts, p.id) ORDER BY p.created DESC', {
posts: userPosts.map(p => p.postId!),
}),
logger: request.log
})
const group = await getItem<Group>({ container: containerFor(server.database.client, 'Groups'), id: user.group.id })
return {
user,
user: {
...user,
group,
},
posts,
}
})
@ -415,7 +497,7 @@ function postRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespon
}
const plugin: Plugin<Server, IncomingMessage, ServerResponse, PluginOptions> = async server => {
doPostRoute(server)
createPostRoute(server)
postsByUserRoute(server)
postRoute(server)
}

Loading…
Cancel
Save