Merge branch 'julius-dev' of Software_Engineering_I/greenvironment-server into develop
commit
6687baecce
@ -0,0 +1,73 @@
|
||||
import * as crypto from "crypto";
|
||||
import {EventEmitter} from "events";
|
||||
|
||||
export class MemoryCache extends EventEmitter {
|
||||
private cacheItems: any = {};
|
||||
private cacheExpires: any = {};
|
||||
private expireCheck: NodeJS.Timeout;
|
||||
|
||||
/**
|
||||
* Creates interval function.
|
||||
* @param ttl
|
||||
*/
|
||||
constructor(private ttl: number = 500) {
|
||||
super();
|
||||
this.expireCheck = setInterval(() => this.checkExpires(), ttl / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a md5 hash of the given key.
|
||||
* @param key
|
||||
*/
|
||||
public hashKey(key: string): string {
|
||||
const hash = crypto.createHash("md5");
|
||||
const data = hash.update(key, "utf8");
|
||||
return data.digest("hex");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an entry.
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
public set(key: string, value: any) {
|
||||
this.cacheItems[key] = value;
|
||||
this.cacheExpires[key] = Date.now() + this.ttl;
|
||||
this.emit("set", key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the entry stored with the given key.
|
||||
* @param key
|
||||
*/
|
||||
public get(key: string) {
|
||||
if (this.cacheItems.hasOwnProperty(key)) {
|
||||
this.emit("hit", key, this.cacheItems[key]);
|
||||
return this.cacheItems[key];
|
||||
} else {
|
||||
this.emit("miss", key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a cache item.
|
||||
* @param key
|
||||
*/
|
||||
public delete(key: string) {
|
||||
this.emit("delete", key);
|
||||
delete this.cacheItems[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks expires and clears items that are over the expire value.
|
||||
*/
|
||||
private checkExpires() {
|
||||
for (const [key, value] of Object.entries(this.cacheExpires)) {
|
||||
if (value < Date.now()) {
|
||||
this.emit("delete", key);
|
||||
delete this.cacheItems[key];
|
||||
delete this.cacheExpires[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import markdown from "../markdown";
|
||||
import {Chatroom} from "./Chatroom";
|
||||
import {User} from "./User";
|
||||
|
||||
export class ChatMessage {
|
||||
constructor(
|
||||
public readonly author: User,
|
||||
public readonly chat: Chatroom,
|
||||
public readonly createdAt: number,
|
||||
public readonly content: string) {}
|
||||
|
||||
/**
|
||||
* The content rendered by markdown-it.
|
||||
*/
|
||||
public htmlContent(): string {
|
||||
return markdown.renderInline(this.content);
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
import globals from "../globals";
|
||||
import {ChatMessage} from "./ChatMessage";
|
||||
import {queryHelper} from "./index";
|
||||
import {User} from "./User";
|
||||
|
||||
export class Chatroom {
|
||||
|
||||
constructor(private readonly id: number) {
|
||||
this.id = Number(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public async members(): Promise<User[]> {
|
||||
const result = await queryHelper.all({
|
||||
cache: true,
|
||||
text: `SELECT * FROM chat_members
|
||||
JOIN users ON (chat_members.member = users.id)
|
||||
WHERE chat_members.chat = $1;`,
|
||||
values: [this.id],
|
||||
});
|
||||
const chatMembers = [];
|
||||
for (const row of result) {
|
||||
const user = new User(row.id, row);
|
||||
chatMembers.push(user);
|
||||
}
|
||||
return chatMembers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns messages of the chat
|
||||
* @param limit - the limit of messages to return
|
||||
* @param offset - the offset of messages to return
|
||||
* @param containing - filter by containing
|
||||
*/
|
||||
public async messages({first, offset, containing}: {first?: number, offset?: number, containing?: string}) {
|
||||
const lim = first || 16;
|
||||
const offs = offset || 0;
|
||||
|
||||
const result = await queryHelper.all({
|
||||
cache: true,
|
||||
text: "SELECT * FROM chat_messages WHERE chat = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3",
|
||||
values: [this.id, lim, offs],
|
||||
});
|
||||
|
||||
const messages = [];
|
||||
const users: any = {};
|
||||
for (const row of result) {
|
||||
if (!users[row.author]) {
|
||||
const user = new User(row.author);
|
||||
await user.exists();
|
||||
users[row.author] = user;
|
||||
}
|
||||
messages.push(new ChatMessage(users[row.author], this, row.created_at, row.content));
|
||||
}
|
||||
if (containing) {
|
||||
return messages.filter((x) => x.content.includes(containing));
|
||||
} else {
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,163 @@
|
||||
import {RequestNotFoundError} from "../errors/RequestNotFoundError";
|
||||
import {Chatroom} from "./Chatroom";
|
||||
import dataaccess, {queryHelper} from "./index";
|
||||
import {User} from "./User";
|
||||
import {Request} from "./Request";
|
||||
|
||||
export class Profile extends User {
|
||||
|
||||
/**
|
||||
* Returns all chatrooms (with pagination).
|
||||
* Skips the query if the user doesn't exist.
|
||||
* @param first
|
||||
* @param offset
|
||||
*/
|
||||
public async chats({first, offset}: {first: number, offset?: number}): Promise<Chatroom[]> {
|
||||
if (!(await this.exists())) {
|
||||
return [];
|
||||
}
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all open requests the user has send.
|
||||
*/
|
||||
public async sentRequests() {
|
||||
const result = await queryHelper.all({
|
||||
cache: true,
|
||||
text: "SELECT * FROM requests WHERE sender = $1",
|
||||
values: [this.id],
|
||||
});
|
||||
return this.getRequests(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all received requests of the user.
|
||||
*/
|
||||
public async receivedRequests() {
|
||||
const result = await queryHelper.all({
|
||||
cache: true,
|
||||
text: "SELECT * FROM requests WHERE receiver = $1",
|
||||
values: [this.id],
|
||||
});
|
||||
return this.getRequests(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the greenpoints of a user.
|
||||
* @param points
|
||||
*/
|
||||
public async setGreenpoints(points: number): Promise<number> {
|
||||
const result = await queryHelper.first({
|
||||
text: "UPDATE users SET greenpoints = $1 WHERE id = $2 RETURNING greenpoints",
|
||||
values: [points, this.id],
|
||||
});
|
||||
return result.greenpoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the email of the user
|
||||
* @param email
|
||||
*/
|
||||
public async setEmail(email: string): Promise<string> {
|
||||
const result = await queryHelper.first({
|
||||
text: "UPDATE users SET email = $1 WHERE users.id = $2 RETURNING email",
|
||||
values: [email, this.id],
|
||||
});
|
||||
return result.email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the handle of the user
|
||||
*/
|
||||
public async setHandle(handle: string): Promise<string> {
|
||||
const result = await queryHelper.first({
|
||||
text: "UPDATE users SET handle = $1 WHERE id = $2",
|
||||
values: [handle, this.id],
|
||||
});
|
||||
return result.handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the username of the user
|
||||
* @param name
|
||||
*/
|
||||
public async setName(name: string): Promise<string> {
|
||||
const result = await queryHelper.first({
|
||||
text: "UPDATE users SET name = $1 WHERE id = $2",
|
||||
values: [name, this.id],
|
||||
});
|
||||
return result.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Denys a request.
|
||||
* @param sender
|
||||
* @param type
|
||||
*/
|
||||
public async denyRequest(sender: number, type: dataaccess.RequestType) {
|
||||
await queryHelper.first({
|
||||
text: "DELETE FROM requests WHERE receiver = $1 AND sender = $2 AND type = $3",
|
||||
values: [this.id, sender, type],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts a request.
|
||||
* @param sender
|
||||
* @param type
|
||||
*/
|
||||
public async acceptRequest(sender: number, type: dataaccess.RequestType) {
|
||||
const exists = await queryHelper.first({
|
||||
cache: true,
|
||||
text: "SELECT 1 FROM requests WHERE receiver = $1 AND sender = $2 AND type = $3",
|
||||
values: [this.id, sender, type],
|
||||
});
|
||||
if (exists) {
|
||||
if (type === dataaccess.RequestType.FRIENDREQUEST) {
|
||||
await queryHelper.first({
|
||||
text: "INSERT INTO user_friends (user_id, friend_id) VALUES ($1, $2)",
|
||||
values: [this.id, sender],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw new RequestNotFoundError(sender, this.id, type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns request wrapper for a row database request result.
|
||||
* @param rows
|
||||
*/
|
||||
private getRequests(rows: any) {
|
||||
const requests = [];
|
||||
const requestUsers: any = {};
|
||||
|
||||
for (const row of rows) {
|
||||
let sender = requestUsers[row.sender];
|
||||
|
||||
if (!sender) {
|
||||
sender = new User(row.sender);
|
||||
requestUsers[row.sender] = sender;
|
||||
}
|
||||
let receiver = requestUsers[row.receiver];
|
||||
if (!receiver) {
|
||||
receiver = new User(row.receiver);
|
||||
requestUsers[row.receiver] = receiver;
|
||||
}
|
||||
requests.push(new Request(sender, receiver, row.type));
|
||||
}
|
||||
return requests;
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import dataaccess from "./index";
|
||||
import {User} from "./User";
|
||||
|
||||
/**
|
||||
* Represents a request to a user.
|
||||
*/
|
||||
export class Request {
|
||||
constructor(
|
||||
public readonly sender: User,
|
||||
public readonly receiver: User,
|
||||
public readonly type: dataaccess.RequestType) {}
|
||||
}
|
@ -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,9 @@
|
||||
import dataaccess from "../dataaccess";
|
||||
import {BaseError} from "./BaseError";
|
||||
|
||||
export class RequestNotFoundError extends BaseError {
|
||||
constructor(sender: number, receiver: number, type: dataaccess.RequestType) {
|
||||
super(`Request with sender '${sender}' and receiver '${receiver}' of type '${type}' 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;
|
@ -0,0 +1,11 @@
|
||||
export namespace is {
|
||||
const emailRegex = /\S+?@\S+?(\.\S+?)?\.\w{2,3}(.\w{2-3})?/g
|
||||
|
||||
/**
|
||||
* Tests if a string is a valid email.
|
||||
* @param testString
|
||||
*/
|
||||
export function email(testString: string) {
|
||||
return emailRegex.test(testString)
|
||||
}
|
||||
}
|
@ -1,3 +1,19 @@
|
||||
ALTER TABLE IF EXISTS votes
|
||||
ADD COLUMN IF NOT EXISTS vote_type varchar(8) DEFAULT 'upvote',
|
||||
ALTER COLUMN vote_type SET DEFAULT 'upvote';
|
||||
DO $$ BEGIN
|
||||
|
||||
ALTER TABLE IF EXISTS votes
|
||||
ADD COLUMN IF NOT EXISTS vote_type votetype DEFAULT 'UPVOTE',
|
||||
ALTER COLUMN vote_type TYPE votetype USING cast_to_votetype(vote_type::text),
|
||||
ALTER COLUMN vote_type DROP DEFAULT,
|
||||
ALTER COLUMN vote_type SET DEFAULT 'UPVOTE';
|
||||
|
||||
ALTER TABLE IF EXISTS posts
|
||||
ALTER COLUMN type TYPE posttype USING cast_to_posttype(type::text),
|
||||
ALTER COLUMN type DROP DEFAULT,
|
||||
ALTER COLUMN type SET DEFAULT 'MISC',
|
||||
DROP COLUMN IF EXISTS upvotes,
|
||||
DROP COLUMN IF EXISTS downvotes;
|
||||
|
||||
ALTER TABLE requests
|
||||
ADD COLUMN IF NOT EXISTS type requesttype DEFAULT 'FRIENDREQUEST';
|
||||
|
||||
END $$;
|
||||
|
@ -1,13 +0,0 @@
|
||||
div#feedcontainer
|
||||
div.postinput
|
||||
input(type=text placeholder='Post something')
|
||||
button.submitbutton Submit
|
||||
div.feeditem
|
||||
div.itemhead
|
||||
span.title Testuser
|
||||
span.handle
|
||||
a(href='#') @testuser
|
||||
span.date 23.09.19 10:07
|
||||
p.text
|
||||
| Example Test text.
|
||||
| This is a test
|
@ -1 +0,0 @@
|
||||
div#friendscontainer
|
@ -1,9 +0,0 @@
|
||||
html
|
||||
head
|
||||
title Greenvironment Network
|
||||
include ../includes/head
|
||||
body
|
||||
div#content
|
||||
include stylebar
|
||||
include feed
|
||||
include friends
|
@ -1,2 +0,0 @@
|
||||
div.stylebar
|
||||
h1 Greenvironment
|
@ -1 +0,0 @@
|
||||
link(rel='stylesheet' href='stylesheets/style.css')
|
@ -1,8 +0,0 @@
|
||||
html
|
||||
head
|
||||
title Greenvironment Network Login
|
||||
include ../includes/head
|
||||
body
|
||||
div#content
|
||||
include stylebar
|
||||
include login
|
@ -1,2 +0,0 @@
|
||||
div.stylebar
|
||||
h1 Greenvironment
|
@ -1,8 +0,0 @@
|
||||
html
|
||||
head
|
||||
title Greenvironment Network Register
|
||||
include ../includes/head
|
||||
body
|
||||
div#content
|
||||
include stylebar
|
||||
include register
|
@ -1,8 +0,0 @@
|
||||
div#input-register
|
||||
input(type=text placeholder='username')
|
||||
input(type=text placeholder='email')
|
||||
input(type=text placeholder='password')
|
||||
input(type=text placeholder='repeat password')
|
||||
button.registerButton Register
|
||||
a(href="/login" )
|
||||
| You are already part of greenvironment? - login
|
@ -1,2 +0,0 @@
|
||||
div.stylebar
|
||||
h1 Greenvironment
|
Loading…
Reference in New Issue