|
|
|
import * as crypto from "crypto";
|
|
|
|
import * as status from "http-status";
|
|
|
|
import * as sqz from "sequelize";
|
|
|
|
import {Sequelize} from "sequelize-typescript";
|
|
|
|
import {ChatNotFoundError} from "./errors/ChatNotFoundError";
|
|
|
|
import {EmailAlreadyRegisteredError} from "./errors/EmailAlreadyRegisteredError";
|
|
|
|
import {GroupNotFoundGqlError, NotLoggedInGqlError} from "./errors/graphqlErrors";
|
|
|
|
import {GroupNotFoundError} from "./errors/GroupNotFoundError";
|
|
|
|
import {InvalidLoginError} from "./errors/InvalidLoginError";
|
|
|
|
import {NoActionSpecifiedError} from "./errors/NoActionSpecifiedError";
|
|
|
|
import {UserNotFoundError} from "./errors/UserNotFoundError";
|
|
|
|
import globals from "./globals";
|
|
|
|
import {InternalEvents} from "./InternalEvents";
|
|
|
|
import * as models from "./models";
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generates a new handle from the username and a base64 string of the current time.
|
|
|
|
* @param username
|
|
|
|
*/
|
|
|
|
async function generateHandle(username: string) {
|
|
|
|
username = username.toLowerCase().replace(/\s/g, "_");
|
|
|
|
const count = await models.User.count({where: {handle: {[sqz.Op.like]: `%${username}%`}}});
|
|
|
|
if (count > 0) {
|
|
|
|
return `${username}${count}`;
|
|
|
|
} else {
|
|
|
|
return username;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Namespace with functions to fetch initial data for wrapping.
|
|
|
|
*/
|
|
|
|
namespace dataaccess {
|
|
|
|
|
|
|
|
let sequelize: Sequelize;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initializes everything that needs to be initialized asynchronous.
|
|
|
|
*/
|
|
|
|
export async function init(seq: Sequelize) {
|
|
|
|
sequelize = seq;
|
|
|
|
try {
|
|
|
|
await sequelize.addModels([
|
|
|
|
models.ChatMember,
|
|
|
|
models.ChatMessage,
|
|
|
|
models.ChatRoom,
|
|
|
|
models.Friendship,
|
|
|
|
models.Post,
|
|
|
|
models.PostVote,
|
|
|
|
models.Request,
|
|
|
|
models.User,
|
|
|
|
models.Group,
|
|
|
|
models.GroupAdmin,
|
|
|
|
models.GroupMember,
|
|
|
|
models.EventParticipant,
|
|
|
|
models.Event,
|
|
|
|
]);
|
|
|
|
} catch (err) {
|
|
|
|
globals.logger.error(err.message);
|
|
|
|
globals.logger.debug(err.stack);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the user by handle.
|
|
|
|
* @param userHandle
|
|
|
|
*/
|
|
|
|
export async function getUserByHandle(userHandle: string): Promise<models.User> {
|
|
|
|
const user = await models.User.findOne({where: {handle: userHandle}});
|
|
|
|
if (user) {
|
|
|
|
return user;
|
|
|
|
} else {
|
|
|
|
throw new UserNotFoundError(userHandle);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the user by email and password
|
|
|
|
* @param email
|
|
|
|
* @param password
|
|
|
|
*/
|
|
|
|
export async function getUserByLogin(email: string, password: string): Promise<models.User> {
|
|
|
|
const hash = crypto.createHash("sha512");
|
|
|
|
hash.update(password);
|
|
|
|
password = hash.digest("hex");
|
|
|
|
const user = await models.User.findOne({where: {email}});
|
|
|
|
if (user) {
|
|
|
|
if (user.password === password) {
|
|
|
|
return user;
|
|
|
|
} else {
|
|
|
|
throw new InvalidLoginError(email);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw new UserNotFoundError(email);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Registers a user with a username and password returning a user
|
|
|
|
* @param username
|
|
|
|
* @param email
|
|
|
|
* @param password
|
|
|
|
*/
|
|
|
|
export async function registerUser(username: string, email: string, password: string): Promise<models.User> {
|
|
|
|
const hash = crypto.createHash("sha512");
|
|
|
|
hash.update(password);
|
|
|
|
password = hash.digest("hex");
|
|
|
|
const existResult = !!(await models.User.findOne({where: {username, email, password}}));
|
|
|
|
const handle = await generateHandle(username);
|
|
|
|
if (!existResult) {
|
|
|
|
return models.User.create({username, email, password, handle});
|
|
|
|
} else {
|
|
|
|
throw new EmailAlreadyRegisteredError(email);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns a post for a given postId.s
|
|
|
|
* @param postId
|
|
|
|
*/
|
|
|
|
export async function getPost(postId: number): Promise<models.Post> {
|
|
|
|
const post = await models.Post.findByPk(postId);
|
|
|
|
if (post) {
|
|
|
|
return post;
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns all posts sorted by new or top with pagination.
|
|
|
|
* @param first
|
|
|
|
* @param offset
|
|
|
|
* @param sort
|
|
|
|
*/
|
|
|
|
export async function getPosts(first: number, offset: number, sort: SortType) {
|
|
|
|
if (sort === SortType.NEW) {
|
|
|
|
return models.Post.findAll({
|
|
|
|
include: [{association: "rVotes"}],
|
|
|
|
limit: first,
|
|
|
|
offset,
|
|
|
|
order: [["createdAt", "DESC"]],
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
return await sequelize.query(
|
|
|
|
`SELECT * FROM (
|
|
|
|
SELECT *,
|
|
|
|
(SELECT count(*) FROM post_votes 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
|
|
|
|
FROM posts) AS a ORDER BY (a.upvotes - a.downvotes) DESC LIMIT ? OFFSET ?`,
|
|
|
|
{replacements: [first, offset], mapToModel: true, model: models.Post}) as models.Post[];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a post
|
|
|
|
* @param content
|
|
|
|
* @param authorId
|
|
|
|
* @param type
|
|
|
|
*/
|
|
|
|
export async function createPost(content: string, authorId: number, type?: string): Promise<models.Post> {
|
|
|
|
type = type || "MISC";
|
|
|
|
const post = await models.Post.create({content, authorId});
|
|
|
|
globals.internalEmitter.emit(InternalEvents.POSTCREATE, post);
|
|
|
|
return post;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Deletes a post
|
|
|
|
* @param postId
|
|
|
|
*/
|
|
|
|
export async function deletePost(postId: number): Promise<boolean> {
|
|
|
|
await (await models.Post.findByPk(postId)).destroy();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a chatroom containing two users
|
|
|
|
* @param members
|
|
|
|
*/
|
|
|
|
export async function createChat(...members: number[]): Promise<models.ChatRoom> {
|
|
|
|
return sequelize.transaction(async (t) => {
|
|
|
|
const chat = await models.ChatRoom.create({}, {transaction: t, include: [models.User]});
|
|
|
|
for (const member of members) {
|
|
|
|
const user = await models.User.findByPk(member);
|
|
|
|
await chat.$add("rMember", user, {transaction: t});
|
|
|
|
}
|
|
|
|
await chat.save({transaction: t});
|
|
|
|
globals.internalEmitter.emit(InternalEvents.CHATCREATE, chat);
|
|
|
|
return chat;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sends a message into a chat.
|
|
|
|
* @param authorId
|
|
|
|
* @param chatId
|
|
|
|
* @param content
|
|
|
|
*/
|
|
|
|
export async function sendChatMessage(authorId: number, chatId: number, content: string) {
|
|
|
|
const chat = await models.ChatRoom.findByPk(chatId);
|
|
|
|
if (chat) {
|
|
|
|
const message = await chat.$create("rMessage", {content, authorId}) as models.ChatMessage;
|
|
|
|
globals.internalEmitter.emit(InternalEvents.CHATMESSAGE, message);
|
|
|
|
return message;
|
|
|
|
} else {
|
|
|
|
throw new ChatNotFoundError(chatId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns all rChats.
|
|
|
|
*/
|
|
|
|
export async function getAllChats(): Promise<models.ChatRoom[]> {
|
|
|
|
return models.ChatRoom.findAll();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sends a request to a user.
|
|
|
|
* @param sender
|
|
|
|
* @param receiver
|
|
|
|
* @param requestType
|
|
|
|
*/
|
|
|
|
export async function createRequest(sender: number, receiver: number, requestType?: RequestType) {
|
|
|
|
requestType = requestType || RequestType.FRIENDREQUEST;
|
|
|
|
|
|
|
|
const request = await models.Request.create({senderId: sender, receiverId: receiver, requestType});
|
|
|
|
globals.internalEmitter.emit(InternalEvents.REQUESTCREATE, request);
|
|
|
|
return request;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a new group.
|
|
|
|
* @param name
|
|
|
|
* @param creator
|
|
|
|
* @param members
|
|
|
|
*/
|
|
|
|
export async function createGroup(name: string, creator: number, members: number[]): Promise<models.Group> {
|
|
|
|
members = members || [];
|
|
|
|
return sequelize.transaction(async (t) => {
|
|
|
|
members.push(creator);
|
|
|
|
const groupChat = await createChat(...members);
|
|
|
|
const group = await models.Group.create({name, creatorId: creator, chatId: groupChat.id}, {transaction: t});
|
|
|
|
const creatorUser = await models.User.findByPk(creator, {transaction: t});
|
|
|
|
await group.$add("rAdmins", creatorUser, {transaction: t});
|
|
|
|
for (const member of members) {
|
|
|
|
const user = await models.User.findByPk(member, {transaction: t});
|
|
|
|
await group.$add("rMembers", user, {transaction: t});
|
|
|
|
}
|
|
|
|
return group;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Changes the membership of a user
|
|
|
|
* @param groupId
|
|
|
|
* @param userId
|
|
|
|
* @param action
|
|
|
|
*/
|
|
|
|
export async function changeGroupMembership(groupId: number, userId: number, action: MembershipChangeAction):
|
|
|
|
Promise<models.Group> {
|
|
|
|
const group = await models.Group.findByPk(groupId);
|
|
|
|
if (group) {
|
|
|
|
const user = await models.User.findByPk(userId);
|
|
|
|
if (user) {
|
|
|
|
if (action === MembershipChangeAction.ADD) {
|
|
|
|
await group.$add("rMembers", user);
|
|
|
|
} else if (action === MembershipChangeAction.REMOVE) {
|
|
|
|
await group.$remove("rMembers", user);
|
|
|
|
} else if (action === MembershipChangeAction.OP) {
|
|
|
|
await group.$add("rAdmins", user);
|
|
|
|
} else if (action === MembershipChangeAction.DEOP) {
|
|
|
|
await group.$remove("rAdmins", user);
|
|
|
|
} else {
|
|
|
|
throw new NoActionSpecifiedError(MembershipChangeAction);
|
|
|
|
}
|
|
|
|
return group;
|
|
|
|
} else {
|
|
|
|
throw new UserNotFoundError(userId);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw new GroupNotFoundError(groupId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Enum representing the types of votes that can be performed on a post.
|
|
|
|
*/
|
|
|
|
export enum VoteType {
|
|
|
|
UPVOTE = "UPVOTE",
|
|
|
|
DOWNVOTE = "DOWNVOTE",
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Enum representing the types of request that can be created.
|
|
|
|
*/
|
|
|
|
export enum RequestType {
|
|
|
|
FRIENDREQUEST = "FRIENDREQUEST",
|
|
|
|
GROUPINVITE = "GROUPINVITE",
|
|
|
|
EVENTINVITE = "EVENTINVITE",
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Enum representing the types of sorting in the feed.
|
|
|
|
*/
|
|
|
|
export enum SortType {
|
|
|
|
TOP = "TOP",
|
|
|
|
NEW = "NEW",
|
|
|
|
}
|
|
|
|
|
|
|
|
export enum MembershipChangeAction {
|
|
|
|
ADD,
|
|
|
|
REMOVE,
|
|
|
|
OP,
|
|
|
|
DEOP,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default dataaccess;
|