Merge branch 'develop' of Software_Engineering_I/greenvironment-server into master

pull/5/head
Trivernis 5 years ago committed by Gitea
commit c987c4da72

@ -29,6 +29,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- rate limits with defaults of 10/min for `/upload` and 30/min for `/graphql`
- complexity limits for graphql queries that can be configured with the `api.maxQueryComplexity` option
- complexity headers `X-Query-Complexity` and `X-Max-Query-Complexity`
- Media model to store information about media (videos and images)
- Media association to users, groups and posts
- 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

@ -1,11 +1,12 @@
FROM node:current-alpine
FROM node:13.7.0
COPY . /home/node/green
WORKDIR /home/node/green
RUN apt update
RUN apt install redis-server -y
RUN npm install -g gulp
RUN npm install --save-dev
RUN npm rebuild node-sass
RUN yarn install
RUN gulp
COPY . .
EXPOSE 8080
CMD ["npm" , "run"]
CMD ["redis-server", "&", "node" , "./dist"]

@ -4,6 +4,9 @@
# A connection uri string to the database
connectionUri = "sqlite://greenvironment.db"
# The cleanup interval of orphaned entries in seconds
cleanupInterval = 6000
# Configuration for the redis connection
[redis]
@ -54,6 +57,7 @@ publicPath = "./public"
# Configuration for the api
[api]
# if graphiql should be enabled
graphiql = true

@ -8,4 +8,4 @@ services:
- NODE_ENV=production
ports:
- "8080:8080"
command: "npm start"
command: "yarn start"

