Api functions

- added chat message functions
- added markdown rendering
- added custom error classes
pull/1/head
Trivernis 5 years ago
parent d7f819e02e
commit c97d0ffe55

67
package-lock.json generated

@ -171,6 +171,21 @@
"integrity": "sha512-SGGAhXLHDx+PK4YLNcNGa6goPf9XRWQNAUUbffkwVGGXIxmDKWyGGL4inzq2sPmExu431Ekb9aEMn9BkPqEYFA==",
"dev": true
},
"@types/linkify-it": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-2.1.0.tgz",
"integrity": "sha512-Q7DYAOi9O/+cLLhdaSvKdaumWyHbm7HAk/bFwwyTuU0arR5yyCeW5GOoqt4tJTpDRxhpx9Q8kQL6vMpuw9hDSw==",
"dev": true
},
"@types/markdown-it": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-0.0.9.tgz",
"integrity": "sha512-IFSepyZXbF4dgSvsk8EsgaQ/8Msv1I5eTL0BZ0X3iGO9jw6tCVtPG8HchIPm3wrkmGdqZOD42kE0zplVi1gYDA==",
"dev": true,
"requires": {
"@types/linkify-it": "*"
}
},
"@types/mime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz",
@ -178,9 +193,9 @@
"dev": true
},
"@types/node": {
"version": "12.7.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.2.tgz",
"integrity": "sha512-dyYO+f6ihZEtNPDcWNR1fkoTDf3zAK3lAABDze3mz6POyIercH0lEUawUFXlG8xaQZmm1yEBON/4TsYv/laDYg==",
"version": "12.7.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.8.tgz",
"integrity": "sha512-FMdVn84tJJdV+xe+53sYiZS4R5yn1mAIxfj+DVoNiQjTYz1+OYmjwEZr1ev9nU0axXwda0QDbYl06QHanRVH3A==",
"dev": true
},
"@types/pg": {
@ -1894,6 +1909,11 @@
"has-binary2": "~1.0.2"
}
},
"entities": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz",
"integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw=="
},
"env-variable": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.5.tgz",
@ -3329,12 +3349,6 @@
}
}
},
"gulp-angular": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/gulp-angular/-/gulp-angular-0.1.2.tgz",
"integrity": "sha1-ljV2ul7qoDZqMf6l7S7AHvC0O3g=",
"dev": true
},
"gulp-minify": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/gulp-minify/-/gulp-minify-3.1.0.tgz",
@ -4214,6 +4228,14 @@
"resolve": "^1.1.7"
}
},
"linkify-it": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz",
"integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==",
"requires": {
"uc.micro": "^1.0.1"
}
},
"load-json-file": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
@ -4320,6 +4342,23 @@
"object-visit": "^1.0.0"
}
},
"markdown-it": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz",
"integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==",
"requires": {
"argparse": "^1.0.7",
"entities": "~2.0.0",
"linkify-it": "^2.0.0",
"mdurl": "^1.0.1",
"uc.micro": "^1.0.5"
}
},
"markdown-it-emoji": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz",
"integrity": "sha1-m+4OmpkKljupbfaYDE/dsF37Tcw="
},
"matchdep": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz",
@ -4360,6 +4399,11 @@
"resolve-dir": "^1.0.0"
}
},
"mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
"integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4="
},
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@ -6897,6 +6941,11 @@
"integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==",
"dev": true
},
"uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
},
"uglify-js": {
"version": "2.8.29",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",

@ -29,13 +29,13 @@
"@types/graphql": "^14.2.3",
"@types/http-status": "^0.2.30",
"@types/js-yaml": "^3.12.1",
"@types/node": "^12.7.2",
"@types/markdown-it": "0.0.9",
"@types/node": "^12.7.8",
"@types/pg": "^7.11.0",
"@types/socket.io": "^2.1.2",
"@types/winston": "^2.4.4",
"delete": "^1.1.0",
"gulp": "^4.0.2",
"gulp-angular": "^0.1.2",
"gulp-minify": "^3.1.0",
"gulp-sass": "^4.0.2",
"gulp-typescript": "^5.0.1",
@ -58,6 +58,8 @@
"graphql-import": "^0.7.1",
"http-status": "^1.3.2",
"js-yaml": "^3.13.1",
"markdown-it": "^10.0.0",
"markdown-it-emoji": "^1.4.0",
"pg": "^7.12.1",
"pug": "^2.0.4",
"socket.io": "^2.2.0",

@ -13,3 +13,7 @@ server:
session:
secret: REPLACE WITH SAFE RANDOM GENERATED SECRET
cookieMaxAge: 604800000 # 7 days
markdown:
plugins:
- 'markdown-it-emoji'

@ -1,7 +1,15 @@
import markdown from "../markdown";
import {Chatroom} from "./Chatroom";
import {User} from "./User";
export class ChatMessage {
constructor(public author: User, public chat: Chatroom, public timestamp: number, public content: string) {
}
/**
* The content rendered by markdown-it.
*/
public htmlContent(): string {
return markdown.renderInline(this.content);
}
}

@ -6,6 +6,17 @@ export class Chatroom {
constructor(private id: number) {}
/**
* Returns if the chat exists.
*/
public async exists(): Promise<boolean> {
const result = await queryHelper.first({
text: "SELECT id FROM chats WHERE id = $1",
values: [this.id],
});
return !!result.id;
}
/**
* Returns all members of a chatroom.
*/

@ -7,6 +7,14 @@ export abstract class DataObject {
constructor(public id: number, protected row?: any) {
}
/**
* Returns if the object extists by trying to load data.
*/
public async exists() {
await this.loadDataIfNotExists();
return this.dataLoaded;
}
protected abstract loadData(): Promise<void>;
/**

@ -1,3 +1,4 @@
import markdown from "../markdown";
import {DataObject} from "./DataObject";
import {queryHelper} from "./index";
import dataaccess from "./index";
@ -40,6 +41,14 @@ export class Post extends DataObject {
return this.$content;
}
/**
* the content rendered by markdown-it.
*/
public async htmlContent(): Promise<string> {
await this.loadDataIfNotExists();
return markdown.render(this.$content);
}
/**
* The date the post was created at.
*/

@ -1,7 +1,29 @@
import {Chatroom} from "./Chatroom";
import {queryHelper} from "./index";
import {User} from "./User";
export class Profile extends User {
/**
* Returns all chatrooms (with pagination).
* @param first
* @param offset
*/
public async chats({first, offset}: {first: number, offset?: number}) {
first = first || 10;
offset = offset || 0;
const result = await queryHelper.all({
text: "SELECT chat FROM chat_members WHERE member = $1 LIMIT $2 OFFSET $3",
values: [this.id, first, offset],
});
if (result) {
return result.map((row) => new Chatroom(row.chat));
} else {
return [];
}
}
/**
* Sets the greenpoints of a user.
* @param points

@ -8,6 +8,7 @@ export class User extends DataObject {
private $email: string;
private $greenpoints: number;
private $joinedAt: string;
private $exists: boolean;
/**
* The name of the user

@ -1,6 +1,9 @@
import {Pool} from "pg";
import {ChatNotFoundError} from "../errors/ChatNotFoundError";
import {UserNotFoundError} from "../errors/UserNotFoundError";
import globals from "../globals";
import {QueryHelper} from "../QueryHelper";
import {ChatMessage} from "./ChatMessage";
import {Chatroom} from "./Chatroom";
import {Post} from "./Post";
import {Profile} from "./Profile";
@ -27,6 +30,9 @@ function generateHandle(username: string) {
return `${username}.${Buffer.from(Date.now().toString()).toString("base64")}`;
}
/**
* Namespace with functions to fetch initial data for wrapping.
*/
namespace dataaccess {
export const pool: Pool = dbClient;
@ -39,24 +45,20 @@ namespace dataaccess {
await queryHelper.createTables();
}
/**
* Returns the user by id
* @param userId
*/
export function getUser(userId: number) {
return new User(userId);
}
/**
* Returns the user by handle.
* @param userHandle
*/
export async function getUserByHandle(userHandle: string) {
export async function getUserByHandle(userHandle: string): Promise<User> {
const result = await queryHelper.first({
text: "SELECT * FROM users WHERE users.handle = $1",
values: [userHandle],
});
return new User(result.id, result);
if (result) {
return new User(result.id, result);
} else {
throw new UserNotFoundError(userHandle);
}
}
/**
@ -72,7 +74,7 @@ namespace dataaccess {
if (result) {
return new Profile(result.id, result);
} else {
return null;
throw new UserNotFoundError(email);
}
}
@ -94,7 +96,7 @@ namespace dataaccess {
* Returns a post for a given postId.s
* @param postId
*/
export async function getPost(postId: number) {
export async function getPost(postId: number): Promise<Post> {
const result = await queryHelper.first({
text: "SELECT * FROM posts WHERE id = $1",
values: [postId],
@ -112,7 +114,7 @@ namespace dataaccess {
* @param authorId
* @param type
*/
export async function createPost(content: string, authorId: number, type?: string) {
export async function createPost(content: string, authorId: number, type?: string): Promise<Post> {
const result = await queryHelper.first({
text: "INSERT INTO posts (content, author, type) VALUES ($1, $2, $3) RETURNING *",
values: [content, authorId, type],
@ -124,7 +126,7 @@ namespace dataaccess {
* Deletes a post
* @param postId
*/
export async function deletePost(postId: number) {
export async function deletePost(postId: number): Promise<boolean> {
const result = await queryHelper.first({
text: "DELETE FROM posts WHERE posts.id = $1",
values: [postId],
@ -136,7 +138,7 @@ namespace dataaccess {
* Creates a chatroom containing two users
* @param members
*/
export async function createChat(...members: number[]) {
export async function createChat(...members: number[]): Promise<Chatroom> {
const idResult = await queryHelper.first({
text: "INSERT INTO chats (id) values (nextval('chats_id_seq'::regclass)) RETURNING *;",
});
@ -161,6 +163,25 @@ namespace dataaccess {
return new Chatroom(id);
}
/**
* Sends a message into a chat.
* @param authorId
* @param chatId
* @param content
*/
export async function sendChatMessage(authorId: number, chatId: number, content: string) {
const chat = new Chatroom(chatId);
if ((await chat.exists())) {
const result = await queryHelper.first({
text: "INSERT INTO chat_messages (chat, author, content, created_at) values ($1, $2, $3) RETURNING *",
values: [chatId, authorId, content],
});
return new ChatMessage(new User(result.author), chat, result.timestamp, result.content);
} else {
throw new ChatNotFoundError(chatId);
}
}
/**
* Enum representing the types of votes that can be performed on a post.
*/

@ -0,0 +1,13 @@
import {GraphQLError} from "graphql";
/**
* Base error class.
*/
export class BaseError extends Error {
public readonly graphqlError: GraphQLError;
constructor(message?: string, friendlyMessage?: string) {
super(message);
this.graphqlError = new GraphQLError(friendlyMessage || message);
}
}

@ -0,0 +1,7 @@
import {BaseError} from "./BaseError";
export class ChatNotFoundError extends BaseError {
constructor(chatId: number) {
super(`Chat with id ${chatId} not found.`);
}
}

@ -0,0 +1,7 @@
import {BaseError} from "./BaseError";
export class UserNotFoundError extends BaseError {
constructor(username: string) {
super(`User ${username} not found!`);
}
}

@ -0,0 +1,9 @@
import {GraphQLError} from "graphql";
import {BaseError} from "./BaseError";
export class NotLoggedInGqlError extends GraphQLError {
constructor() {
super("Not logged in");
}
}

@ -0,0 +1,36 @@
import * as MarkdownIt from "markdown-it/lib";
import globals from "./globals";
namespace markdown {
const md = new MarkdownIt();
for (const pluginName of globals.config.markdown.plugins) {
try {
const plugin = require(pluginName);
if (plugin) {
md.use(plugin);
}
} catch (err) {
globals.logger.warn(`Markdown-it plugin '${pluginName}' not found!`);
}
}
/**
* Renders the markdown string inline (without blocks).
* @param markdownString
*/
export function renderInline(markdownString: string) {
return md.renderInline(markdownString);
}
/**
* Renders the markdown string.
* @param markdownString
*/
export function render(markdownString: string) {
return md.render(markdownString);
}
}
export default markdown;

@ -53,7 +53,7 @@ type Mutation {
denyRequest(requestId: ID!): Boolean
"send a message in a Chatroom"
sendMessage(chatId: ID!, content: String!): Boolean
sendMessage(chatId: ID!, content: String!): ChatMessage
"create the post"
createPost(content: String!): Boolean
@ -70,6 +70,9 @@ type User {
"name of the User"
name: String!
"returns the chatrooms the user joined."
chats(first: Int=10, offset: Int): [ChatRoom]
"unique identifier name from the User"
handle: String!
@ -85,7 +88,7 @@ type User {
"creation date of the user account"
joinedAt: String!
"returns all friends of the user"
"all friends of the user"
friends: [User]
"all request for groupChats/friends/events"
@ -95,9 +98,12 @@ type User {
"represents a single user post"
type Post {
"returns the text of the post"
"the text of the post"
content: String
"the content of the post rendered by markdown-it"
htmlContent: String
"upvotes of the Post"
upvotes: Int!
@ -110,7 +116,7 @@ type Post {
"date the post was created"
creationDate: String!
"returns the type of vote the user performed on the post"
"the type of vote the user performed on the post"
userVote: VoteType
}
@ -135,12 +141,29 @@ type ChatRoom {
members: [User!]
"return a specfic range of messages posted in the chat"
getMessages(first: Int, offset: Int): [String]
getMessages(first: Int, offset: Int): [ChatMessage]
"id of the chat"
id: ID!
}
type ChatMessage {
"The author of the chat message."
author: User
"The chatroom the message was posted in"
chat: ChatRoom
"The timestamp when the message was posted (epoch)."
timestamp: Int
"The content of the message."
content: String
"The content of the message rendered by markdown-it."
htmlContent: String
}
"represents the type of vote performed on a post"
enum VoteType {
UPVOTE

@ -3,8 +3,12 @@ import {GraphQLError} from "graphql";
import * as status from "http-status";
import {Server} from "socket.io";
import dataaccess from "../lib/dataaccess";
import {Chatroom} from "../lib/dataaccess/Chatroom";
import {Post} from "../lib/dataaccess/Post";
import {Profile} from "../lib/dataaccess/Profile";
import {User} from "../lib/dataaccess/User";
import {NotLoggedInGqlError} from "../lib/errors/graphqlErrors";
import globals from "../lib/globals";
import {is} from "../lib/regex";
import Route from "../lib/Route";
@ -48,14 +52,14 @@ class HomeRoute extends Route {
return new Profile(req.session.userId);
} else {
res.status(status.UNAUTHORIZED);
return new GraphQLError("Not logged in");
return new NotLoggedInGqlError();
}
},
async getUser({userId, handle}: {userId: number, handle: string}) {
if (handle) {
return await dataaccess.getUserByHandle(handle);
} else if (userId) {
return dataaccess.getUser(userId);
return new User(userId);
} else {
res.status(status.BAD_REQUEST);
return new GraphQLError("No userId or handle provided.");
@ -69,19 +73,28 @@ class HomeRoute extends Route {
return new GraphQLError("No postId given.");
}
},
async getChat({chatId}: {chatId: number}) {
if (chatId) {
return new Chatroom(chatId);
} else {
res.status(status.BAD_REQUEST);
return new GraphQLError("No chatId given.");
}
},
acceptCookies() {
req.session.cookiesAccepted = true;
return true;
},
async login({email, passwordHash}: {email: string, passwordHash: string}) {
if (email && passwordHash) {
const user = await dataaccess.getUserByLogin(email, passwordHash);
if (user && user.id) {
try {
const user = await dataaccess.getUserByLogin(email, passwordHash);
req.session.userId = user.id;
return user;
} else {
} catch (err) {
globals.logger.verbose(`Failed to login user '${email}'`);
res.status(status.BAD_REQUEST);
return new GraphQLError("Invalid login data.");
return err.graphqlError;
}
} else {
res.status(status.BAD_REQUEST);
@ -94,7 +107,7 @@ class HomeRoute extends Route {
return true;
} else {
res.status(status.UNAUTHORIZED);
return new GraphQLError("Not logged in.");
return new NotLoggedInGqlError();
}
},
async register({username, email, passwordHash}: {username: string, email: string, passwordHash: string}) {
@ -165,7 +178,23 @@ class HomeRoute extends Route {
} else {
res.status(status.UNAUTHORIZED);
return new GraphQLError("Not logged in.");
return new NotLoggedInGqlError();
}
},
async sendChatMessage({chatId, content}: {chatId: number, content: string}) {
if (!req.session.userId) {
return new NotLoggedInGqlError();
}
if (chatId && content) {
try {
return await dataaccess.sendChatMessage(req.session.userId, chatId, content);
} catch (err) {
res.status(status.BAD_REQUEST);
return err.graphqlError;
}
} else {
res.status(status.BAD_REQUEST);
return new GraphQLError("No chatId or content given.");
}
},
};

Loading…
Cancel
Save