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.
pull/4/head
trivernis 5 years ago
parent 8dc1424775
commit 6ede632507

@ -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) - Upload handling for media entries (via /upload)
- routine to cleanup orphaned media entries (not referenced by post, user, group) - routine to cleanup orphaned media entries (not referenced by post, user, group)
- delete handler for media to delete the corresponding file - 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 ### Removed

@ -179,6 +179,9 @@ namespace dataaccess {
limit: first, limit: first,
offset, offset,
order: [["createdAt", "DESC"]], order: [["createdAt", "DESC"]],
where: {
visible: true,
},
}); });
} else { } else {
// more performant way to get the votes with plain sql // more performant way to get the votes with plain sql
@ -206,15 +209,17 @@ namespace dataaccess {
* @param content * @param content
* @param authorId * @param authorId
* @param activityId * @param activityId
* @param type
*/ */
export async function createPost(content: string, authorId: number, activityId?: number): Promise<models.Post> { export async function createPost(content: string, authorId: number, activityId?: number,
type: PostType = PostType.TEXT): Promise<models.Post> {
const blacklisted = await checkBlacklisted(content); const blacklisted = await checkBlacklisted(content);
if (blacklisted.length > 0) { if (blacklisted.length > 0) {
throw new BlacklistedError(blacklisted.map((p) => p.phrase), "content"); throw new BlacklistedError(blacklisted.map((p) => p.phrase), "content");
} }
const activity = await models.Activity.findByPk(activityId); const activity = await models.Activity.findByPk(activityId);
if (!activityId || activity) { 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); globals.internalEmitter.emit(InternalEvents.POSTCREATE, post);
if (activity) { if (activity) {
const user = await models.User.findByPk(authorId); 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 post = await models.Post.findByPk(postId, {include: [{model: Activity}, {association: "rAuthor"}]});
const activity = await post.activity(); const activity = await post.activity();
const author = await post.author(); const author = await post.author();
const media = await post.$get("rMedia") as models.Media;
if (activity && author) { if (activity && author) {
author.rankpoints -= activity.points; author.rankpoints -= activity.points;
await author.save(); await author.save();
} }
await post.destroy(); await post.destroy();
if (media) {
await media.destroy();
}
} catch (err) { } catch (err) {
globals.logger.error(err.message); globals.logger.error(err.message);
globals.logger.debug(err.stack); globals.logger.debug(err.stack);
@ -417,6 +426,17 @@ namespace dataaccess {
NEW = "NEW", 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 { export enum MembershipChangeAction {
ADD, ADD,
REMOVE, REMOVE,

@ -14,8 +14,8 @@ export class Media extends Model<Media> {
* @param instance * @param instance
*/ */
@BeforeDestroy @BeforeDestroy
public static deleteMediaFile(instance: Media) { public static async deleteMediaFile(instance: Media) {
fsx.unlinkSync(instance.path); await fsx.unlink(instance.path);
} }
/** /**

@ -31,6 +31,13 @@ export class Post extends Model<Post> {
@Column({type: sqz.STRING(2048), allowNull: false}) @Column({type: sqz.STRING(2048), allowNull: false})
public content: string; public content: string;
/**
* If the post is publically visible
*/
@NotNull
@Column({defaultValue: true, allowNull: false})
public visible: boolean;
/** /**
* The id of the post author * The id of the post author
*/ */

@ -305,11 +305,15 @@ export class User extends Model<User> {
* a list of posts the user has created * a list of posts the user has created
* @param first * @param first
* @param offset * @param offset
* @param request
*/ */
public async posts({first, offset}: { first: number, offset: number }): Promise<Post[]> { public async posts({first, offset}: { first: number, offset: number }, request: any): Promise<Post[]> {
const limit = first ?? 10; const limit = first ?? 10;
offset = offset ?? 0; 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[];
} }
/** /**

@ -216,6 +216,8 @@ export class UploadRoute extends Route {
} }
if (media) { if (media) {
await post.$set("rMedia", media); await post.$set("rMedia", media);
post.visible = true;
await post.save();
fileName = media.url; fileName = media.url;
success = true; success = true;
} }

@ -2,7 +2,7 @@ import {GraphQLError} from "graphql";
import {FileUpload} from "graphql-upload"; import {FileUpload} from "graphql-upload";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
import isEmail from "validator/lib/isEmail"; import isEmail from "validator/lib/isEmail";
import dataaccess from "../../lib/dataAccess"; import dataAccess from "../../lib/dataAccess";
import {BlacklistedError} from "../../lib/errors/BlacklistedError"; import {BlacklistedError} from "../../lib/errors/BlacklistedError";
import {GroupNotFoundError} from "../../lib/errors/GroupNotFoundError"; import {GroupNotFoundError} from "../../lib/errors/GroupNotFoundError";
import {InvalidEmailError} from "../../lib/errors/InvalidEmailError"; import {InvalidEmailError} from "../../lib/errors/InvalidEmailError";
@ -53,7 +53,7 @@ export class MutationResolver extends BaseResolver {
* @param request * @param request
*/ */
public async login({email, passwordHash}: { email: string, passwordHash: string }, request: any): Promise<User> { public async login({email, passwordHash}: { email: string, passwordHash: string }, request: any): Promise<User> {
const user = await dataaccess.getUserByLogin(email, passwordHash); const user = await dataAccess.getUserByLogin(email, passwordHash);
request.session.userId = user.id; request.session.userId = user.id;
return user; return user;
} }
@ -96,7 +96,7 @@ export class MutationResolver extends BaseResolver {
if (!mailValid) { if (!mailValid) {
throw new InvalidEmailError(email); 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; request.session.userId = user.id;
return user; return user;
} }
@ -124,8 +124,8 @@ export class MutationResolver extends BaseResolver {
* @param type * @param type
* @param request * @param request
*/ */
public async vote({postId, type}: { postId: number, type: dataaccess.VoteType }, request: any): public async vote({postId, type}: { postId: number, type: dataAccess.VoteType }, request: any):
Promise<{ post: Post, voteType: dataaccess.VoteType }> { Promise<{ post: Post, voteType: dataAccess.VoteType }> {
this.ensureLoggedIn(request); this.ensureLoggedIn(request);
const post = await Post.findByPk(postId); const post = await Post.findByPk(postId);
if (post) { if (post) {
@ -143,15 +143,17 @@ export class MutationResolver extends BaseResolver {
* Creates a new post * Creates a new post
* @param content * @param content
* @param activityId * @param activityId
* @param type
* @param request * @param request
*/ */
public async createPost({content, activityId}: { content: string, activityId?: number}, public async createPost(
request: any): Promise<Post> { {content, activityId, type}: { content: string, activityId?: number, type: dataAccess.PostType},
request: any): Promise<Post> {
this.ensureLoggedIn(request); this.ensureLoggedIn(request);
if (content.length > 2048) { if (content.length > 2048) {
throw new GraphQLError("Content too long."); 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); globals.internalEmitter.emit(InternalEvents.GQLPOSTCREATE, post);
return post; return post;
} }
@ -171,7 +173,7 @@ export class MutationResolver extends BaseResolver {
}); });
const isAdmin = (await User.findOne({where: {id: request.session.userId}})).isAdmin; const isAdmin = (await User.findOne({where: {id: request.session.userId}})).isAdmin;
if (post.rAuthor.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 { } else {
throw new GraphQLError("User is not author of the post."); throw new GraphQLError("User is not author of the post.");
} }
@ -188,7 +190,7 @@ export class MutationResolver extends BaseResolver {
if (members) { if (members) {
chatMembers.push(...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): public async sendMessage({chatId, content}: { chatId: number, content: string }, request: any):
Promise<ChatMessage> { Promise<ChatMessage> {
this.ensureLoggedIn(request); 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); globals.internalEmitter.emit(InternalEvents.GQLCHATMESSAGE, message);
return message; return message;
} }
@ -211,10 +213,10 @@ export class MutationResolver extends BaseResolver {
* @param type * @param type
* @param request * @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<Request> { Promise<Request> {
this.ensureLoggedIn(request); 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 type
* @param request * @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); this.ensureLoggedIn(request);
const user = await User.findByPk(request.session.userId); const user = await User.findByPk(request.session.userId);
await user.acceptRequest(sender, type); await user.acceptRequest(sender, type);
@ -236,7 +238,7 @@ export class MutationResolver extends BaseResolver {
* @param type * @param type
* @param request * @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); this.ensureLoggedIn(request);
const user = await User.findByPk(request.session.userId); const user = await User.findByPk(request.session.userId);
await user.acceptRequest(sender, type); 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<Group> { public async createGroup({name, members}: { name: string, members: number[] }, request: any): Promise<Group> {
this.ensureLoggedIn(request); 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<Group> { public async joinGroup({groupId}: { groupId: number }, request: any): Promise<Group> {
this.ensureLoggedIn(request); this.ensureLoggedIn(request);
return dataaccess.changeGroupMembership(groupId, request.session.userId, return dataAccess.changeGroupMembership(groupId, request.session.userId,
dataaccess.MembershipChangeAction.ADD); dataAccess.MembershipChangeAction.ADD);
} }
/** /**
@ -302,8 +304,8 @@ export class MutationResolver extends BaseResolver {
*/ */
public async leaveGroup({groupId}: { groupId: number }, request: any): Promise<Group> { public async leaveGroup({groupId}: { groupId: number }, request: any): Promise<Group> {
this.ensureLoggedIn(request); this.ensureLoggedIn(request);
return dataaccess.changeGroupMembership(groupId, request.session.userId, return dataAccess.changeGroupMembership(groupId, request.session.userId,
dataaccess.MembershipChangeAction.REMOVE); dataAccess.MembershipChangeAction.REMOVE);
} }
/** /**
@ -319,8 +321,8 @@ export class MutationResolver extends BaseResolver {
if (group && !(await group.$has("rAdmins", user)) && (await group.creator()) !== user.id) { if (group && !(await group.$has("rAdmins", user)) && (await group.creator()) !== user.id) {
throw new NotAGroupAdminError(groupId); throw new NotAGroupAdminError(groupId);
} }
return dataaccess.changeGroupMembership(groupId, userId, return dataAccess.changeGroupMembership(groupId, userId,
dataaccess.MembershipChangeAction.OP); dataAccess.MembershipChangeAction.OP);
} }
/** /**
@ -341,8 +343,8 @@ export class MutationResolver extends BaseResolver {
throw new GraphQLError( throw new GraphQLError(
"You are not allowed to remove a creator as an admin."); "You are not allowed to remove a creator as an admin.");
} }
return await dataaccess.changeGroupMembership(groupId, userId, return await dataAccess.changeGroupMembership(groupId, userId,
dataaccess.MembershipChangeAction.DEOP); dataAccess.MembershipChangeAction.DEOP);
} }
/** /**
@ -361,7 +363,7 @@ export class MutationResolver extends BaseResolver {
if (!(await group.$has("rAdmins", user))) { if (!(await group.$has("rAdmins", user))) {
throw new NotAGroupAdminError(groupId); throw new NotAGroupAdminError(groupId);
} }
const blacklisted = await dataaccess.checkBlacklisted(name); const blacklisted = await dataAccess.checkBlacklisted(name);
if (blacklisted.length > 0) { if (blacklisted.length > 0) {
throw new BlacklistedError(blacklisted.map((p) => p.phrase), "event name"); throw new BlacklistedError(blacklisted.map((p) => p.phrase), "event name");
} }

@ -85,7 +85,7 @@ type Mutation {
sendMessage(chatId: ID!, content: String!): ChatMessage sendMessage(chatId: ID!, content: String!): ChatMessage
"create a post that can belong to an activity" "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" "delete the post for a given post id"
deletePost(postId: ID!): Boolean! deletePost(postId: ID!): Boolean!
@ -317,6 +317,9 @@ type Post {
"the text of the post" "the text of the post"
content: String content: String
"If the post is publically visible"
visible: Boolean!
"the content of the post rendered by markdown-it" "the content of the post rendered by markdown-it"
htmlContent: String htmlContent: String
@ -543,7 +546,16 @@ enum RequestType {
EVENTINVITE EVENTINVITE
} }
"the type of sorting for getPosts"
enum SortType { enum SortType {
TOP TOP
NEW 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
}

Loading…
Cancel
Save