@ -5,26 +5,18 @@ import * as path from "path";
import * as sharp from "sharp";
import {Readable} from "stream";
import globals from "./globals";
import {Media} from "./models";
const toArray = require("stream-to-array");
const dataDirName = "data";
interface IUploadConfirmation {
/**
* Indicates the error that might have occured during the upload
* An enum representing a type of media
*/
error?: string;
/**
* The file that has been uploaded
*/
fileName?: string;
/**
* If the upload was successful
*/
success: boolean;
export enum MediaType {
IMAGE = "IMAGE",
VIDEO = "VIDEO",
}
type ImageFit = "cover" | "contain" | "fill" | "inside" | "outside";
@ -71,7 +63,7 @@ export class UploadManager {
* @param fit
*/
public async processAndStoreImage(data: Buffer, width = 512, height = 512,
fit: ImageFit = "cover"): Promise<string> {
fit: ImageFit = "cover"): Promise<Media> {
const fileBasename = UploadManager.getCrypticFileName() + "." + config.get("api.imageFormat");
await fsx.ensureDir(this.dataDir);
const filePath = path.join(this.dataDir, fileBasename);
@ -93,7 +85,11 @@ export class UploadManager {
});
}
await image.toFile(filePath);
return `/${dataDirName}/${fileBasename}`;
return Media.create({
path: filePath,
type: MediaType.IMAGE,
url: `/${dataDirName}/${fileBasename}`,
});
}
/**
@ -101,12 +97,16 @@ export class UploadManager {
* @param data
* @param extension
*/
public async processAndStoreVideo(data: Buffer, extension: string): Promise<string> {
public async processAndStoreVideo(data: Buffer, extension: string): Promise<Media> {
const fileBasename = UploadManager.getCrypticFileName() + extension;
await fsx.ensureDir(this.dataDir);
const filePath = path.join(this.dataDir, fileBasename);
await fsx.writeFile(filePath, data);
return `/${dataDirName}/${fileBasename}`;
return Media.create({
path: filePath,
type: MediaType.VIDEO,
url: `/${dataDirName}/${fileBasename}`,
});
}
/**

@ -1,3 +1,4 @@
import * as config from "config";
import * as crypto from "crypto";
import * as sqz from "sequelize";
import {Sequelize} from "sequelize-typescript";
@ -63,11 +64,28 @@ namespace dataaccess {
models.Event,
models.Activity,
models.BlacklistedPhrase,
models.Media,
]);
} catch (err) {
globals.logger.error(err.message);
globals.logger.debug(err.stack);
}
await databaseCleanup();
setInterval(databaseCleanup, config.get<number>("database.cleanupInterval"));
}
/**
* Cleans the database.
* - deletes all media entries without associations
*/
async function databaseCleanup() {
const allMedia = await models.Media
.findAll({include: [models.Post, models.User, models.Group]}) as models.Media[];
for (const media of allMedia) {
if (!media.user && !media.post && !media.group) {
await media.destroy();
}
}
}
/**
@ -161,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
@ -170,10 +191,12 @@ namespace dataaccess {
SELECT *,
(SELECT count(*)
FROM post_votes
WHERE vote_type = 'UPVOTE' AND post_id = posts.id) AS upvotes,
WHERE vote_type = 'UPVOTE'
AND post_id = posts.id) AS upvotes,
(SELECT count(*)
FROM post_votes
WHERE vote_type = 'DOWNVOTE' AND post_id = posts.id) AS downvotes
WHERE vote_type = 'DOWNVOTE'
AND post_id = posts.id) AS downvotes
FROM posts) AS a
ORDER BY (a.upvotes - a.downvotes) DESC, a.upvotes DESC, a.id
LIMIT ?
@ -188,15 +211,17 @@ namespace dataaccess {
* @param content
* @param authorId
* @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);
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);
@ -218,11 +243,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);
@ -399,6 +428,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,

@ -1,4 +1,3 @@
import * as sqz from "sequelize";
import {
BelongsTo,
BelongsToMany,
@ -14,6 +13,7 @@ import {ChatRoom} from "./ChatRoom";
import {Event} from "./Event";
import {GroupAdmin} from "./GroupAdmin";
import {GroupMember} from "./GroupMember";
import {Media} from "./Media";
import {User} from "./User";
/**
@ -30,12 +30,12 @@ export class Group extends Model<Group> {
@Column({allowNull: false, unique: true})
public name: string;
/**
* The url of the groups avatar picture
* The id of the media that represents the groups profile picture
*/
@Column({type: sqz.STRING(512)})
public picture: string;
@ForeignKey(() => Media)
@Column({allowNull: true})
public mediaId: number;
/**
* The id of the user who created the group
@ -53,6 +53,12 @@ export class Group extends Model<Group> {
@Column({allowNull: false})
public chatId: number;
/**
* The media of the group
*/
@BelongsTo(() => Media, "mediaId")
public rMedia: Media;
/**
* The creator of the group
*/
@ -83,6 +89,14 @@ export class Group extends Model<Group> {
@HasMany(() => Event, "groupId")
public rEvents: Event[];
/**
* Returns the media url of the group which is the profile picture
*/
public async picture(): Promise<string> {
const media = await this.$get<Media>("rMedia") as Media;
return media ? media.url : undefined;
}
/**
* Returns the creator of the group
*/

@ -0,0 +1,61 @@
import * as fsx from "fs-extra";
import * as sqz from "sequelize";
import {BeforeDestroy, Column, HasOne, Model, NotNull, Table} from "sequelize-typescript";
import {MediaType} from "../UploadManager";
import {Group} from "./Group";
import {Post} from "./Post";
import {User} from "./User";
/**
* Represents a single media file that can be used as a profile picture for groups and users or a post picture
*/
@Table({underscored: true})
export class Media extends Model<Media> {
/**
* Deletes the media file before the media is destroyed
* @param instance
*/
@BeforeDestroy
public static async deleteMediaFile(instance: Media) {
await fsx.unlink(instance.path);
}
/**
* The api url for the media
*/
@NotNull
@Column({type: sqz.STRING(512), allowNull: false})
public url: string;
/**
* The local path of the file
*/
@NotNull
@Column({allowNull: false})
public path: string;
/**
* The type of media
*/
@Column({type: sqz.ENUM, values: ["IMAGE", "VIDEO"]})
public type: MediaType;
/**
* The user that uses the media
*/
@HasOne(() => User)
public user: User;
/**
* The group that uses the media
*/
@HasOne(() => Group)
public group: Group;
/**
* The post that uses the media
*/
@HasOne(() => Post)
public post: Post;
}

@ -1,8 +1,8 @@
import * as config from "config";
import * as sqz from "sequelize";
import {BelongsTo, BelongsToMany, Column, CreatedAt, ForeignKey, Model, NotNull, Table} from "sequelize-typescript";
import markdown from "../markdown";
import {Activity} from "./Activity";
import {Media} from "./Media";
import {PostVote, VoteType} from "./PostVote";
import {User} from "./User";
@ -19,6 +19,13 @@ export class Post extends Model<Post> {
@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
*/
@ -35,10 +42,11 @@ export class Post extends Model<Post> {
public activityId: number;
/**
* An url pointing to any media that belongs to the post
* An id pointing to a media entry
*/
@Column({allowNull: true, type: sqz.STRING(512)})
public mediaUrl: string;
@ForeignKey(() => Media)
@Column({allowNull: true})
public mediaId: number;
/**
* The author of the post
@ -52,6 +60,12 @@ export class Post extends Model<Post> {
@BelongsTo(() => Activity, "activityId")
public rActivity?: Activity;
/**
* The media of the post
*/
@BelongsTo(() => Media, "mediaId")
public rMedia?: Media;
/**
* The votes that were performed on the post
*/
@ -110,17 +124,8 @@ export class Post extends Model<Post> {
/**
* Returns the media description object of the post
*/
public get media() {
const url = this.getDataValue("mediaUrl");
if (url) {
const type = url.endsWith(config.get("api.imageFormat")) ? "IMAGE" : "VIDEO";
return {
type,
url,
};
} else {
return null;
}
public async media() {
return await this.$get<Media>("rMedia") as Media;
}
/**

@ -1,8 +1,10 @@
import * as sqz from "sequelize";
import {
BelongsTo,
BelongsToMany,
Column,
CreatedAt,
ForeignKey,
HasMany,
Model,
NotNull,
@ -22,6 +24,7 @@ import {Friendship} from "./Friendship";
import {Group} from "./Group";
import {GroupAdmin} from "./GroupAdmin";
import {GroupMember} from "./GroupMember";
import {Media} from "./Media";
import {Post} from "./Post";
import {PostVote} from "./PostVote";
import {Request, RequestType} from "./Request";
@ -97,10 +100,18 @@ export class User extends Model<User> {
public isAdmin: boolean;
/**
* The url of the users profile picture
* The id of the media that is the users profile picture
*/
@Column({type: sqz.STRING(512)})
public profilePicture: string;
@ForeignKey(() => Media)
@Column({allowNull: false})
public mediaId: number;
/**
* The media of the user
*/
@BelongsTo(() => Media)
public rMedia: Media;
/**
* The friends of the user
@ -221,6 +232,14 @@ export class User extends Model<User> {
return JSON.stringify(this.getDataValue("frontendSettings"));
}
/**
* Returns the media url which is the profile picture
*/
public async profilePicture(): Promise<string> {
const media = await this.$get<Media>("rMedia") as Media;
return media ? media.url : undefined;
}
/**
* Returns the token for the user that can be used as a bearer in requests
*/
@ -287,11 +306,15 @@ export class User extends Model<User> {
* a list of posts the user has created
* @param first
* @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;
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[];
}
/**

@ -13,3 +13,4 @@ export {Event} from "./Event";
export {EventParticipant} from "./EventParticipant";
export {Activity} from "./Activity";
export {BlacklistedPhrase} from "./BlacklistedPhrase";
export {Media} from "./Media";

@ -2,13 +2,14 @@ import * as bodyParser from "body-parser";
import * as config from "config";
import * as crypto from "crypto";
import {Router} from "express";
import {UploadedFile} from "express-fileupload";
import * as fileUpload from "express-fileupload";
import {UploadedFile} from "express-fileupload";
import * as fsx from "fs-extra";
import * as status from "http-status";
import * as path from "path";
import globals from "../lib/globals";
import {Group, Post, User} from "../lib/models";
import {Media} from "../lib/models";
import {is} from "../lib/regex";
import Route from "../lib/Route";
import {UploadManager} from "../lib/UploadManager";
@ -117,14 +118,15 @@ export class UploadRoute extends Route {
let fileName: string;
const profilePic = request.files.profilePicture as UploadedFile;
try {
const user = await User.findByPk(request.session.userId);
const user = await User.findByPk(request.session.userId, {include: [Media]});
if (user) {
fileName = await this.uploadManager.processAndStoreImage(profilePic.data);
if (user.profilePicture) {
await this.uploadManager.deleteWebFile(user.profilePicture);
const media = await this.uploadManager.processAndStoreImage(profilePic.data);
if (user.mediaId) {
const previousMedia = await user.$get("rMedia") as Media;
await previousMedia.destroy();
}
user.profilePicture = fileName;
await user.save();
await user.$set("rMedia", media);
fileName = media.url;
success = true;
} else {
error = "User not found";
@ -153,7 +155,7 @@ export class UploadRoute extends Route {
if (request.body.groupId) {
try {
const user = await User.findByPk(request.session.userId);
const group = await Group.findByPk(request.body.groupId);
const group = await Group.findByPk(request.body.groupId, {include: [Media]});
if (!group) {
error = `No group with the id '${request.body.groupId}' found.`;
return {
@ -163,12 +165,13 @@ export class UploadRoute extends Route {
}
const isAdmin = await group.$has("rAdmins", user);
if (isAdmin) {
fileName = await this.uploadManager.processAndStoreImage(groupPicture.data);
if (group.picture) {
await this.uploadManager.deleteWebFile(group.picture);
const media = await this.uploadManager.processAndStoreImage(groupPicture.data);
if (group.mediaId) {
const previousMedia = await group.$get("rMedia") as Media;
await previousMedia.destroy();
}
group.picture = fileName;
await group.save();
await group.$set("rMedia", media);
fileName = media.url;
success = true;
} else {
error = "You are not a group admin.";
@ -200,19 +203,22 @@ export class UploadRoute extends Route {
const postMedia = request.files.postMedia as UploadedFile;
if (postId) {
try {
let media: Media;
const post = await Post.findByPk(postId);
if (post.authorId === request.session.userId) {
if (is.image(postMedia.mimetype)) {
fileName = await this.uploadManager.processAndStoreImage(postMedia.data, 1080, 720, "inside");
media = await this.uploadManager.processAndStoreImage(postMedia.data, 1080, 720, "inside");
} else if (is.video(postMedia.mimetype)) {
fileName = await this.uploadManager.processAndStoreVideo(postMedia.data, postMedia.mimetype
media = await this.uploadManager.processAndStoreVideo(postMedia.data, postMedia.mimetype
.replace("video/", ""));
} else {
error = "Wrong type of file provided";
}
if (fileName) {
post.mediaUrl = fileName;
if (media) {
await post.$set("rMedia", media);
post.visible = true;
await post.save();
fileName = media.url;
success = true;
}
} else {

@ -1,12 +1,10 @@
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";
import {InvalidFileError} from "../../lib/errors/InvalidFileError";
import {NotAGroupAdminError} from "../../lib/errors/NotAGroupAdminError";
import {NotAnAdminError} from "../../lib/errors/NotAnAdminError";
import {NotTheGroupCreatorError} from "../../lib/errors/NotTheGroupCreatorError";
@ -14,7 +12,6 @@ import {PostNotFoundError} from "../../lib/errors/PostNotFoundError";
import globals from "../../lib/globals";
import {InternalEvents} from "../../lib/InternalEvents";
import {Activity, BlacklistedPhrase, ChatMessage, ChatRoom, Event, Group, Post, Request, User} from "../../lib/models";
import {is} from "../../lib/regex";
import {UploadManager} from "../../lib/UploadManager";
import {BaseResolver} from "./BaseResolver";
@ -53,7 +50,7 @@ export class MutationResolver extends BaseResolver {
* @param request
*/
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;
return user;
}
@ -96,7 +93,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 +121,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 +140,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},
public async createPost(
{content, activityId, type}: { content: string, activityId?: number, type: dataAccess.PostType },
request: any): Promise<Post> {
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,15 +170,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) {
if (post.mediaUrl) {
try {
await this.uploadManager.deleteWebFile(post.mediaUrl);
} catch (err) {
globals.logger.error(err.message);
globals.logger.debug(err.stack);
}
}
return await dataaccess.deletePost(post.id);
return await dataAccess.deletePost(post.id);
} else {
throw new GraphQLError("User is not author of the post.");
}
@ -196,7 +187,7 @@ export class MutationResolver extends BaseResolver {
if (members) {
chatMembers.push(...members);
}
return await dataaccess.createChat(...chatMembers);
return await dataAccess.createChat(...chatMembers);
}
/**
@ -208,7 +199,7 @@ export class MutationResolver extends BaseResolver {
public async sendMessage({chatId, content}: { chatId: number, content: string }, request: any):
Promise<ChatMessage> {
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;
}
@ -219,10 +210,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<Request> {
this.ensureLoggedIn(request);
return dataaccess.createRequest(request.session.userId, receiver, type);
return dataAccess.createRequest(request.session.userId, receiver, type);
}
/**
@ -231,7 +222,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);
@ -244,7 +235,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);
@ -270,7 +261,7 @@ export class MutationResolver extends BaseResolver {
*/
public async createGroup({name, members}: { name: string, members: number[] }, request: any): Promise<Group> {
this.ensureLoggedIn(request);
return await dataaccess.createGroup(name, request.session.userId, members);
return await dataAccess.createGroup(name, request.session.userId, members);
}
/**
@ -299,8 +290,8 @@ export class MutationResolver extends BaseResolver {
*/
public async joinGroup({groupId}: { groupId: number }, request: any): Promise<Group> {
this.ensureLoggedIn(request);
return dataaccess.changeGroupMembership(groupId, request.session.userId,
dataaccess.MembershipChangeAction.ADD);
return dataAccess.changeGroupMembership(groupId, request.session.userId,
dataAccess.MembershipChangeAction.ADD);
}
/**
@ -310,8 +301,8 @@ export class MutationResolver extends BaseResolver {
*/
public async leaveGroup({groupId}: { groupId: number }, request: any): Promise<Group> {
this.ensureLoggedIn(request);
return dataaccess.changeGroupMembership(groupId, request.session.userId,
dataaccess.MembershipChangeAction.REMOVE);
return dataAccess.changeGroupMembership(groupId, request.session.userId,
dataAccess.MembershipChangeAction.REMOVE);
}
/**
@ -327,8 +318,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);
}
/**
@ -349,8 +340,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);
}
/**
@ -369,7 +360,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");
}

@ -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
}

@ -29,7 +29,8 @@
"privacies": "all",
"locations": "instance"
}
}]
}],
"array-type": false
},
"jsRules": {
"max-line-length": {

Loading…
Cancel
Save