From 6ede632507a88eed7c699a8297ab5a2577c86042 Mon Sep 17 00:00:00 2001 From: trivernis Date: Fri, 24 Jan 2020 14:49:16 +0100 Subject: [PATCH] Add differentiation between media and text posts Add type argument when creating posts. If the post is of type MEDIA it will be invisible until an image was uploaded for the post. --- CHANGELOG.md | 1 + src/lib/dataAccess.ts | 24 +++++++++++- src/lib/models/Media.ts | 4 +- src/lib/models/Post.ts | 7 ++++ src/lib/models/User.ts | 8 +++- src/routes/UploadRoute.ts | 2 + src/routes/graphql/MutationResolver.ts | 52 +++++++++++++------------- src/routes/graphql/schema.graphql | 14 ++++++- 8 files changed, 80 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13425a5..267adef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Upload handling for media entries (via /upload) - routine to cleanup orphaned media entries (not referenced by post, user, group) - delete handler for media to delete the corresponding file +- type for create post to know if it is a media or text post (media posts are invisible until a media file is uploaded) ### Removed diff --git a/src/lib/dataAccess.ts b/src/lib/dataAccess.ts index 9668b4c..e821e4e 100644 --- a/src/lib/dataAccess.ts +++ b/src/lib/dataAccess.ts @@ -179,6 +179,9 @@ namespace dataaccess { limit: first, offset, order: [["createdAt", "DESC"]], + where: { + visible: true, + }, }); } else { // more performant way to get the votes with plain sql @@ -206,15 +209,17 @@ namespace dataaccess { * @param content * @param authorId * @param activityId + * @param type */ - export async function createPost(content: string, authorId: number, activityId?: number): Promise { + export async function createPost(content: string, authorId: number, activityId?: number, + type: PostType = PostType.TEXT): Promise { const blacklisted = await checkBlacklisted(content); if (blacklisted.length > 0) { throw new BlacklistedError(blacklisted.map((p) => p.phrase), "content"); } const activity = await models.Activity.findByPk(activityId); if (!activityId || activity) { - const post = await models.Post.create({content, authorId, activityId}); + const post = await models.Post.create({content, authorId, activityId, visible: type !== PostType.MEDIA}); globals.internalEmitter.emit(InternalEvents.POSTCREATE, post); if (activity) { const user = await models.User.findByPk(authorId); @@ -236,11 +241,15 @@ namespace dataaccess { const post = await models.Post.findByPk(postId, {include: [{model: Activity}, {association: "rAuthor"}]}); const activity = await post.activity(); const author = await post.author(); + const media = await post.$get("rMedia") as models.Media; if (activity && author) { author.rankpoints -= activity.points; await author.save(); } await post.destroy(); + if (media) { + await media.destroy(); + } } catch (err) { globals.logger.error(err.message); globals.logger.debug(err.stack); @@ -417,6 +426,17 @@ namespace dataaccess { NEW = "NEW", } + /** + * The type of the post + */ + export enum PostType { + TEXT = "TEXT", + MEDIA = "MEDIA", + } + + /** + * Enum representing the type of membership change for the membership change function + */ export enum MembershipChangeAction { ADD, REMOVE, diff --git a/src/lib/models/Media.ts b/src/lib/models/Media.ts index 6bf6e84..a9440d5 100644 --- a/src/lib/models/Media.ts +++ b/src/lib/models/Media.ts @@ -14,8 +14,8 @@ export class Media extends Model { * @param instance */ @BeforeDestroy - public static deleteMediaFile(instance: Media) { - fsx.unlinkSync(instance.path); + public static async deleteMediaFile(instance: Media) { + await fsx.unlink(instance.path); } /** diff --git a/src/lib/models/Post.ts b/src/lib/models/Post.ts index 9463981..c39c545 100644 --- a/src/lib/models/Post.ts +++ b/src/lib/models/Post.ts @@ -31,6 +31,13 @@ export class Post extends Model { @Column({type: sqz.STRING(2048), allowNull: false}) public content: string; + /** + * If the post is publically visible + */ + @NotNull + @Column({defaultValue: true, allowNull: false}) + public visible: boolean; + /** * The id of the post author */ diff --git a/src/lib/models/User.ts b/src/lib/models/User.ts index f8f71f1..a98fc88 100644 --- a/src/lib/models/User.ts +++ b/src/lib/models/User.ts @@ -305,11 +305,15 @@ export class User extends Model { * a list of posts the user has created * @param first * @param offset + * @param request */ - public async posts({first, offset}: { first: number, offset: number }): Promise { + public async posts({first, offset}: { first: number, offset: number }, request: any): Promise { const limit = first ?? 10; offset = offset ?? 0; - return await this.$get("rPosts", {limit, offset}) as Post[]; + if (request.session.userId === this.getDataValue("id")) { + return await this.$get("rPosts", { limit, offset, order: [["id", "desc"]]}) as Post[]; + } + return await this.$get("rPosts", { limit, offset, where: {visible: true}, order: [["id", "desc"]]}) as Post[]; } /** diff --git a/src/routes/UploadRoute.ts b/src/routes/UploadRoute.ts index 95b9d2f..2899788 100644 --- a/src/routes/UploadRoute.ts +++ b/src/routes/UploadRoute.ts @@ -216,6 +216,8 @@ export class UploadRoute extends Route { } if (media) { await post.$set("rMedia", media); + post.visible = true; + await post.save(); fileName = media.url; success = true; } diff --git a/src/routes/graphql/MutationResolver.ts b/src/routes/graphql/MutationResolver.ts index edf8f60..c54db01 100644 --- a/src/routes/graphql/MutationResolver.ts +++ b/src/routes/graphql/MutationResolver.ts @@ -2,7 +2,7 @@ import {GraphQLError} from "graphql"; import {FileUpload} from "graphql-upload"; import * as yaml from "js-yaml"; import isEmail from "validator/lib/isEmail"; -import dataaccess from "../../lib/dataAccess"; +import dataAccess from "../../lib/dataAccess"; import {BlacklistedError} from "../../lib/errors/BlacklistedError"; import {GroupNotFoundError} from "../../lib/errors/GroupNotFoundError"; import {InvalidEmailError} from "../../lib/errors/InvalidEmailError"; @@ -53,7 +53,7 @@ export class MutationResolver extends BaseResolver { * @param request */ public async login({email, passwordHash}: { email: string, passwordHash: string }, request: any): Promise { - const user = await dataaccess.getUserByLogin(email, passwordHash); + const user = await dataAccess.getUserByLogin(email, passwordHash); request.session.userId = user.id; return user; } @@ -96,7 +96,7 @@ export class MutationResolver extends BaseResolver { if (!mailValid) { throw new InvalidEmailError(email); } - const user = await dataaccess.registerUser(username, email, passwordHash); + const user = await dataAccess.registerUser(username, email, passwordHash); request.session.userId = user.id; return user; } @@ -124,8 +124,8 @@ export class MutationResolver extends BaseResolver { * @param type * @param request */ - public async vote({postId, type}: { postId: number, type: dataaccess.VoteType }, request: any): - Promise<{ post: Post, voteType: dataaccess.VoteType }> { + public async vote({postId, type}: { postId: number, type: dataAccess.VoteType }, request: any): + Promise<{ post: Post, voteType: dataAccess.VoteType }> { this.ensureLoggedIn(request); const post = await Post.findByPk(postId); if (post) { @@ -143,15 +143,17 @@ export class MutationResolver extends BaseResolver { * Creates a new post * @param content * @param activityId + * @param type * @param request */ - public async createPost({content, activityId}: { content: string, activityId?: number}, - request: any): Promise { + public async createPost( + {content, activityId, type}: { content: string, activityId?: number, type: dataAccess.PostType}, + request: any): Promise { this.ensureLoggedIn(request); if (content.length > 2048) { throw new GraphQLError("Content too long."); } - const post = await dataaccess.createPost(content, request.session.userId, activityId); + const post = await dataAccess.createPost(content, request.session.userId, activityId, type); globals.internalEmitter.emit(InternalEvents.GQLPOSTCREATE, post); return post; } @@ -171,7 +173,7 @@ export class MutationResolver extends BaseResolver { }); const isAdmin = (await User.findOne({where: {id: request.session.userId}})).isAdmin; if (post.rAuthor.id === request.session.userId || isAdmin) { - return await dataaccess.deletePost(post.id); + return await dataAccess.deletePost(post.id); } else { throw new GraphQLError("User is not author of the post."); } @@ -188,7 +190,7 @@ export class MutationResolver extends BaseResolver { if (members) { chatMembers.push(...members); } - return await dataaccess.createChat(...chatMembers); + return await dataAccess.createChat(...chatMembers); } /** @@ -200,7 +202,7 @@ export class MutationResolver extends BaseResolver { public async sendMessage({chatId, content}: { chatId: number, content: string }, request: any): Promise { this.ensureLoggedIn(request); - const message = await dataaccess.sendChatMessage(request.session.userId, chatId, content); + const message = await dataAccess.sendChatMessage(request.session.userId, chatId, content); globals.internalEmitter.emit(InternalEvents.GQLCHATMESSAGE, message); return message; } @@ -211,10 +213,10 @@ export class MutationResolver extends BaseResolver { * @param type * @param request */ - public async sendRequest({receiver, type}: { receiver: number, type: dataaccess.RequestType }, request: any): + public async sendRequest({receiver, type}: { receiver: number, type: dataAccess.RequestType }, request: any): Promise { this.ensureLoggedIn(request); - return dataaccess.createRequest(request.session.userId, receiver, type); + return dataAccess.createRequest(request.session.userId, receiver, type); } /** @@ -223,7 +225,7 @@ export class MutationResolver extends BaseResolver { * @param type * @param request */ - public async denyRequest({sender, type}: { sender: number, type: dataaccess.RequestType }, request: any) { + public async denyRequest({sender, type}: { sender: number, type: dataAccess.RequestType }, request: any) { this.ensureLoggedIn(request); const user = await User.findByPk(request.session.userId); await user.acceptRequest(sender, type); @@ -236,7 +238,7 @@ export class MutationResolver extends BaseResolver { * @param type * @param request */ - public async acceptRequest({sender, type}: { sender: number, type: dataaccess.RequestType }, request: any) { + public async acceptRequest({sender, type}: { sender: number, type: dataAccess.RequestType }, request: any) { this.ensureLoggedIn(request); const user = await User.findByPk(request.session.userId); await user.acceptRequest(sender, type); @@ -262,7 +264,7 @@ export class MutationResolver extends BaseResolver { */ public async createGroup({name, members}: { name: string, members: number[] }, request: any): Promise { this.ensureLoggedIn(request); - return await dataaccess.createGroup(name, request.session.userId, members); + return await dataAccess.createGroup(name, request.session.userId, members); } /** @@ -291,8 +293,8 @@ export class MutationResolver extends BaseResolver { */ public async joinGroup({groupId}: { groupId: number }, request: any): Promise { this.ensureLoggedIn(request); - return dataaccess.changeGroupMembership(groupId, request.session.userId, - dataaccess.MembershipChangeAction.ADD); + return dataAccess.changeGroupMembership(groupId, request.session.userId, + dataAccess.MembershipChangeAction.ADD); } /** @@ -302,8 +304,8 @@ export class MutationResolver extends BaseResolver { */ public async leaveGroup({groupId}: { groupId: number }, request: any): Promise { this.ensureLoggedIn(request); - return dataaccess.changeGroupMembership(groupId, request.session.userId, - dataaccess.MembershipChangeAction.REMOVE); + return dataAccess.changeGroupMembership(groupId, request.session.userId, + dataAccess.MembershipChangeAction.REMOVE); } /** @@ -319,8 +321,8 @@ export class MutationResolver extends BaseResolver { if (group && !(await group.$has("rAdmins", user)) && (await group.creator()) !== user.id) { throw new NotAGroupAdminError(groupId); } - return dataaccess.changeGroupMembership(groupId, userId, - dataaccess.MembershipChangeAction.OP); + return dataAccess.changeGroupMembership(groupId, userId, + dataAccess.MembershipChangeAction.OP); } /** @@ -341,8 +343,8 @@ export class MutationResolver extends BaseResolver { throw new GraphQLError( "You are not allowed to remove a creator as an admin."); } - return await dataaccess.changeGroupMembership(groupId, userId, - dataaccess.MembershipChangeAction.DEOP); + return await dataAccess.changeGroupMembership(groupId, userId, + dataAccess.MembershipChangeAction.DEOP); } /** @@ -361,7 +363,7 @@ export class MutationResolver extends BaseResolver { if (!(await group.$has("rAdmins", user))) { throw new NotAGroupAdminError(groupId); } - const blacklisted = await dataaccess.checkBlacklisted(name); + const blacklisted = await dataAccess.checkBlacklisted(name); if (blacklisted.length > 0) { throw new BlacklistedError(blacklisted.map((p) => p.phrase), "event name"); } diff --git a/src/routes/graphql/schema.graphql b/src/routes/graphql/schema.graphql index 41b009a..5ed3592 100644 --- a/src/routes/graphql/schema.graphql +++ b/src/routes/graphql/schema.graphql @@ -85,7 +85,7 @@ type Mutation { sendMessage(chatId: ID!, content: String!): ChatMessage "create a post that can belong to an activity" - createPost(content: String!, activityId: ID): Post! + createPost(content: String!, activityId: ID, type: PostType = TEXT): Post! "delete the post for a given post id" deletePost(postId: ID!): Boolean! @@ -317,6 +317,9 @@ type Post { "the text of the post" content: String + "If the post is publically visible" + visible: Boolean! + "the content of the post rendered by markdown-it" htmlContent: String @@ -543,7 +546,16 @@ enum RequestType { EVENTINVITE } +"the type of sorting for getPosts" enum SortType { TOP NEW } +""" +The type of the post. If the post was created with the type MEDIA, +It stays invisible until a media file has been uploaded for the post +""" +enum PostType { + MEDIA + TEXT +}