[ABANDONED] API server for Flexor social network.
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.
 

415 lines
13 KiB

import {
FastifyInstance,
Plugin,
DefaultQuery,
DefaultParams,
DefaultBody,
RouteShorthandOptions,
DefaultHeaders,
} from 'fastify'
import { Server, IncomingMessage, ServerResponse } from 'http'
import { MIN_ID_LENGTH, MAX_NAME_LENGTH } from '../../constants'
import { errorSchema, groupListingSchema } from '../../schemas'
import { unauthorizedError, badRequestError, notFoundError, serverError } from '../../lib/errors'
import { containerFor, createQuerySpec, queryItems, getItem, normalize } from '../../lib/database'
import { wait } from '../../lib/util'
import { User, Group, GroupListing, GroupMembership, UserBlock, GroupBlock } from '../../types/collections'
interface PluginOptions {}
function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
interface Body {
name: string
about?: string
open: boolean
requiresApproval: boolean
}
const options: RouteShorthandOptions = {
schema: {
body: {
type: 'object',
required: ['name', 'open', 'requiresApproval'],
properties: {
name: {
type: 'string',
minLength: MIN_ID_LENGTH,
maxLength: MAX_NAME_LENGTH,
},
about: { type: 'string' },
open: { type: 'boolean' },
requiresApproval: { type: 'boolean' },
},
},
response: {
200: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
400: errorSchema,
},
},
}
server.post<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/api/group', options, async (request, reply) => {
if (!server.database) return serverError(reply)
if (!request.viewer) return unauthorizedError(reply)
const viewerItem = containerFor(server.database.client, 'Users').item(request.viewer.id, request.viewer.id)
const { resource: viewer, requestCharge } = await viewerItem.read<User>()
request.log.trace('Get: %d', requestCharge)
const groupContainer = containerFor(server.database.client, 'Groups')
if (viewer.group) return badRequestError(reply)
const { name, about, open, requiresApproval } = request.body
const id = normalize(name)
const existingGroup = await getItem<Group>({
container: groupContainer,
id,
logger: request.log
})
if (existingGroup) return badRequestError(reply, 'Name already used')
const group: Group = {
id: id,
pk: id,
t: 'group',
userId: request.viewer.id,
name,
about,
open,
requiresApproval,
status: 'pending',
active: true,
created: Date.now(),
}
const membership: GroupMembership = {
id: request.viewer.id,
pk: id,
t: 'membership',
userId: request.viewer.id,
pending: false,
membership: 'admin',
created: Date.now(),
}
await groupContainer.items.create(group)
await groupContainer.items.create(membership)
await viewerItem.replace<User>({
...viewer,
group: {
id: group.id,
name: group.name,
imageUrl: group.imageUrl,
coverImageUrl: group.coverImageUrl,
},
})
return {
id: group.id,
}
})
}
function getRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
interface Params {
id: string
}
const options: RouteShorthandOptions = {
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
response: {
200: groupListingSchema,
},
},
}
server.get<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/api/group/:id', options, async (request, reply) => {
if (!server.database) return serverError(reply)
const listing = await getItem<GroupListing>({
container: containerFor(server.database.client, 'GroupDirectory'),
id: request.params.id,
logger: request.log,
})
if (!listing) return notFoundError(reply)
return listing
})
}
function blockRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
interface Params {
id: string
}
interface Body {
description?: string
}
const options: RouteShorthandOptions = {
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
body: {
type: 'object',
properties: {
description: { type: 'string' },
},
},
},
}
server.post<DefaultQuery, Params, DefaultHeaders, Body>('/api/group/:id/block', options, async (request, reply) => {
if (!server.database) return serverError(reply)
if (!request.viewer) return unauthorizedError(reply)
const groupContainer = containerFor(server.database.client, 'Groups')
const group = await getItem<Group>({
container: groupContainer,
id: request.params.id,
logger: request.log,
})
if (!group) return notFoundError(reply)
await containerFor(server.database.client, 'Users').items.create<UserBlock>({
blockedId: group.id,
pk: request.viewer.id,
t: 'block',
blockType: 'group',
description: request.body.description,
created: Date.now(),
})
await groupContainer.items.create<GroupBlock>({
pk: group.id,
t: 'block',
blockedId: group.id,
userId: request.viewer.id,
created: Date.now(),
})
reply.code(204)
})
}
function unblockRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
interface Params {
id: string
}
const options: RouteShorthandOptions = {
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
},
}
server.post<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/api/group/:id/unblock', options, async (request, reply) => {
if (!server.database) return serverError(reply)
if (!request.viewer) return unauthorizedError(reply)
const userContainer = containerFor(server.database.client, 'Users')
const groupContainer = containerFor(server.database.client, 'Groups')
const group = await getItem<Group>({
container: groupContainer,
id: request.params.id,
logger: request.log
})
if (!group) return notFoundError(reply)
const userBlockQuery = createQuerySpec(`SELECT u.id FROM Users u WHERE u.pk = @pk AND u.blockedId = @blocked AND u.type = 'block'`, {
pk: request.viewer.id,
blocked: group.id,
})
const userBlocks = await queryItems<UserBlock>({
container: userContainer,
query: userBlockQuery,
logger: request.log
})
for (const userBlock of userBlocks) {
await userContainer.item(userBlock.id!, request.viewer.id).delete()
}
const groupBlockQuery = createQuerySpec(
`SELECT g.id FROM Groups g WHERE g.pk = @pk AND u.blockedId = @blocked AND u.userId = @viewer AND u.type = 'block'`,
{
pk: group.id,
blocked: group.id,
viewer: request.viewer.id,
}
)
const groupBlocks = await queryItems<UserBlock>({
container: groupContainer,
query: groupBlockQuery,
logger: request.log
})
for (const groupBlock of groupBlocks) {
await groupContainer.item(groupBlock.id!, group.id).delete()
}
reply.code(204)
})
}
function activateRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
interface Params {
id: string
}
const options: RouteShorthandOptions = {
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
},
}
server.post<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/api/group/:id/activate', options, async (request, reply) => {
if (!server.database) return serverError(reply)
const container = containerFor(server.database.client, 'Groups')
const groupItem = container.item(request.params.id, request.params.id)
const { resource: group, requestCharge: groupRequestCharge } = await groupItem.read<Group>()
request.log.trace('Get: %d', groupRequestCharge)
if (!group) return notFoundError(reply)
if (group.active && group.status === 'paid') {
return badRequestError(reply, 'Already activated')
}
await groupItem.replace<Group>({
...group,
active: true,
status: 'paid',
})
const directoryContainer = containerFor(server.database.client, 'GroupDirectory')
const listingItem = directoryContainer.item(request.params.id, 'pk')
const { resource: listing, requestCharge: listingRequestCharge } = await listingItem.read<GroupListing>()
request.log.trace('Get: %d', listingRequestCharge)
if (!listing) {
await directoryContainer.items.create<GroupListing>({
id: group.id,
name: group.name,
pk: 'pk',
open: group.open,
requiresApproval: group.requiresApproval,
members: 0,
posts: 0,
awards: 0,
points: 0,
latestAwards: [],
created: Date.now(),
})
}
reply.code(204)
})
}
function listRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
interface Query {
sort?: string
requiresApproval?: boolean
continuation?: string
}
const options: RouteShorthandOptions = {
schema: {
querystring: {
type: 'object',
properties: {
sort: {
type: 'string',
enum: ['name', 'members', 'points'],
},
continuation: { type: 'string' },
},
},
response: {
200: {
type: 'object',
properties: {
groups: {
type: 'array',
items: groupListingSchema,
},
continuation: { type: 'string' },
}
},
400: errorSchema,
}
},
}
server.get<Query, DefaultParams, DefaultHeaders, DefaultBody>('/api/groups', options, async (request, reply) => {
if (!server.database) return serverError(reply)
const { sort = 'members', requiresApproval, continuation } = request.query
let requiresApprovalString = ''
if (requiresApproval !== undefined) {
requiresApprovalString = `AND d.requiresApproval = ${requiresApproval.toString()}`
}
const container = containerFor(server.database.client, 'GroupDirectory')
const { resources: groups, requestCharge, continuation: newContinuation } = await container.items.query<GroupListing>(
`SELECT * FROM GroupDirectory d WHERE d.pk = 'pk' AND d.open = true ${requiresApprovalString} ORDER BY d.${sort}`,
{
maxItemCount: 40,
continuation,
}
).fetchAll()
request.log.trace('Query: %d', requestCharge)
return {
groups,
continuation: newContinuation,
}
})
}
const plugin: Plugin<Server, IncomingMessage, ServerResponse, PluginOptions> = async server => {
createRoute(server)
getRoute(server)
blockRoute(server)
unblockRoute(server)
activateRoute(server)
listRoute(server)
}
export default plugin