Vue.use(VueMaterial.default); const getCreatePostError = (json) => { switch (json.message) { case 'ERROR_REQUEST_INVALID_DATA': { switch (json.errors[0].param) { case 'title': { return 'Invalid title. Must be between 3 and 32 characters.'; } case 'body': { return 'Invalid content. Must be between 3 and 1000 characters'; } case 'category': { return 'Invalid category. Something went wrong.'; } default: { return 'Invalid value sent to server. Something went wrong.'; } } } case 'ERROR_CATEGORY_NOT_FOUND': { return 'The category you tried to post to no longer exists.'; } case 'ERROR_ACCESS_DENIED': { return 'You are not allowed to perform this action.' } default: { return 'Unknown error. Something went wrong.'; } } } const getCreateCategoryError = (json) => { switch (json.message) { case 'ERROR_REQUEST_INVALID_DATA': { switch (json.errors[0].param) { case 'title': { return 'Invalid title. Title must be between 3 and 32 characters.'; } } } case 'ERROR_ACCESS_DENIED': { return 'You are not allowed to perform this action.' } default: { return 'Unknown error. Something went wrong.'; } } } class GatewayConnection { constructor() { this.isConnected = false; this.socket = null; // TODO: set up proper event listening and such, not this dumb crap this.onDisconnect = () => {} this.onConnect = () => {} } } GatewayConnection.prototype.disconnect = function() { this.socket?.disconnect(); this.socket = null; this.isConnected = false; }; GatewayConnection.prototype.connect = function(token) { console.log('[*] [gateway] [handshake] Trying to connect to gateway'); this.socket = io('/gateway', { query: { token }, transports: ['websocket'] }); this.socket.on('connect', () => { this.socket.once('hello', (debugInfo) => { console.log('[*] [gateway] [handshake] Got hello from server, sending yoo...', debugInfo); this.socket.emit('yoo'); this.isConnected = true; this.debugInfo = debugInfo; this.onConnect('CONNECT_RECEIVED_HELLO'); console.log('[*] [gateway] [handshake] Assuming that server received yoo and that connection is completed.'); }); }) this.socket.on('error', (e) => { console.log('[E] [gateway] Gateway error', e); this.isConnected = false; this.socket = null; this.onDisconnect('DISCONNECT_ERR', e); }); this.socket.on('disconnectNotification', (e) => { console.log('[E] [gateway] Received disconnect notfication', e); this.isConnected = false; this.socket = null; this.onDisconnect('DISCONNECT_NOTIF', e); }); this.socket.on('disconnect', (e) => { console.log('[E] [gateway] Disconnected from gateway: ', e); this.isConnected = false; this.onDisconnect('DISCONNECT', e); }); }; GatewayConnection.prototype.sendMessage = function(categoryId, content) { if (!this.isConnected) return 1; if (content.length >= 2000) return 1; this.socket.emit('message', { category: { _id: categoryId }, content }); }; GatewayConnection.prototype.subscribeToCategoryChat = function(categoryId) { if (!this.isConnected) return; const request = [categoryId]; console.log('[*] [gateway] Subscribing to channel(s)', request); this.socket.emit('subscribe', request); }; const app = new Vue({ el: '#app', data: { showSnackbarNotification: false, snackbarNotification: '', snackbarNotificationDuration: 999999, snackbarButtonText: 'Ok', loggedInUser: {}, showApp: false, menuVisible: false, selection: { category: { title: '', browsing: false, _id: undefined, isCategory: false, isChatContext: false }, posts: [] }, cardButtons: [], dialog: { show: { createPost: false, createCategory: false, debug: false }, text: { createPost: { title: '', body: '' }, createCategory: { title: '' } } }, viewingProfile: { show: false, _id: '', username: '', role: '' }, gateway: new GatewayConnection(), messages: { 'X': [ { username: '__SYSTEM', content: 'TEST MSG' } ] }, userLists: { 'X': [ { username: '__SYSTEM', _id: 'INVALID_ID' } ] }, message: { typed: '' } }, mounted: async function() { const res = await fetch(`${window.location.origin}/api/v1/users/current/info`, { method: 'GET', headers: { 'Accept': 'application/json', }, credentials: 'include' }); if (res.ok) { const json = await res.json(); if (json.user.permissionLevel >= 1) { this.loggedInUser = json.user; this.showApp = true; this.performGatewayConnection(); this.browseCategories(); Notification.requestPermission(); } else { this.showApp = false; this.snackbarEditButton('Manage', () => { window.location.href = `${window.location.origin}/auth.html`; this.resetSnackbarButton(); this.showSnackbarNotification = false; }); this.notification('Your account does not have the required permissions to enter this page'); } } else { this.showApp = false; this.snackbarEditButton('Manage', () => { window.location.href = `${window.location.origin}/auth.html`; this.resetSnackbarButton(); this.showSnackbarNotification = false; }); this.notification('You are not logged in or your session is invalid'); } }, methods: { // Gateway and chat performGatewayConnection: function() { // TODO: again, the thing im doing with the token is not very secure, since its being sent by the current user info endpoint and is also being send through query parameters this.gateway.onDisconnect = (e) => { this.okNotification('Connection lost.'); }; this.gateway.connect(this.loggedInUser.token); this.gateway.socket.on('message', (e) => { //console.log('[*] [gateway] Message received', e); this.processMessage(e); }); this.gateway.socket.on('clientListUpdate', (e) => { console.log('[*] [gateway] Client list update', e); this.processUserListUpdate(e); }); this.gateway.socket.on('refreshClient', (e) => { console.log('[*] [gateway] Gateway requested refresh', e); this.gateway.disconnect(); this.messages = {}; this.userLists = {}; this.message.typed = ''; if (e.reason === 'exit') { this.showApp = false; this.okNotification('The server has exited. Sit tight!'); } else if (e.reason === 'upd') { this.showApp = false; this.okNotification('An update has just rolled out! To ensure everything runs smoothly, you need to refresh the page!'); } else { this.showApp = false; this.okNotification('Sorry, but something happened and a refresh is required to keep using the app!'); } this.snackbarEditButton('Refresh', () => { window.location.reload(); }); }); }, shouldMergeMessage: function(messageObject, categoryMessageList) { const lastMessageIndex = categoryMessageList.length-1; const lastMessage = categoryMessageList[lastMessageIndex]; if (!lastMessage) return; if (lastMessage.author._id === messageObject.author._id) { if (lastMessage.nickAuthor && messageObject.nickAuthor && lastMessage.nickAuthor.username === messageObject.nickAuthor.username) { return true; } } if (lastMessage.author._id === messageObject.author.id) return true; }, processMessage: async function(messageObject) { if (!this.messages[messageObject.category._id]) this.$set(this.messages, messageObject.category._id, []); const categoryMessageList = this.messages[messageObject.category._id]; const lastMessageIndex = categoryMessageList.length-1; if (this.shouldMergeMessage(messageObject, categoryMessageList)) { categoryMessageList[lastMessageIndex].content += `\n${messageObject.content}`; } else { this.messages[messageObject.category._id].push(messageObject); } if (messageObject.category._id === this.selection.category._id) { this.$nextTick(() => { // TODO: When the user presses back, actually undo this scroll cause its annoying to scroll back up in the category list const container = this.$el.querySelector('#posts-container'); container.scrollTop = container.scrollHeight; }); } if (messageObject.author.username !== this.loggedInUser.username && messageObject.category._id !== this.selection.category._id) { this.okNotification(`${messageObject.category.title}/${messageObject.author.username}: ${messageObject.content}`); } if (messageObject.author.username !== this.loggedInUser.username) { if (Notification.permission === 'granted') { try { new Notification(`${messageObject.category.title}/${messageObject.author.username}`, { body: messageObject.content }); } catch(e) { console.log('[E] [chat] Failed to show notification'); } } } }, processUserListUpdate: async function(e) { const { category, clientList } = e; if (!this.userLists[category._id]) this.$set(this.userLists, category._id, []); this.userLists[category._id] = clientList; }, openChatForCategory: async function(categoryId) { this.gateway.subscribeToCategoryChat(categoryId); this.selection.category.isChatContext = true; this.selection.category.browsing = true; this.selection.category.title = 'Chat'; this.selection.category._id = categoryId; }, sendCurrentMessage: async function() { const status = await this.gateway.sendMessage(this.selection.category._id, this.message.typed); if (status === 1) { this.okNotification('Failed to send message!'); return; } this.message.typed = ''; }, // Debug toggleDebugDialog: async function() { this.dialog.show.debug = !this.dialog.show.debug; }, debugDump: async function() { console.log('[DEBUG DUMP] [gateway]', this.gateway); console.log('[DEBUG DUMP] [loggedInUser] (this contains sensitive information about the current logged in user, do not leak it to other people lol)', this.loggedInUser); }, // Category and post browsing browseCategories: async function() { const res = await fetch(`${window.location.origin}/api/v1/content/category/list?count=50`, { method: 'GET', headers: { 'Accept': 'application/json', }, credentials: 'include' }); if (res.ok) { const json = await res.json(); this.selection.category.title = 'categories'; this.selection.category.browsing = true; this.selection.category.isCategory = false; this.selection.category.isChatContext = false; this.selection.category._id = '__CATEGORY_LIST'; this.selection.posts = []; this.cardButtons = []; this.button('chat', (post) => { if (post._id) { this.openChatForCategory(post._id); } }); this.button('topic', (post) => { this.browse(post); }); for (let i = 0; i < json.categories.length; i++) { const v = json.categories[i]; this.selection.posts.push({ title: v.title, body: '', _id: v._id, creator: v.creator }); } } else { this.okNotification('Failed to fetch category list'); } }, browse: async function(category) { const { _id, title } = category; const res = await fetch(`${window.location.origin}/api/v1/content/category/${_id}/info`, { method: 'GET', headers: { 'Accept': 'application/json', }, credentials: 'include' }); if (res.ok) { const json = await res.json(); this.selection.category.title = title; this.selection.category._id = _id; this.selection.category.browsing = true; this.selection.category.isCategory = true; this.selection.category.isChatContext = false; this.selection.posts = []; this.cardButtons = []; for (let i = 0; i < json.category.posts.length; i++) { const v = json.category.posts[i]; this.selection.posts.push({ title: v.title, body: v.body, _id: v._id, creator: v.creator }); } } else { this.okNotification('Failed to fetch category'); } }, refresh: function() { if (this.selection.category.title === 'categories' && this.selection.category.isCategory === false) { this.browseCategories(); } else { this.browse(this.selection.category); } }, button: function(text, click) { this.cardButtons.push({ text, click }); }, stopBrowsing: function() { this.selection.category = { title: '', browsing: false, _id: undefined, isCategory: false, isChatContext: false }; this.selection.posts = []; this.cardButtons = []; }, viewProfile: async function(id) { // TODO: this just returns the username for now const res = await fetch(`${window.location.origin}/api/v1/users/user/${id}/info`, { method: 'GET', headers: { 'Accept': 'application/json', }, credentials: 'include' }); if (res.ok) { const json = await res.json(); this.viewingProfile.username = json.user.username; this.viewingProfile._id = json.user._id; this.viewingProfile.role = json.user.role; this.viewingProfile.show = true; } else { this.okNotification('Failed to fetch user data'); } }, // Content creation showCreatePostDialog: function() { if (!this.selection.category.isCategory) { this.okNotification('You are not in a category'); return; } this.dialog.show.createPost = true; }, createPost: async function() { if (!this.selection.category.isCategory) { this.okNotification('You are not in a category'); return; } const category = this.selection.category; const input = this.dialog.text.createPost; const res = await fetch(`${window.location.origin}/api/v1/content/post/create`, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ category: category._id, title: input.title, body: input.body }) }); if (res.ok) { this.okNotification('Successfully created post'); this.dialog.show.createPost = false; this.browse(this.selection.category); return; } else { if (res.status === 401 || res.status === 403) { this.okNotification('You are not allowed to do that'); return; } if (res.status === 429) { this.okNotification('Chill! You are posting too much!'); return; } const json = await res.json(); this.okNotification(getCreatePostError(json)); return; } }, createCategory: async function() { const input = this.dialog.text.createCategory; const res = await fetch(`${window.location.origin}/api/v1/content/category/create`, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ title: input.title }) }); if (res.ok) { this.okNotification('Successfully created category'); this.dialog.show.createCategory = false; this.browseCategories(); return; } else { if (res.status === 401 || res.status === 403) { this.okNotification('You are not allowed to do that'); return; } if (res.status === 429) { this.okNotification('Chill! You are posting too much!'); return; } const json = await res.json(); this.okNotification(getCreateCategoryError(json)); return; } }, // Navigation navigateToAccountManager() { window.location.href = `${window.location.origin}/auth.html`; }, // Snackbar snackbarButtonAction: function() { this.showSnackbarNotification = false; }, snackbarButtonClick: function() { this.snackbarButtonAction(); }, snackbarEditButton: function(buttonText="Ok", action) { this.snackbarButtonText = buttonText; this.snackbarButtonAction = action; }, resetSnackbarButton: function() { this.snackbarButtonText = 'Ok'; this.snackbarButtonAction = () => { this.showSnackbarNotification = false; }; }, notification: function(text) { this.snackbarNotification = text; this.showSnackbarNotification = true; }, okNotification: function(text) { this.resetSnackbarButton(); this.notification(text); } } });