[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.

459 lines
14 KiB

5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
  1. import {
  2. FastifyInstance,
  3. Plugin,
  4. DefaultQuery,
  5. DefaultParams,
  6. RouteShorthandOptions,
  7. DefaultHeaders,
  8. DefaultBody,
  9. } from 'fastify'
  10. import { Server, IncomingMessage, ServerResponse } from 'http'
  11. import { MAX_NAME_LENGTH } from '../../constants'
  12. import { userSchema, errorSchema } from '../../schemas'
  13. import { unauthorizedError, serverError, notFoundError, badRequestError } from '../../lib/errors'
  14. import { getUserBlocks } from '../../lib/collections'
  15. import { containerFor, createQuerySpec, queryItems, getItem, normalize } from '../../lib/database'
  16. import { User, UserSubscription, UserBlock, GroupBlock, UserPrivacyType } from '../../types/collections'
  17. import { PluginOptions } from '../../types'
  18. function availabilityRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
  19. interface Body {
  20. name: string
  21. }
  22. const options: RouteShorthandOptions = {
  23. schema: {
  24. body: {
  25. type: 'object',
  26. required: ['name'],
  27. properties: {
  28. name: {
  29. type: 'string',
  30. maxLength: MAX_NAME_LENGTH,
  31. },
  32. },
  33. },
  34. response: {
  35. 200: {
  36. type: 'object',
  37. properties: {
  38. id: { type: 'string' },
  39. available: { type: 'boolean' },
  40. },
  41. },
  42. 400: errorSchema,
  43. },
  44. },
  45. }
  46. server.post<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/api/user/available', options, async (request, reply) => {
  47. if (!server.database) return serverError(reply)
  48. const id = normalize(request.body.name)
  49. const user = await getItem<User>({
  50. container: containerFor(server.database.client, 'Users'),
  51. id,
  52. logger: request.log,
  53. })
  54. return {
  55. id,
  56. available: !!user,
  57. }
  58. })
  59. }
  60. function updateRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
  61. interface Body {
  62. name?: string
  63. about?: string
  64. requiresApproval?: boolean
  65. privacy?: UserPrivacyType
  66. }
  67. const options: RouteShorthandOptions = {
  68. schema: {
  69. body: {
  70. type: 'object',
  71. properties: {
  72. name: {
  73. type: 'string',
  74. maxLength: MAX_NAME_LENGTH,
  75. },
  76. about: { type: 'string' },
  77. requiresApproval: { type: 'boolean' },
  78. privacy: {
  79. type: 'string',
  80. enum: ['public', 'group', 'subscribers', 'private'],
  81. },
  82. },
  83. },
  84. response: {
  85. 200: userSchema,
  86. 400: errorSchema,
  87. },
  88. },
  89. }
  90. server.put<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/api/self', options, async (request, reply) => {
  91. if (!server.database) return serverError(reply)
  92. if (!request.viewer) return unauthorizedError(reply)
  93. const viewerItem = containerFor(server.database.client, 'Users').item(request.viewer.id, request.viewer.id)
  94. const { resource: viewer, requestCharge } = await viewerItem.read<User>()
  95. request.log.trace('Get: %d', requestCharge)
  96. if (!viewer) return serverError(reply)
  97. if (request.body.name) {
  98. const name = request.body.name.trim()
  99. if (name !== '') {
  100. viewer.name = name
  101. }
  102. }
  103. if (request.body.about) {
  104. const about = request.body.about.trim()
  105. if (about !== '') {
  106. viewer.about = about
  107. }
  108. }
  109. if (request.body.requiresApproval !== undefined) {
  110. viewer.requiresApproval = request.body.requiresApproval
  111. }
  112. if (request.body.privacy) {
  113. viewer.privacy = request.body.privacy
  114. }
  115. await viewerItem.replace<User>(viewer)
  116. return viewer
  117. })
  118. }
  119. function getRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
  120. interface Params {
  121. id: string
  122. }
  123. const options: RouteShorthandOptions = {
  124. schema: {
  125. params: {
  126. type: 'object',
  127. properties: {
  128. id: { type: 'string' },
  129. },
  130. },
  131. response: {
  132. 200: userSchema,
  133. 400: errorSchema,
  134. },
  135. },
  136. }
  137. server.get<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/api/user/:id', options, async (request, reply) => {
  138. if (!server.database) return serverError(reply)
  139. const userContainer = containerFor(server.database.client, 'Users')
  140. const user = await getItem<User>({
  141. container: userContainer,
  142. id: request.params.id,
  143. logger: request.log
  144. })
  145. if (!user) return notFoundError(reply)
  146. if (request.viewer) {
  147. const viewer = await getItem<User>({
  148. container: userContainer,
  149. id: request.viewer.id,
  150. logger: request.log
  151. })
  152. if (!viewer) return serverError(reply)
  153. if (!viewer.group) return unauthorizedError(reply)
  154. const blocks = await getUserBlocks(server.database.client, user.id, [viewer.id, viewer.group.id], request.log)
  155. if (blocks.length > 0) return unauthorizedError(reply)
  156. }
  157. return user
  158. })
  159. }
  160. function subscribeRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
  161. interface Params {
  162. id: string
  163. }
  164. const options: RouteShorthandOptions = {
  165. schema: {
  166. params: {
  167. type: 'object',
  168. properties: {
  169. id: { type: 'string' },
  170. },
  171. },
  172. response: {
  173. 400: errorSchema,
  174. },
  175. },
  176. }
  177. server.post<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/api/user/:id/subscribe', options, async (request, reply) => {
  178. if (!server.database) return serverError(reply)
  179. if (!request.viewer) return unauthorizedError(reply)
  180. if (request.viewer.id === request.params.id) return badRequestError(reply)
  181. const userContainer = containerFor(server.database.client, 'Users')
  182. const user = await getItem<User>({ container: userContainer, id: request.params.id, logger: request.log })
  183. const viewer = await getItem<User>({ container: userContainer, id: request.viewer.id, logger: request.log })
  184. if (!user) return notFoundError(reply)
  185. if (!viewer) return serverError(reply)
  186. if (!viewer.group) return unauthorizedError(reply)
  187. const subscriptionQuery = createQuerySpec(`SELECT u.id FROM Users u WHERE u.subscriberId = @user AND u.pk = @viewer AND u.t = 'subscription'`, {
  188. user: user.id,
  189. viewer: viewer.id,
  190. })
  191. const subscriptions = await queryItems<UserSubscription>({ container: userContainer, query: subscriptionQuery, logger: request.log })
  192. if (subscriptions.length > 0) return badRequestError(reply, 'Already subscribed')
  193. let pending = false
  194. switch (user.privacy) {
  195. case 'private':
  196. return unauthorizedError(reply)
  197. case 'group':
  198. if (user.group !== viewer.group) return unauthorizedError(reply)
  199. case 'subscribers':
  200. pending = true
  201. break
  202. }
  203. const blockQuery = createQuerySpec(`
  204. SELECT g.id FROM Groups g WHERE
  205. g.pk = @viewerGroup AND
  206. g.t = 'block' AND
  207. g.userId = @user AND
  208. (g.blockedId = @viewer OR g.blockedId = @viewerGroup)
  209. `, {
  210. user: user.id,
  211. viewerGroup: viewer.group.id,
  212. })
  213. const blocks = await queryItems<GroupBlock>({
  214. container: containerFor(server.database.client, 'Groups'),
  215. query: blockQuery,
  216. logger: request.log
  217. })
  218. if (blocks.length > 0) return badRequestError(reply, 'Invalid operation')
  219. await userContainer.items.create<UserSubscription>({
  220. subscriberId: user.id,
  221. pk: request.viewer.id,
  222. t: 'subscription',
  223. pending,
  224. created: Date.now(),
  225. })
  226. reply.code(204)
  227. })
  228. }
  229. function unsubscribeRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
  230. interface Params {
  231. id: string
  232. }
  233. const options: RouteShorthandOptions = {
  234. schema: {
  235. params: {
  236. type: 'object',
  237. properties: {
  238. id: { type: 'string' },
  239. },
  240. },
  241. },
  242. }
  243. server.post<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/api/user/:id/unsubscribe', options, async (request, reply) => {
  244. if (!server.database) return serverError(reply)
  245. if (!request.viewer) return unauthorizedError(reply)
  246. const userContainer = containerFor(server.database.client, 'Users')
  247. const user = await getItem<User>({ container: userContainer, id: request.params.id, logger: request.log })
  248. const viewer = await getItem<User>({ container: userContainer, id: request.viewer.id, logger: request.log })
  249. if (!user) return notFoundError(reply)
  250. if (!viewer) return serverError(reply)
  251. const subscriptionQuery = createQuerySpec(`SELECT u.id FROM Users u WHERE u.subscriberId = @user AND u.pk = @viewer AND u.t = 'subscription'`, {
  252. user: user.id,
  253. viewer: viewer.id,
  254. })
  255. const subscriptions = await queryItems<UserSubscription>({ container: userContainer, query: subscriptionQuery, logger: request.log })
  256. for (const subscription of subscriptions) {
  257. await userContainer.item(subscription.id!, viewer.id).delete()
  258. }
  259. reply.code(204)
  260. })
  261. }
  262. function blockRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
  263. interface Params {
  264. id: string
  265. }
  266. interface Body {
  267. description?: string
  268. }
  269. const options: RouteShorthandOptions = {
  270. schema: {
  271. params: {
  272. type: 'object',
  273. properties: {
  274. id: { type: 'string' },
  275. },
  276. },
  277. body: {
  278. type: 'object',
  279. properties: {
  280. description: { type: 'string' },
  281. },
  282. },
  283. response: {
  284. 400: errorSchema,
  285. }
  286. },
  287. }
  288. server.post<DefaultQuery, Params, DefaultHeaders, Body>('/api/user/:id/block', options, async (request, reply) => {
  289. if (!server.database) return serverError(reply)
  290. if (!request.viewer) return unauthorizedError(reply)
  291. const userContainer = containerFor(server.database.client, 'Users')
  292. const user = await getItem<User>({
  293. container: userContainer,
  294. id: request.params.id,
  295. logger: request.log
  296. })
  297. if (!user) return notFoundError(reply)
  298. if (!user.group) return badRequestError(reply)
  299. await userContainer.items.create<UserBlock>({
  300. blockedId: user.id,
  301. pk: request.viewer.id,
  302. t: 'block',
  303. blockType: 'user',
  304. description: request.body.description,
  305. created: Date.now(),
  306. })
  307. await containerFor(server.database.client, 'Groups').items.create<GroupBlock>({
  308. pk: user.group.id,
  309. t: 'block',
  310. blockedId: user.id,
  311. userId: request.viewer.id,
  312. created: Date.now(),
  313. })
  314. reply.code(204)
  315. })
  316. }
  317. function unblockRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
  318. interface Params {
  319. id: string
  320. }
  321. const options: RouteShorthandOptions = {
  322. schema: {
  323. params: {
  324. type: 'object',
  325. properties: {
  326. id: { type: 'string' },
  327. },
  328. },
  329. },
  330. }
  331. server.post<DefaultQuery, Params, DefaultHeaders, Body>('/api/user/:id/unblock', options, async (request, reply) => {
  332. if (!server.database) return serverError(reply)
  333. if (!request.viewer) return unauthorizedError(reply)
  334. const userContainer = containerFor(server.database.client, 'Users')
  335. const groupContainer = containerFor(server.database.client, 'Groups')
  336. const user = await getItem<User>({
  337. container: userContainer,
  338. id: request.params.id,
  339. logger: request.log
  340. })
  341. if (!user) return notFoundError(reply)
  342. if (!user.group) return badRequestError(reply, 'Invalid operation')
  343. const userBlockQuery = createQuerySpec(`SELECT u.id FROM Users u WHERE u.pk = @pk AND u.blockedId = @blocked AND u.type = 'block'`, {
  344. pk: request.viewer.id,
  345. blocked: user.id,
  346. })
  347. const userBlocks = await queryItems<UserBlock>({
  348. container: userContainer,
  349. query: userBlockQuery,
  350. logger: request.log
  351. })
  352. for (const userBlock of userBlocks) {
  353. await userContainer.item(userBlock.id!, request.viewer.id).delete()
  354. }
  355. const groupBlockQuery = createQuerySpec(
  356. `SELECT g.id FROM Groups g WHERE g.pk = @pk AND u.blockedId = @blocked AND u.userId = @viewer AND u.type = 'block'`,
  357. {
  358. pk: user.group.id,
  359. blocked: user.id,
  360. viewer: request.viewer.id,
  361. }
  362. )
  363. const groupBlocks = await queryItems<GroupBlock>({
  364. container: groupContainer,
  365. query: groupBlockQuery,
  366. logger: request.log
  367. })
  368. for (const groupBlock of groupBlocks) {
  369. await groupContainer.item(groupBlock.id!, user.group.id).delete()
  370. }
  371. reply.code(204)
  372. })
  373. }
  374. const plugin: Plugin<Server, IncomingMessage, ServerResponse, PluginOptions> = async server => {
  375. availabilityRoute(server)
  376. updateRoute(server)
  377. getRoute(server)
  378. subscribeRoute(server)
  379. unsubscribeRoute(server)
  380. blockRoute(server)
  381. unblockRoute(server)
  382. }
  383. export default plugin