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 { 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 { 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 { 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 { 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 { 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 { await (await models.Post.findByPk(postId)).destroy(); return true; } /** * Creates a chatroom containing two users * @param members */ export async function createChat(...members: number[]): Promise { 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 { 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 { 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 { 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;