diff --git a/src/config/passport.config.ts b/src/config/passport.config.ts index c9441f8..ad68064 100644 --- a/src/config/passport.config.ts +++ b/src/config/passport.config.ts @@ -4,7 +4,7 @@ import passport from 'passport' import { ExtractJwt, Strategy } from 'passport-jwt' import User from '../models/user' -import { handleError, UserBanned, UserNoAuth } from '../utils/errors.utils' +import { handleError, UserBanned, UserNoAuth, UserNotAllowed } from '../utils/errors.utils' passport.serializeUser((user, done) => done(null, user)) passport.deserializeUser((id, done) => done(null, id)) @@ -20,7 +20,7 @@ passport.use(new Strategy({ return done(null, user) } catch (error) { - if (error.response.indexOf(noSessionResponses)) + if (error.response && error.response.indexOf(noSessionResponses)) return done(UserNoAuth) done(error) @@ -71,7 +71,7 @@ export const authenticate = async (req: Request, res: Response, next: NextFuncti return handleError(UserBanned, res) if (req.baseUrl === '/admin' && user.roles.indexOf('admin') === -1) - return handleError(UserNoAuth, res) + return handleError(UserNotAllowed, res) req.user = user diff --git a/src/controllers/internal.controller.ts b/src/controllers/internal.controller.ts index cf68651..a7cb230 100644 --- a/src/controllers/internal.controller.ts +++ b/src/controllers/internal.controller.ts @@ -37,7 +37,7 @@ app.put('/portal', authenticate, async (req, res) => { try { const doc = await StoredRoom.findOne({ 'info.portal.id': id }) - if(!doc) + if (!doc) return RoomNotFound //console.log('room found, updating status...') @@ -48,24 +48,22 @@ app.put('/portal', authenticate, async (req, res) => { //console.log('status updated and online members fetched:', online) - if(online.length > 0) { + if (online.length > 0) { /** * Broadcast allocation to all online clients */ const updateMessage = new WSMessage(0, allocation, 'PORTAL_UPDATE') - await updateMessage.broadcast(online) + await updateMessage.broadcastRoom(room) - if(status === 'open') { - //JanusId is -1 when a janus instance is not running. - if(allocation.janusId == -1) { - const token = signApertureToken(id), + if (status === 'open') + if (allocation.janusId) { + const token = signApertureToken(id), apertureMessage = new WSMessage(0, { ws: process.env.APERTURE_WS_URL, t: token }, 'APERTURE_CONFIG') - await apertureMessage.broadcast(online) + await apertureMessage.broadcastRoom(room) } else { - const janusMessage = new WSMessage(0, { id: janusId }, 'JANUS_CONFIG') - await janusMessage.broadcast(online) + const janusMessage = new WSMessage(0, { id: janusId, ip: janusIp }, 'JANUS_CONFIG') + await janusMessage.broadcastRoom(room) } - } } res.sendStatus(200) diff --git a/src/drivers/portals.driver.ts b/src/drivers/portals.driver.ts index b7b6abc..9053228 100644 --- a/src/drivers/portals.driver.ts +++ b/src/drivers/portals.driver.ts @@ -18,7 +18,7 @@ export const createPortal = (room: Room) => new Promise(async (resolve, reject) log(`Sending request to ${url}create with room id: ${room.id}`, [{ content: 'portals', color: 'MAGENTA' }]) await axios.post(`${url}create`, { roomId: room.id }, { headers }) - .catch((reason) => { + .catch(reason => { console.log(`AXIOS POST FAILED: ${reason}`) throw reason } @@ -36,7 +36,7 @@ export const destroyPortal = (room: Room) => new Promise(async (resolve, reject) { portal } = room if (!portal.id) - return + reject() await axios.delete(`${url}${portal.id}`, { headers }) diff --git a/src/models/room/index.ts b/src/models/room/index.ts index 362bcbf..2237dd0 100644 --- a/src/models/room/index.ts +++ b/src/models/room/index.ts @@ -14,6 +14,7 @@ import client from '../../config/redis.config' import WSMessage from '../../server/websocket/models/message' import { ControllerIsNotAvailable, + PortalAlreadyAssigned, PortalNotOpen, RoomNotFound, UserAlreadyInRoom, @@ -21,7 +22,7 @@ import { UserIsNotPermitted } from '../../utils/errors.utils' import { generateFlake } from '../../utils/generate.utils' -import { extractUserId, GroupedMessage, groupMessages } from '../../utils/helpers.utils' +import { extractUserId, GroupedMessage, groupMessages, UNALLOCATED_PORTALS_KEYS } from '../../utils/helpers.utils' export type RoomResolvable = Room | string @@ -46,7 +47,7 @@ export default class Room { public online: string[] constructor(json?: IRoom) { - if(!json) return + if (!json) return this.setup(json) } @@ -54,19 +55,19 @@ export default class Room { public load = (id: string) => new Promise(async (resolve, reject) => { try { const doc = await StoredRoom.findOne({ 'info.id': id }) - if(!doc) + if (!doc) return reject(RoomNotFound) this.setup(doc) resolve(this) - } catch(error) { + } catch (error) { reject(error) } }) public create = (name: string, creator: User) => new Promise(async (resolve, reject) => { - if(creator.room) + if (creator.room) return reject(UserAlreadyInRoom) try { @@ -77,7 +78,7 @@ export default class Room { type: 'vm', portal: { - status: 'waiting', + status: 'closed', lastUpdatedAt: Date.now() }, @@ -100,7 +101,7 @@ export default class Room { client.hset('controller', this.id, creator.id) resolve(this) - } catch(error) { + } catch (error) { reject(error) } }) @@ -111,25 +112,25 @@ export default class Room { public createInvite = (creator: User, system: boolean) => new Promise(async (resolve, reject) => { try { const invite = await new Invite().create( - this, - 'room', - { maxUses: 0, unlimitedUses: true }, - { system: true }, + this, + 'room', + { maxUses: 0, unlimitedUses: true }, + { system: true }, creator ) - if(!this.invites) + if (!this.invites) this.invites = [] this.invites.push(invite) - if(system) { + if (system) { const message = new WSMessage(0, invite, 'INVITE_UPDATE') message.broadcast([ extractUserId(this.owner) ]) } resolve(invite) - } catch(error) { + } catch (error) { reject(error) } }) @@ -152,7 +153,7 @@ export default class Room { this.owner = to resolve(this) - } catch(error) { + } catch (error) { reject(error) } }) @@ -160,8 +161,8 @@ export default class Room { public fetchMembers = (index: number = 0) => new Promise(async (resolve, reject) => { try { const docs = await StoredUser.find({ 'info.room': this.id }).skip(index).limit(10) - - if(docs.length === 0) + + if (docs.length === 0) return resolve(this) const members = docs.map(doc => new User(doc)) @@ -171,15 +172,15 @@ export default class Room { controllerId = extractUserId(this.controller) members.forEach(member => { - if(ownerId === member.id) + if (ownerId === member.id) this.owner = member - - if(controllerId === member.id) + + if (controllerId === member.id) this.controller = member }) resolve(this) - } catch(error) { + } catch (error) { reject(error) } }) @@ -192,23 +193,24 @@ export default class Room { this.online = connectedClientIds.filter(id => memberIds.indexOf(id) > -1) resolve(this) - } catch(error) { + } catch (error) { reject(error) } }) public fetchMessages = (index: number = 0) => new Promise(async (resolve, reject) => { try { - const docs = await StoredMessage.find({ 'info.room': this.id }).sort({ 'info.createdAt': -1 }).skip(index).limit(50) - - if(docs.length === 0) + const docs = await StoredMessage.find({ 'info.room': this.id }) + .sort({ 'info.createdAt': -1 }).skip(index).limit(50) + + if (docs.length === 0) return resolve(this) const messages = docs.map(doc => new Message(doc)) this.messages = groupMessages(messages.reverse()) resolve(this) - } catch(error) { + } catch (error) { reject(error) } }) @@ -229,14 +231,14 @@ export default class Room { ] }).skip(index).limit(10) - if(docs.length === 0) + if (docs.length === 0) return resolve(this) const invites = docs.map(doc => new Invite(doc)) this.invites = invites resolve(this) - } catch(error) { + } catch (error) { reject(error) } }) @@ -248,7 +250,7 @@ export default class Room { const invite = await this.createInvite(user, system) resolve(invite) - } catch(error) { + } catch (error) { reject(error) } }) @@ -269,7 +271,7 @@ export default class Room { this.invites = [] resolve(this) - } catch(error) { + } catch (error) { reject(error) } }) @@ -278,8 +280,6 @@ export default class Room { try { const allocation: IPortalAllocation = { id, - janusId: 1, - janusIp: '0.0.0.0', status: 'creating', lastUpdatedAt: Date.now() } @@ -298,7 +298,7 @@ export default class Room { this.portal = allocation resolve(this) - } catch(error) { + } catch (error) { reject(error) } }) @@ -307,11 +307,18 @@ export default class Room { allocation.lastUpdatedAt = Date.now() try { - const currentAllocation = this.portal + let currentAllocation: IPortalAllocation = this.portal + if (!currentAllocation) + // dummy allocation. will get updated, anyway. + currentAllocation = { + id: null, + status: 'closed' + } + Object.keys(allocation).forEach(key => currentAllocation[key] = allocation[key]) - if(currentAllocation.status === 'closed') - delete currentAllocation.id + if (currentAllocation.status === 'closed') + currentAllocation.id = null await StoredRoom.updateOne({ 'info.id': this.id @@ -324,7 +331,7 @@ export default class Room { this.portal = currentAllocation resolve(this) - } catch(error) { + } catch (error) { reject(error) } }) @@ -332,7 +339,7 @@ export default class Room { public takeControl = (from: UserResolvable) => new Promise(async (resolve, reject) => { const fromId = extractUserId(from) - if(this.controller !== null) + if (this.controller !== null) return reject(ControllerIsNotAvailable) try { @@ -352,7 +359,7 @@ export default class Room { this.controller = fromId resolve(this) - } catch(error) { + } catch (error) { reject(error) } }) @@ -363,7 +370,7 @@ export default class Room { toId = extractUserId(to), fromId = extractUserId(from) - if(fromId !== controllerId && fromId !== ownerId) + if (fromId !== controllerId && fromId !== ownerId) return reject(UserDoesNotHaveRemote) try { @@ -383,7 +390,7 @@ export default class Room { this.controller = toId resolve(this) - } catch(error) { + } catch (error) { reject(error) } }) @@ -393,7 +400,7 @@ export default class Room { senderId = extractUserId(sender), controllerId = extractUserId(this.controller) - if(senderId !== ownerId && senderId !== controllerId) + if (senderId !== ownerId && senderId !== controllerId) return reject(UserIsNotPermitted) try { @@ -413,15 +420,41 @@ export default class Room { this.controller = null resolve(this) - } catch(error) { + } catch (error) { reject(error) } }) - public createPortal = () => createPortal(this) + public createPortal = () => new Promise(async (resolve, reject) => { + if (UNALLOCATED_PORTALS_KEYS.indexOf(this.portal.status) === -1) + return reject(PortalAlreadyAssigned) + + await this.updatePortalAllocation({ status: 'waiting' }) + const update = new WSMessage(0, this.portal, 'PORTAL_UPDATE') + update.broadcastRoom(this) + + try { + await createPortal(this) + } catch (error) { + await this.updatePortalAllocation({ status: 'error' }) + const message = new WSMessage(0, this.portal, 'PORTAL_UPDATE') + message.broadcastRoom(this) + + // we cannot reject with an error since it'll break things, at least as of how things work right now. + // reject(error) + + // god forgive me for this thing I'm going to do + setTimeout(async () => { + if (this.portal.status === 'error' && this.portal.lastUpdatedAt >= 15000) { + return await this.createPortal() + } + }, 15000) + } + resolve() + }) public restartPortal = () => new Promise(async (resolve, reject) => { - if(this.portal.status !== 'open') + if (!this.portal.id) return reject(PortalNotOpen) try { @@ -429,7 +462,7 @@ export default class Room { await this.createPortal() resolve() - } catch(error) { + } catch (error) { reject(error) } }) @@ -437,8 +470,6 @@ export default class Room { public destroyPortal = async () => { await destroyPortal(this) await this.updatePortalAllocation({ status: 'closed' }) - - delete this.portal } public updateType = (type: RoomType) => new Promise(async (resolve, reject) => { @@ -454,7 +485,7 @@ export default class Room { this.type = type resolve(this) - } catch(error) { + } catch (error) { reject(error) } }) @@ -477,13 +508,13 @@ export default class Room { await this.destroyInvites() - if(this.portal) + if (this.portal) destroyPortal(this) await client.hdel('controller', this.id) resolve() - } catch(error) { + } catch (error) { reject(error) } }) diff --git a/src/models/user/defs.ts b/src/models/user/defs.ts index 5ebb7bf..db18ec3 100644 --- a/src/models/user/defs.ts +++ b/src/models/user/defs.ts @@ -20,7 +20,8 @@ export type Credentials = IRegularCredentials | IDiscordCredentials export interface IProfile { name: string - icon: string + icon: string, + hoverIcon?: string } export default interface IUser { diff --git a/src/models/user/index.ts b/src/models/user/index.ts index 3046c79..aa4317f 100644 --- a/src/models/user/index.ts +++ b/src/models/user/index.ts @@ -4,18 +4,17 @@ import IUser, { IDiscordCredentials, Role } from './defs' import Room from '../room' import Ban from './ban' -import { createPortal } from '../../drivers/portals.driver' import StoredBan from '../../schemas/ban.schema' import StoredMessage from '../../schemas/message.schema' -import config from '../../config/defaults.js' +import config from '../../config/defaults' import client from '../../config/redis.config' import WSMessage from '../../server/websocket/models/message' import { constructAvatar, exchangeRefreshToken, fetchUserProfile } from '../../services/oauth2/discord.service' import { TooManyMembers, UserNotFound, UserNotInRoom } from '../../utils/errors.utils' import { generateFlake, signToken } from '../../utils/generate.utils' -import { extractUserId, UNALLOCATED_PORTALS_KEYS, extractRoomId } from '../../utils/helpers.utils' +import { extractRoomId, extractUserId, UNALLOCATED_PORTALS_KEYS } from '../../utils/helpers.utils' export type UserResolvable = User | string @@ -28,6 +27,7 @@ export default class User { public name: string public icon: string + public hoverIcon?: string public room?: Room | string @@ -82,7 +82,12 @@ export default class User { userId: id, email, hash: avatarHash - }) + }, false), + animAvatar = constructAvatar({ + userId: id, + email, hash: + avatarHash + }, true) if (existing) { this.setup(existing) @@ -93,6 +98,7 @@ export default class User { $set: { 'profile.name': name, 'profile.icon': avatar, + 'profile.hoverIcon': animAvatar, 'security.credentials.email': email, 'security.credentials.scopes': scopes, @@ -123,7 +129,8 @@ export default class User { }, profile: { name, - icon: avatar + icon: avatar, + hoverIcon: animAvatar } } @@ -145,12 +152,18 @@ export default class User { { refreshToken } = (credentials as IDiscordCredentials), { access_token, refresh_token } = await exchangeRefreshToken(refreshToken), { id, username: name, email, avatar: avatarHash } = await fetchUserProfile(access_token), - icon = constructAvatar({ + avatar = constructAvatar({ userId: id, email, hash: avatarHash - }) + }, false), + animAvatar = constructAvatar({ + userId: id, + email, + + hash: avatarHash + }, true) await StoredUser.updateOne({ 'info.id': this.id @@ -160,16 +173,18 @@ export default class User { 'security.credentials.refreshToken': refresh_token, 'profile.name': name, - 'profile.icon': icon + 'profile.icon': avatar, + 'profile.hoverIcon': animAvatar, } }) this.name = name - this.icon = icon + this.icon = avatar + this.hoverIcon = animAvatar if (this.room) { const message = new WSMessage(0, this, 'USER_UPDATE') - message.broadcastRoom(this.room, [this.id]) + await message.broadcastRoom(this.room, [this.id]) } resolve(this) @@ -196,8 +211,7 @@ export default class User { const roomId = extractRoomId(this.room) try { - const room = await new Room().load(roomId) - this.room = room + this.room = await new Room().load(roomId) resolve(this) } catch (error) { @@ -255,7 +269,7 @@ export default class User { room.members.length === (config.min_member_portal_creation_count - 1) && UNALLOCATED_PORTALS_KEYS.indexOf(room.portal.status) > -1 ) - createPortal(room) + room.createPortal() const message = new WSMessage(0, { ...this, room: undefined }, 'USER_JOIN') message.broadcastRoom(room) @@ -270,10 +284,10 @@ export default class User { public leaveRoom = () => new Promise(async (resolve, reject) => { try { - if (typeof this.room === 'string') + if (this.room && typeof this.room === 'string') await this.fetchRoom() - if (typeof this.room === 'string') + if (!this.room || typeof this.room === 'string') return await this.room.fetchMembers() @@ -293,10 +307,10 @@ export default class User { const leavingUserIsOwner = this.id === extractUserId(this.room.owner) if (leavingUserIsOwner) - this.room.transferOwnership(this.room.members[0]) + await this.room.transferOwnership(this.room.members[0]) const message = new WSMessage(0, { u: this.id }, 'USER_LEAVE') - message.broadcastRoom(this.room) + await message.broadcastRoom(this.room) } await StoredUser.updateOne({ @@ -307,7 +321,7 @@ export default class User { } }) - client.hset('undelivered_events', this.id, JSON.stringify([])) + await client.hset('undelivered_events', this.id, JSON.stringify([])) delete this.room @@ -345,6 +359,7 @@ export default class User { this.name = json.profile.name this.icon = json.profile.icon + this.hoverIcon = json.profile.hoverIcon if (!this.room) this.room = json.info.room diff --git a/src/schemas/user.schema.ts b/src/schemas/user.schema.ts index 43e91c1..e9508ed 100644 --- a/src/schemas/user.schema.ts +++ b/src/schemas/user.schema.ts @@ -25,7 +25,8 @@ const UserSchema = new Schema({ }, profile: { name: String, - icon: String + icon: String, + hoverIcon: String } }, { typeKey: '$type' diff --git a/src/server/websocket/index.ts b/src/server/websocket/index.ts index 0fd002a..5e5ac18 100644 --- a/src/server/websocket/index.ts +++ b/src/server/websocket/index.ts @@ -120,7 +120,7 @@ export default (wss: Server) => { if (extractUserId(room.controller) === socket.user.id) room.releaseControl(socket.user) - if (config.destroy_portal_when_empty) { + if (config.destroy_portal_when_empty) setTimeout(async () => (await room.load(room.id)).fetchOnlineMemberIds().then(({ portal, online }) => { if (online.length > 0) @@ -132,7 +132,6 @@ export default (wss: Server) => { room.destroyPortal() }).catch(console.error), config.empty_room_portal_destroy * 1000 ) - } } }) }) diff --git a/src/server/websocket/models/socket.ts b/src/server/websocket/models/socket.ts index 476794f..8a9d50d 100644 --- a/src/server/websocket/models/socket.ts +++ b/src/server/websocket/models/socket.ts @@ -76,26 +76,25 @@ export default class WSSocket { const { room } = user as { room: Room } const message = new WSMessage(0, { u: user.id, presence: 'online' }, 'PRESENCE_UPDATE') - message.broadcastRoom(room, [user.id]) + await message.broadcastRoom(room, [user.id]) if (room.portal.status !== 'open') { - room.fetchMembers().then(({ members }) => { + room.fetchMembers().then(async ({ members }) => { if ( members.length > (config.min_member_portal_creation_count - 1) && UNALLOCATED_PORTALS_KEYS.indexOf(room.portal.status) > -1 ) - room.createPortal() + await room.createPortal() }) - } else if (room.portal.id) { - //JanusId is -1 when a janus instance is not running. - if(room.portal.janusId == -1) { - const token = signApertureToken(room.portal.id), apertureMessage = new WSMessage(0, { ws: process.env.APERTURE_WS_URL, t: token }, 'APERTURE_CONFIG') - apertureMessage.broadcast([ extractUserId(user) ]) - } else { + } else if (room.portal.id) + if (room.portal.janusId) { const janusMessage = new WSMessage(0, { id: room.portal.janusId, ip: room.portal.janusIp }, 'JANUS_CONFIG') - janusMessage.broadcast([ extractUserId(user) ]) + await janusMessage.broadcast([ extractUserId(user) ]) + } else { + const token = signApertureToken(room.portal.id), + apertureMessage = new WSMessage(0, { ws: process.env.APERTURE_WS_URL, t: token }, 'APERTURE_CONFIG') + await apertureMessage.broadcast([ extractUserId(user) ]) } - } } // Log update diff --git a/src/services/oauth2/discord.service.ts b/src/services/oauth2/discord.service.ts index f2c238c..2dd3983 100644 --- a/src/services/oauth2/discord.service.ts +++ b/src/services/oauth2/discord.service.ts @@ -20,14 +20,14 @@ interface IAvatarConstruction { hash?: string } -export const constructAvatar = (data: IAvatarConstruction) => { +export const constructAvatar = (data: IAvatarConstruction, animated = true) => { if (!data.hash) return `https://www.gravatar.com/avatar/${md5(data.email || '')}?d=retro&s=128` const { userId, hash } = data let url = `https://cdn.discordapp.com/avatars/${userId}/` - if (data.hash.substr(0, 2) === 'a_') + if (hash.substr(0, 2) === 'a_' && animated) url += `${hash}.gif` else url += `${hash}.png` diff --git a/src/utils/errors.utils.ts b/src/utils/errors.utils.ts index 6f6cc4d..e2e1b23 100644 --- a/src/utils/errors.utils.ts +++ b/src/utils/errors.utils.ts @@ -36,6 +36,15 @@ export const UserNotAuthorized: IAPIResponse = { status: 401 } +export const UserNotAllowed: IAPIResponse = { + response: 'USER_NOT_ALLOWED', + error: { + title: 'User Not Allowed', + description: 'You\'re lacking permissions to access this.' + }, + status: 401 +} + export const UserNotInRoom: IAPIResponse = { response: 'USER_NOT_IN_ROOM', error: { @@ -216,6 +225,15 @@ export const PortalNotOpen: IAPIResponse = { status: 409 } +export const PortalAlreadyAssigned: IAPIResponse = { + response: 'PORTAL_ALREADY_ASSIGNED', + error: { + title: 'Portal Already Assigned', + description: 'This room already has a portal assigned.' + }, + status: 409 +} + export const handleError = (error: any, res: Response) => { if (process.env.NODE_ENV === 'development') console.error(error)