553 lines
No EOL
20 KiB
JavaScript
Executable file
553 lines
No EOL
20 KiB
JavaScript
Executable file
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();
|
|
});
|
|
});
|
|
},
|
|
processMessage: async function(messageObject) {
|
|
if (!this.messages[messageObject.category._id]) this.$set(this.messages, messageObject.category._id, []);
|
|
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);
|
|
}
|
|
}
|
|
}); |