migreate chat to proper sql tables with relations
This commit is contained in:
parent
493b0106ac
commit
482cfd3fe3
3
API.md
3
API.md
|
@ -17,8 +17,9 @@ All requests are made as JSON encoded array.
|
|||
|
||||
All chat messages, except the once you send with `chat` or `mcchat`, will be sent to you in the form:
|
||||
|
||||
```["msg", name, message, country, channelId]```
|
||||
```["msg", name, message, id, country, channelId]```
|
||||
channelId is an integer, channel 0 is `en` channel 1 is `int` and maybe more to come.
|
||||
id is the user id
|
||||
country is the [two-letter country code](https://www.nationsonline.org/oneworld/country_code_list.htm) in lowercase
|
||||
### Subscribe to online user counter
|
||||
```["sub", "online"]```
|
||||
|
|
7022
package-lock.json
generated
7022
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
108
package.json
108
package.json
|
@ -42,30 +42,30 @@
|
|||
"hammerjs": "^2.0.8",
|
||||
"http-proxy-agent": "^4.0.1",
|
||||
"image-q": "^2.1.2",
|
||||
"ip-address": "^6.3.0",
|
||||
"ip-address": "^6.4.0",
|
||||
"isomorphic-fetch": "^2.2.1",
|
||||
"js-file-download": "^0.4.12",
|
||||
"keycode": "^2.1.9",
|
||||
"localforage": "^1.5.0",
|
||||
"localforage": "^1.9.0",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.1",
|
||||
"mysql2": "^2.1.0",
|
||||
"node-sass": "^4.14.0",
|
||||
"nodemailer": "^6.4.6",
|
||||
"mysql2": "^2.2.5",
|
||||
"node-sass": "^4.14.1",
|
||||
"nodemailer": "^6.4.11",
|
||||
"passport": "^0.4.0",
|
||||
"passport-discord": "^0.1.2",
|
||||
"passport-discord": "^0.1.4",
|
||||
"passport-facebook": "^3.0.0",
|
||||
"passport-google-oauth": "^2.0.0",
|
||||
"passport-json": "^1.2.0",
|
||||
"passport-reddit": "^0.2.4",
|
||||
"passport-vkontakte": "^0.3.2",
|
||||
"passport-vkontakte": "^0.3.3",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-icons": "^3.10.0",
|
||||
"react-icons": "^3.11.0",
|
||||
"react-modal": "^3.11.2",
|
||||
"react-redux": "^7.2.0",
|
||||
"react-responsive": "^8.0.3",
|
||||
"react-stay-scrolled": "^7.0.0",
|
||||
"react-redux": "^7.2.1",
|
||||
"react-responsive": "^8.1.0",
|
||||
"react-stay-scrolled": "^7.1.0",
|
||||
"react-toggle-button": "^2.1.0",
|
||||
"redis": "^3.0.2",
|
||||
"redlock": "^4.0.0",
|
||||
|
@ -74,69 +74,69 @@
|
|||
"redux-persist": "^6.0.0",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"sequelize": "^5.21.7",
|
||||
"sharp": "^0.25.2",
|
||||
"sharp": "^0.26.1",
|
||||
"startaudiocontext": "^1.2.1",
|
||||
"sweetalert2": "^9.10.12",
|
||||
"three": "^0.116.0",
|
||||
"three-trackballcontrols-ts": "^0.2.1",
|
||||
"three": "^0.120.1",
|
||||
"three-trackballcontrols-ts": "^0.2.2",
|
||||
"url-search-params-polyfill": "^8.1.0",
|
||||
"winston": "^3.2.1",
|
||||
"winston-daily-rotate-file": "^4.4.2",
|
||||
"ws": "^7.2.5"
|
||||
"winston": "^3.3.3",
|
||||
"winston-daily-rotate-file": "^4.5.0",
|
||||
"ws": "^7.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.9.6",
|
||||
"@babel/node": "^7.8.7",
|
||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||
"@babel/plugin-proposal-decorators": "^7.8.3",
|
||||
"@babel/plugin-proposal-do-expressions": "^7.8.3",
|
||||
"@babel/plugin-proposal-export-default-from": "^7.8.3",
|
||||
"@babel/plugin-proposal-export-namespace-from": "^7.8.3",
|
||||
"@babel/plugin-proposal-function-bind": "^7.8.3",
|
||||
"@babel/plugin-proposal-function-sent": "^7.8.3",
|
||||
"@babel/plugin-proposal-json-strings": "^7.8.3",
|
||||
"@babel/plugin-proposal-logical-assignment-operators": "^7.8.3",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.8.3",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.9.6",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.9.0",
|
||||
"@babel/plugin-proposal-pipeline-operator": "^7.8.3",
|
||||
"@babel/plugin-proposal-throw-expressions": "^7.8.3",
|
||||
"@babel/core": "^7.11.6",
|
||||
"@babel/node": "^7.10.5",
|
||||
"@babel/plugin-proposal-class-properties": "^7.10.4",
|
||||
"@babel/plugin-proposal-decorators": "^7.10.5",
|
||||
"@babel/plugin-proposal-do-expressions": "^7.10.4",
|
||||
"@babel/plugin-proposal-export-default-from": "^7.10.4",
|
||||
"@babel/plugin-proposal-export-namespace-from": "^7.10.4",
|
||||
"@babel/plugin-proposal-function-bind": "^7.11.5",
|
||||
"@babel/plugin-proposal-function-sent": "^7.10.4",
|
||||
"@babel/plugin-proposal-json-strings": "^7.10.4",
|
||||
"@babel/plugin-proposal-logical-assignment-operators": "^7.11.0",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.10.4",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.11.0",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.11.0",
|
||||
"@babel/plugin-proposal-pipeline-operator": "^7.10.5",
|
||||
"@babel/plugin-proposal-throw-expressions": "^7.10.4",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-syntax-import-meta": "^7.8.3",
|
||||
"@babel/plugin-transform-flow-strip-types": "^7.9.0",
|
||||
"@babel/plugin-transform-react-constant-elements": "^7.9.0",
|
||||
"@babel/plugin-transform-react-inline-elements": "^7.9.0",
|
||||
"@babel/polyfill": "^7.8.7",
|
||||
"@babel/preset-env": "^7.9.6",
|
||||
"@babel/preset-flow": "^7.9.0",
|
||||
"@babel/preset-react": "^7.9.4",
|
||||
"@babel/preset-typescript": "^7.9.0",
|
||||
"clean-css": "^4.2.3",
|
||||
"@babel/plugin-syntax-import-meta": "^7.10.4",
|
||||
"@babel/plugin-transform-flow-strip-types": "^7.10.4",
|
||||
"@babel/plugin-transform-react-constant-elements": "^7.10.4",
|
||||
"@babel/plugin-transform-react-inline-elements": "^7.10.4",
|
||||
"@babel/polyfill": "^7.11.5",
|
||||
"@babel/preset-env": "^7.11.5",
|
||||
"@babel/preset-flow": "^7.10.4",
|
||||
"@babel/preset-react": "^7.10.4",
|
||||
"@babel/preset-typescript": "^7.10.4",
|
||||
"assets-webpack-plugin": "^3.9.12",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-loader": "^8.1.0",
|
||||
"babel-plugin-transform-react-pure-class-to-function": "^1.0.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
||||
"clean-css": "^4.2.3",
|
||||
"css-loader": "^3.5.3",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-airbnb": "^18.1.0",
|
||||
"eslint-config-airbnb-base": "^14.1.0",
|
||||
"eslint-config-airbnb": "^18.2.0",
|
||||
"eslint-config-airbnb-base": "^14.2.0",
|
||||
"eslint-plugin-flowtype": "^4.7.0",
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"flow-bin": "^0.123.0",
|
||||
"generate-package-json-webpack-plugin": "^1.0.1",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.3.1",
|
||||
"eslint-plugin-react": "^7.21.2",
|
||||
"flow-bin": "^0.134.0",
|
||||
"generate-package-json-webpack-plugin": "^1.1.2",
|
||||
"json-loader": "^0.5.4",
|
||||
"npm-check": "^5.9.2",
|
||||
"react-hot-loader": "^4.12.21",
|
||||
"react-hot-loader": "^4.13.0",
|
||||
"react-svg-loader": "^3.0.3",
|
||||
"rimraf": "^3.0.2",
|
||||
"sass-loader": "^8.0.2",
|
||||
"style-loader": "^1.2.1",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-bundle-analyzer": "^3.7.0",
|
||||
"webpack": "^4.44.2",
|
||||
"webpack-bundle-analyzer": "^3.9.0",
|
||||
"webpack-dev-middleware": "^3.7.1",
|
||||
"webpack-hot-middleware": "^2.18.0",
|
||||
"write-file-webpack-plugin": "^4.0.2"
|
||||
|
|
|
@ -211,13 +211,6 @@ export function receiveChatMessage(
|
|||
};
|
||||
}
|
||||
|
||||
export function receiveChatHistory(data: Array): Action {
|
||||
return {
|
||||
type: 'RECEIVE_CHAT_HISTORY',
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
let lastNotify = null;
|
||||
export function notify(notification: string) {
|
||||
return async (dispatch) => {
|
||||
|
@ -233,7 +226,6 @@ export function notify(notification: string) {
|
|||
}
|
||||
|
||||
let pixelTimeout = null;
|
||||
|
||||
export function tryPlacePixel(
|
||||
i: number,
|
||||
j: number,
|
||||
|
@ -314,7 +306,7 @@ export function receivePixelReturn(
|
|||
msg = 'You can not access this canvas yet. You need to place more pixels';
|
||||
break;
|
||||
case 8:
|
||||
errorTitle = 'Oww noo';
|
||||
errorTitle = 'nope';
|
||||
msg = 'This pixel is protected.';
|
||||
break;
|
||||
case 9:
|
||||
|
@ -498,6 +490,7 @@ export function receiveMe(
|
|||
dailyRanking,
|
||||
minecraftname,
|
||||
canvases,
|
||||
channels,
|
||||
userlvl,
|
||||
} = me;
|
||||
return {
|
||||
|
@ -511,6 +504,7 @@ export function receiveMe(
|
|||
dailyRanking,
|
||||
minecraftname,
|
||||
canvases,
|
||||
channels,
|
||||
userlvl,
|
||||
};
|
||||
}
|
||||
|
@ -575,7 +569,7 @@ export function fetchStats(): PromiseAction {
|
|||
|
||||
export function fetchMe(): PromiseAction {
|
||||
return async (dispatch) => {
|
||||
const response = await fetch('/api/me', {
|
||||
const response = await fetch('api/me', {
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
|
@ -586,6 +580,43 @@ export function fetchMe(): PromiseAction {
|
|||
};
|
||||
}
|
||||
|
||||
function receiveChatHistory(
|
||||
cid: number,
|
||||
history: Array,
|
||||
) {
|
||||
return {
|
||||
type: 'RECEIVE_CHAT_HISTORY',
|
||||
cid,
|
||||
history,
|
||||
};
|
||||
}
|
||||
|
||||
function setChatFetching(fetching: boolean): Action {
|
||||
return {
|
||||
type: 'SET_CHAT_FETCHING',
|
||||
fetching,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchChatMessages(
|
||||
cid: number,
|
||||
): PromiseAction {
|
||||
return async (dispatch) => {
|
||||
dispatch(setChatFetching(true));
|
||||
const response = await fetch(`api/chathistory?cid=${cid}&limit=50`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setTimeout(() => { dispatch(setChatFetching(false)); }, 500);
|
||||
const { history } = await response.json();
|
||||
dispatch(receiveChatHistory(cid, history));
|
||||
} else {
|
||||
setTimeout(() => { dispatch(setChatFetching(false)); }, 5000);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function setCoolDown(coolDown): Action {
|
||||
return {
|
||||
type: 'COOLDOWN_SET',
|
||||
|
|
|
@ -67,8 +67,9 @@ export type Action =
|
|||
channel: number,
|
||||
isPing: boolean,
|
||||
}
|
||||
| { type: 'RECEIVE_CHAT_HISTORY', data: Array }
|
||||
| { type: 'RECEIVE_CHAT_HISTORY', cid: number, history: Array }
|
||||
| { type: 'SET_CHAT_CHANNEL', channelId: number }
|
||||
| { type: 'SET_CHAT_FETCHING', fetching: boolean }
|
||||
| { type: 'RECEIVE_ME',
|
||||
name: string,
|
||||
waitSeconds: number,
|
||||
|
@ -80,6 +81,7 @@ export type Action =
|
|||
dailyRanking: number,
|
||||
minecraftname: string,
|
||||
canvases: Object,
|
||||
channels: Object,
|
||||
userlvl: number,
|
||||
}
|
||||
| { type: 'RECEIVE_STATS', totalRanking: Object, totalDailyRanking: Object }
|
||||
|
|
|
@ -16,7 +16,6 @@ import {
|
|||
receiveOnline,
|
||||
receiveCoolDown,
|
||||
receiveChatMessage,
|
||||
receiveChatHistory,
|
||||
receivePixelReturn,
|
||||
setMobile,
|
||||
tryPlacePixel,
|
||||
|
@ -57,9 +56,6 @@ function init() {
|
|||
const isPing = (nameRegExp && text.match(nameRegExp));
|
||||
store.dispatch(receiveChatMessage(name, text, country, channelId, isPing));
|
||||
});
|
||||
ProtocolClient.on('chatHistory', (data) => {
|
||||
store.dispatch(receiveChatHistory(data));
|
||||
});
|
||||
ProtocolClient.on('changedMe', () => {
|
||||
store.dispatch(fetchMe());
|
||||
});
|
||||
|
|
|
@ -423,7 +423,7 @@ function Admintools({
|
|||
))}
|
||||
</select>
|
||||
<br />
|
||||
<textarea rows="10" cols="100" id="iparea" /><br />
|
||||
<textarea rows="10" cols="17" id="iparea" /><br />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
|
|
|
@ -11,9 +11,13 @@ import { connect } from 'react-redux';
|
|||
|
||||
import type { State } from '../reducers';
|
||||
import ChatMessage from './ChatMessage';
|
||||
import { MAX_CHAT_MESSAGES } from '../core/constants';
|
||||
|
||||
import { showUserAreaModal, setChatChannel } from '../actions';
|
||||
import { MAX_CHAT_MESSAGES, CHAT_CHANNELS } from '../core/constants';
|
||||
import {
|
||||
showUserAreaModal,
|
||||
setChatChannel,
|
||||
fetchChatMessages,
|
||||
} from '../actions';
|
||||
import ProtocolClient from '../socket/ProtocolClient';
|
||||
import { saveSelection, restoreSelection } from '../utils/storeSelection';
|
||||
import splitChatMessage from '../core/chatMessageFilter';
|
||||
|
@ -23,23 +27,30 @@ function escapeRegExp(string) {
|
|||
}
|
||||
|
||||
const Chat = ({
|
||||
chatMessages,
|
||||
channels,
|
||||
messages,
|
||||
chatChannel,
|
||||
ownName,
|
||||
open,
|
||||
setChannel,
|
||||
fetchMessages,
|
||||
fetching,
|
||||
}) => {
|
||||
const listRef = useRef();
|
||||
const inputRef = useRef();
|
||||
const [inputMessage, setInputMessage] = useState('');
|
||||
const [selection, setSelection] = useState(null);
|
||||
const [nameRegExp, setNameRegExp] = useState(null);
|
||||
|
||||
const { stayScrolled } = useStayScrolled(listRef, {
|
||||
initialScroll: Infinity,
|
||||
inaccuracy: 10,
|
||||
});
|
||||
|
||||
const channelMessages = chatMessages[chatChannel];
|
||||
const channelMessages = messages[chatChannel] || [];
|
||||
if (!messages[chatChannel] && !fetching) {
|
||||
fetchMessages(chatChannel);
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
stayScrolled();
|
||||
|
@ -77,6 +88,22 @@ const Chat = ({
|
|||
setInputMessage('');
|
||||
}
|
||||
|
||||
/*
|
||||
* if selected channel isn't in channel list anymore
|
||||
* for whatever reason (left faction etc.)
|
||||
* set channel to first available one
|
||||
*/
|
||||
let i = 0;
|
||||
while (i < channels.length) {
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (channels[i][0] == chatChannel) {
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
if (i && i === channels.length) {
|
||||
setChannel(channels[0][0]);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
|
@ -87,6 +114,17 @@ const Chat = ({
|
|||
onMouseUp={() => { setSelection(saveSelection); }}
|
||||
role="presentation"
|
||||
>
|
||||
{
|
||||
(!channelMessages.length)
|
||||
&& (
|
||||
<ChatMessage
|
||||
name="info"
|
||||
msgArray={splitChatMessage('Start chatting here', nameRegExp)}
|
||||
country="xx"
|
||||
insertText={(txt) => padToInputMessage(txt)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
channelMessages.map((message) => (
|
||||
<ChatMessage
|
||||
|
@ -121,11 +159,20 @@ const Chat = ({
|
|||
</button>
|
||||
<select
|
||||
style={{ flexGrow: 0 }}
|
||||
onChange={(evt) => setChannel(evt.target.selectedIndex)}
|
||||
onChange={(evt) => {
|
||||
const sel = evt.target;
|
||||
setChannel(sel.options[sel.selectedIndex].value);
|
||||
}}
|
||||
>
|
||||
{
|
||||
CHAT_CHANNELS.map((ch) => (
|
||||
<option selected={ch === chatChannel}>{ch}</option>
|
||||
channels.map((ch) => (
|
||||
<option
|
||||
// eslint-disable-next-line eqeqeq
|
||||
selected={ch[0] == chatChannel}
|
||||
value={ch[0]}
|
||||
>
|
||||
{ch[1]}
|
||||
</option>
|
||||
))
|
||||
}
|
||||
</select>
|
||||
|
@ -147,9 +194,16 @@ const Chat = ({
|
|||
};
|
||||
|
||||
function mapStateToProps(state: State) {
|
||||
const { chatMessages, name } = state.user;
|
||||
const { name } = state.user;
|
||||
const { chatChannel } = state.gui;
|
||||
return { chatMessages, chatChannel, ownName: name };
|
||||
const { channels, messages, fetching } = state.chat;
|
||||
return {
|
||||
channels,
|
||||
messages,
|
||||
fetching,
|
||||
chatChannel,
|
||||
ownName: name,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
|
@ -160,6 +214,9 @@ function mapDispatchToProps(dispatch) {
|
|||
setChannel(channelId) {
|
||||
dispatch(setChatChannel(channelId));
|
||||
},
|
||||
fetchMessages(channelId) {
|
||||
dispatch(fetchChatMessages(channelId));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
113
src/core/ChatMessageBuffer.js
Normal file
113
src/core/ChatMessageBuffer.js
Normal file
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* Buffer for chatMessages for the server
|
||||
* it just buffers the msot recent 200 messages for each channel
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
import logger from './logger';
|
||||
|
||||
import { RegUser, Message } from '../data/models';
|
||||
|
||||
const MAX_BUFFER_TIME = 120000;
|
||||
|
||||
class ChatMessageBuffer {
|
||||
constructor() {
|
||||
this.buffer = new Map();
|
||||
this.timestamps = new Map();
|
||||
|
||||
this.cleanBuffer = this.cleanBuffer.bind(this);
|
||||
this.cleanLoop = setInterval(this.cleanBuffer, 3 * 60 * 1000);
|
||||
}
|
||||
|
||||
async getMessages(cid, limit = 30) {
|
||||
if (limit > 200) {
|
||||
return ChatMessageBuffer.getMessagesFromDatabase(cid, limit);
|
||||
}
|
||||
|
||||
let messages = this.buffer.get(cid);
|
||||
if (!messages) {
|
||||
messages = await ChatMessageBuffer.getMessagesFromDatabase(cid);
|
||||
this.buffer.set(cid, messages);
|
||||
}
|
||||
this.timestamps.set(cid, Date.now());
|
||||
return messages.slice(-limit);
|
||||
}
|
||||
|
||||
cleanBuffer() {
|
||||
const curTime = Date.now();
|
||||
const toDelete = [];
|
||||
this.timestamps.forEach((cid, timestamp) => {
|
||||
if (curTime > timestamp + MAX_BUFFER_TIME) {
|
||||
toDelete.push(cid);
|
||||
}
|
||||
});
|
||||
toDelete.forEach((cid) => {
|
||||
this.buffer.delete(cid);
|
||||
this.timestamps.delete(cid);
|
||||
});
|
||||
logger.info(
|
||||
`Cleaned ${toDelete.length} channels from chat message buffer`,
|
||||
);
|
||||
}
|
||||
|
||||
async addMessage(
|
||||
name,
|
||||
message,
|
||||
cid,
|
||||
uid,
|
||||
flag,
|
||||
) {
|
||||
Message.create({
|
||||
cid,
|
||||
uid,
|
||||
message,
|
||||
});
|
||||
const messages = this.buffer.get(cid);
|
||||
if (messages) {
|
||||
messages.push([
|
||||
name,
|
||||
message,
|
||||
flag,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
static async getMessagesFromDatabase(cid, limit = 200) {
|
||||
const messagesModel = await Message.findAll({
|
||||
include: [
|
||||
{
|
||||
model: RegUser,
|
||||
as: 'user',
|
||||
foreignKey: 'uid',
|
||||
attributes: [
|
||||
'name',
|
||||
'flag',
|
||||
],
|
||||
},
|
||||
],
|
||||
attributes: [
|
||||
'message',
|
||||
],
|
||||
where: { cid },
|
||||
limit,
|
||||
order: ['createdAt'],
|
||||
raw: true,
|
||||
});
|
||||
const messages = [];
|
||||
messagesModel.forEach((msg) => {
|
||||
const {
|
||||
message,
|
||||
'user.name': name,
|
||||
'user.flag': flag,
|
||||
} = msg;
|
||||
messages.push([
|
||||
name,
|
||||
message,
|
||||
flag,
|
||||
]);
|
||||
});
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
|
||||
export default ChatMessageBuffer;
|
|
@ -1,25 +1,19 @@
|
|||
/* @flow */
|
||||
|
||||
import Sequelize from 'sequelize';
|
||||
|
||||
import logger from './logger';
|
||||
import redis from '../data/redis';
|
||||
import User from '../data/models/User';
|
||||
import webSockets from '../socket/websockets';
|
||||
import { Channel } from '../data/models';
|
||||
import ChatMessageBuffer from './ChatMessageBuffer';
|
||||
|
||||
import { CHAT_CHANNELS } from './constants';
|
||||
|
||||
export class ChatProvider {
|
||||
/*
|
||||
* TODO:
|
||||
* history really be saved in redis
|
||||
*/
|
||||
history: Array;
|
||||
|
||||
constructor() {
|
||||
this.history = [];
|
||||
for (let i = 0; i < CHAT_CHANNELS.length; i += 1) {
|
||||
this.history.push([]);
|
||||
}
|
||||
this.defaultChannels = [];
|
||||
this.defaultChannelIds = [];
|
||||
this.caseCheck = /^[A-Z !.]*$/;
|
||||
this.cyrillic = new RegExp('[\u0436-\u043B]');
|
||||
this.filters = [
|
||||
|
@ -47,27 +41,69 @@ export class ChatProvider {
|
|||
},
|
||||
];
|
||||
this.mutedCountries = [];
|
||||
this.chatMessageBuffer = new ChatMessageBuffer();
|
||||
}
|
||||
|
||||
addMessage(name, message, country, channelId = 0) {
|
||||
const channelHistory = this.history[channelId];
|
||||
if (channelHistory.length > 20) {
|
||||
channelHistory.shift();
|
||||
async initialize() {
|
||||
// find or create default channels
|
||||
this.defaultChannels.length = 0;
|
||||
this.defaultChannelIds.length = 0;
|
||||
for (let i = 0; i < CHAT_CHANNELS.length; i += 1) {
|
||||
const { name } = CHAT_CHANNELS[i];
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const channel = await Channel.findOrCreate({
|
||||
attributes: [
|
||||
'id',
|
||||
'lastMessage',
|
||||
],
|
||||
where: { name },
|
||||
defaults: {
|
||||
name,
|
||||
lastMessage: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
},
|
||||
raw: true,
|
||||
});
|
||||
const { id } = channel[0];
|
||||
this.defaultChannels.push([
|
||||
id,
|
||||
name,
|
||||
]);
|
||||
this.defaultChannelIds.push(id);
|
||||
}
|
||||
channelHistory.push([name, message, country]);
|
||||
}
|
||||
|
||||
userHasChannelAccess(user, cid, write = false) {
|
||||
if (this.defaultChannelIds.includes(cid)) {
|
||||
if (!write || user.regUser) {
|
||||
return true;
|
||||
}
|
||||
} else if (user.regUser && user.channelIds.includes(cid)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
getHistory(cid, limit = 30) {
|
||||
return this.chatMessageBuffer.getMessages(cid, limit);
|
||||
}
|
||||
|
||||
async sendMessage(user, message, channelId: number = 0) {
|
||||
const name = (user.regUser) ? user.regUser.name : null;
|
||||
const country = user.country || 'xx';
|
||||
const { id } = user;
|
||||
const name = user.getName();
|
||||
if (!name || !id) {
|
||||
// eslint-disable-next-line max-len
|
||||
return 'Couldn\'t send your message, pls log out and back in again.';
|
||||
}
|
||||
|
||||
if (!this.userHasChannelAccess(user, channelId)) {
|
||||
return 'You don\'t have access to this channel';
|
||||
}
|
||||
|
||||
const country = user.regUser.flag || 'xx';
|
||||
let displayCountry = (name.endsWith('berg') || name.endsWith('stein'))
|
||||
? 'il'
|
||||
: country;
|
||||
|
||||
if (!name) {
|
||||
// eslint-disable-next-line max-len
|
||||
return 'Couldn\'t send your message, pls log out and back in again.';
|
||||
}
|
||||
if (!user.regUser.verified) {
|
||||
return 'Your mail has to be verified in order to chat';
|
||||
}
|
||||
|
@ -166,23 +202,49 @@ export class ChatProvider {
|
|||
return 'Your country is temporary muted from chat';
|
||||
}
|
||||
|
||||
this.addMessage(name, message, displayCountry, channelId);
|
||||
webSockets.broadcastChatMessage(name, message, displayCountry, channelId);
|
||||
this.chatMessageBuffer.addMessage(
|
||||
name,
|
||||
message,
|
||||
channelId,
|
||||
id,
|
||||
displayCountry,
|
||||
);
|
||||
webSockets.broadcastChatMessage(
|
||||
name,
|
||||
message,
|
||||
channelId,
|
||||
id,
|
||||
displayCountry,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
broadcastChatMessage(
|
||||
name,
|
||||
message,
|
||||
channelId: number = 1,
|
||||
id = -1,
|
||||
country: string = 'xx',
|
||||
channelId: number = 0,
|
||||
sendapi: boolean = true,
|
||||
) {
|
||||
if (message.length > 250) {
|
||||
return;
|
||||
}
|
||||
this.addMessage(name, message, country, channelId);
|
||||
webSockets.broadcastChatMessage(name, message, country, channelId, sendapi);
|
||||
this.chatMessageBuffer.addMessage(
|
||||
name,
|
||||
message,
|
||||
channelId,
|
||||
id,
|
||||
country,
|
||||
);
|
||||
webSockets.broadcastChatMessage(
|
||||
name,
|
||||
message,
|
||||
channelId,
|
||||
id,
|
||||
country,
|
||||
sendapi,
|
||||
);
|
||||
}
|
||||
|
||||
static automute(name, channelId = 0) {
|
||||
|
|
|
@ -89,6 +89,13 @@ export const HOUR = 60 * MINUTE;
|
|||
export const DAY = 24 * HOUR;
|
||||
export const MONTH = 30 * DAY;
|
||||
|
||||
// available Chat Channels
|
||||
export const CHAT_CHANNELS = ['en', 'int'];
|
||||
// available public Chat Channels
|
||||
export const CHAT_CHANNELS = [
|
||||
{
|
||||
name: 'en',
|
||||
}, {
|
||||
name: 'int',
|
||||
},
|
||||
];
|
||||
|
||||
export const MAX_CHAT_MESSAGES = 100;
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import canvases from './canvases.json';
|
||||
import chatProvider from './ChatProvider';
|
||||
|
||||
|
||||
export default async function getMe(user) {
|
||||
|
@ -29,7 +30,10 @@ export default async function getMe(user) {
|
|||
delete userdata.mailVerified;
|
||||
delete userdata.mcVerified;
|
||||
|
||||
const channels = [...chatProvider.defaultChannels];
|
||||
|
||||
userdata.canvases = canvases;
|
||||
userdata.channels = channels;
|
||||
|
||||
return userdata;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ import { OAuth2Strategy as GoogleStrategy } from 'passport-google-oauth';
|
|||
import logger from './logger';
|
||||
import { sanitizeName } from '../utils/validation';
|
||||
|
||||
import { User, RegUser } from '../data/models';
|
||||
import { User, RegUser, Channel } from '../data/models';
|
||||
import { auth } from './config';
|
||||
import { compareToHash } from '../utils/hash';
|
||||
import { getIPFromRequest } from '../utils/ip';
|
||||
|
@ -28,13 +28,22 @@ passport.serializeUser((user, done) => {
|
|||
passport.deserializeUser(async (req, id, done) => {
|
||||
const user = new User(id, getIPFromRequest(req));
|
||||
if (id) {
|
||||
RegUser.findOne({ where: { id } }).then((reguser) => {
|
||||
RegUser.findByPk(id, {
|
||||
include: {
|
||||
model: Channel,
|
||||
as: 'channel',
|
||||
},
|
||||
}).then((reguser) => {
|
||||
if (reguser) {
|
||||
user.regUser = reguser;
|
||||
user.id = id;
|
||||
for (let i = 0; i < reguser.channel.length; i += 1) {
|
||||
user.channelIds.push(reguser.channel[i].id);
|
||||
}
|
||||
} else {
|
||||
user.id = null;
|
||||
}
|
||||
|
||||
done(null, user);
|
||||
});
|
||||
} else {
|
||||
|
|
33
src/data/models/Channel.js
Normal file
33
src/data/models/Channel.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
*
|
||||
* Database layout for Chat Channels
|
||||
*
|
||||
* @flow
|
||||
*
|
||||
*/
|
||||
|
||||
import DataType from 'sequelize';
|
||||
import Model from '../sequelize';
|
||||
|
||||
const Channel = Model.define('Channel', {
|
||||
// Channel ID
|
||||
id: {
|
||||
type: DataType.INTEGER.UNSIGNED,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
|
||||
name: {
|
||||
type: `${DataType.CHAR(32)} CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci`,
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
lastMessage: {
|
||||
type: DataType.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
}, {
|
||||
updatedAt: false,
|
||||
});
|
||||
|
||||
export default Channel;
|
40
src/data/models/Message.js
Normal file
40
src/data/models/Message.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
*
|
||||
* Database layout for Chat Message History
|
||||
*
|
||||
* @flow
|
||||
*
|
||||
*/
|
||||
|
||||
import DataType from 'sequelize';
|
||||
import Model from '../sequelize';
|
||||
import Channel from './Channel';
|
||||
import RegUser from './RegUser';
|
||||
|
||||
const Message = Model.define('Message', {
|
||||
// Message ID
|
||||
id: {
|
||||
type: DataType.INTEGER.UNSIGNED,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
|
||||
message: {
|
||||
type: `${DataType.CHAR(200)} CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci`,
|
||||
allowNull: false,
|
||||
},
|
||||
}, {
|
||||
updatedAt: false,
|
||||
});
|
||||
|
||||
Message.belongsTo(Channel, {
|
||||
as: 'channel',
|
||||
foreignKey: 'cid',
|
||||
});
|
||||
|
||||
Message.belongsTo(RegUser, {
|
||||
as: 'user',
|
||||
foreignKey: 'uid',
|
||||
});
|
||||
|
||||
export default Message;
|
|
@ -25,7 +25,7 @@ const RegUser = Model.define('User', {
|
|||
},
|
||||
|
||||
name: {
|
||||
type: DataType.CHAR(32),
|
||||
type: `${DataType.CHAR(32)} CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci`,
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
|
@ -91,6 +91,13 @@ const RegUser = Model.define('User', {
|
|||
allowNull: true,
|
||||
},
|
||||
|
||||
// flag == country code
|
||||
flag: {
|
||||
type: DataType.CHAR(2),
|
||||
defaultValue: 'xx',
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
lastLogIn: {
|
||||
type: DataType.DATE,
|
||||
allowNull: true,
|
||||
|
|
|
@ -25,12 +25,14 @@ class User {
|
|||
regUser: Object;
|
||||
|
||||
constructor(id: string = null, ip: string = '127.0.0.1') {
|
||||
// id should stay null if unregistered, and user email if registered
|
||||
// id should stay null if unregistered
|
||||
this.id = id;
|
||||
this.ip = ip;
|
||||
this.ipSub = getIPv6Subnet(ip);
|
||||
this.wait = null;
|
||||
// following gets populated by passport
|
||||
this.regUser = null;
|
||||
this.channelIds = [];
|
||||
}
|
||||
|
||||
static async name2Id(name: string) {
|
||||
|
@ -48,6 +50,10 @@ class User {
|
|||
}
|
||||
}
|
||||
|
||||
getName() {
|
||||
return (this.regUser) ? this.regUser.name : null;
|
||||
}
|
||||
|
||||
async setWait(coolDown: number, canvasId: number): Promise<boolean> {
|
||||
if (!coolDown) return false;
|
||||
this.wait = Date.now() + coolDown;
|
||||
|
@ -115,6 +121,14 @@ class User {
|
|||
}
|
||||
}
|
||||
|
||||
async setCountry(country) {
|
||||
if (this.regUser && this.regUser.flag !== country) {
|
||||
this.regUser.update({
|
||||
flag: country,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async updateLogInTimestamp(): Promise<boolean> {
|
||||
if (!this.regUser) return false;
|
||||
try {
|
||||
|
|
23
src/data/models/UserChannel.js
Normal file
23
src/data/models/UserChannel.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
*
|
||||
* Junction table for User -> Channels
|
||||
* A channel can be anything,
|
||||
* Group, Public Chat, DM, etc.
|
||||
*
|
||||
* @flow
|
||||
*
|
||||
*/
|
||||
|
||||
import DataType from 'sequelize';
|
||||
import Model from '../sequelize';
|
||||
|
||||
const UserChannel = Model.define('UserChannel', {
|
||||
lastRead: {
|
||||
type: DataType.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
}, {
|
||||
timestamps: false,
|
||||
});
|
||||
|
||||
export default UserChannel;
|
|
@ -5,13 +5,32 @@ import Blacklist from './Blacklist';
|
|||
import Whitelist from './Whitelist';
|
||||
import User from './User';
|
||||
import RegUser from './RegUser';
|
||||
import Channel from './Channel';
|
||||
import UserChannel from './UserChannel';
|
||||
import Message from './Message';
|
||||
|
||||
RegUser.belongsToMany(Channel, {
|
||||
as: 'channel',
|
||||
through: UserChannel,
|
||||
});
|
||||
Channel.belongsToMany(RegUser, {
|
||||
as: 'user',
|
||||
through: UserChannel,
|
||||
});
|
||||
|
||||
function sync(...args) {
|
||||
return sequelize.sync(...args);
|
||||
}
|
||||
|
||||
export default { sync };
|
||||
/*
|
||||
* makes sure that minimum required rows are present
|
||||
*
|
||||
*/
|
||||
function validateTables() {
|
||||
|
||||
}
|
||||
|
||||
export default { sync, validateTables };
|
||||
export {
|
||||
Whitelist, Blacklist, User, RegUser,
|
||||
Whitelist, Blacklist, User, RegUser, Channel, UserChannel, Message,
|
||||
};
|
||||
|
|
79
src/reducers/chat.js
Normal file
79
src/reducers/chat.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
/* @flow */
|
||||
|
||||
import { MAX_CHAT_MESSAGES } from '../core/constants';
|
||||
|
||||
import type { Action } from '../actions/types';
|
||||
|
||||
export type ChatState = {
|
||||
// [[cid, name], [cid2, name2],...]
|
||||
channels: Array,
|
||||
// { cid: [message1,message2,message2,...]}
|
||||
messages: Object,
|
||||
// if currently fetching messages
|
||||
fetching: boolean,
|
||||
}
|
||||
|
||||
const initialState: ChatState = {
|
||||
channels: [],
|
||||
messages: {},
|
||||
fetching: false,
|
||||
};
|
||||
|
||||
export default function chat(
|
||||
state: ChatState = initialState,
|
||||
action: Action,
|
||||
): ChatState {
|
||||
switch (action.type) {
|
||||
case 'RECEIVE_ME': {
|
||||
return {
|
||||
...state,
|
||||
channels: action.channels,
|
||||
};
|
||||
}
|
||||
|
||||
case 'SET_CHAT_FETCHING': {
|
||||
const { fetching } = action;
|
||||
return {
|
||||
...state,
|
||||
fetching,
|
||||
};
|
||||
}
|
||||
|
||||
case 'RECEIVE_CHAT_MESSAGE': {
|
||||
const {
|
||||
name, text, country, channel,
|
||||
} = action;
|
||||
if (!state.messages[channel]) {
|
||||
return state;
|
||||
}
|
||||
const messages = {
|
||||
...state.messages,
|
||||
[channel]: [
|
||||
...state.messages[channel],
|
||||
[name, text, country],
|
||||
],
|
||||
};
|
||||
if (messages[channel].length > MAX_CHAT_MESSAGES) {
|
||||
messages[channel].shift();
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
messages,
|
||||
};
|
||||
}
|
||||
|
||||
case 'RECEIVE_CHAT_HISTORY': {
|
||||
const { cid, history } = action;
|
||||
return {
|
||||
...state,
|
||||
messages: {
|
||||
...state.messages,
|
||||
[cid]: history,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -30,7 +30,7 @@ const initialState: GUIState = {
|
|||
compactPalette: false,
|
||||
paletteOpen: true,
|
||||
menuOpen: false,
|
||||
chatChannel: 0,
|
||||
chatChannel: 1,
|
||||
style: 'default',
|
||||
};
|
||||
|
||||
|
|
|
@ -7,12 +7,14 @@ import canvas from './canvas';
|
|||
import gui from './gui';
|
||||
import modal from './modal';
|
||||
import user from './user';
|
||||
import chat from './chat';
|
||||
|
||||
import type { AudioState } from './audio';
|
||||
import type { CanvasState } from './canvas';
|
||||
import type { GUIState } from './gui';
|
||||
import type { ModalState } from './modal';
|
||||
import type { UserState } from './user';
|
||||
import type { ChatState } from './chat';
|
||||
|
||||
export type State = {
|
||||
audio: AudioState,
|
||||
|
@ -20,12 +22,13 @@ export type State = {
|
|||
gui: GUIState,
|
||||
modal: ModalState,
|
||||
user: UserState,
|
||||
chat: ChatState,
|
||||
};
|
||||
|
||||
const config = {
|
||||
key: 'primary',
|
||||
storage: localForage,
|
||||
blacklist: ['user', 'canvas', 'modal'],
|
||||
blacklist: ['user', 'canvas', 'modal', 'chat'],
|
||||
};
|
||||
|
||||
export default persistCombineReducers(config, {
|
||||
|
@ -34,4 +37,5 @@ export default persistCombineReducers(config, {
|
|||
gui,
|
||||
modal,
|
||||
user,
|
||||
chat,
|
||||
});
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
/* @flow */
|
||||
|
||||
import { MAX_CHAT_MESSAGES } from '../core/constants';
|
||||
|
||||
import type { Action } from '../actions/types';
|
||||
|
||||
|
||||
|
@ -24,8 +22,6 @@ export type UserState = {
|
|||
// global stats
|
||||
totalRanking: Object,
|
||||
totalDailyRanking: Object,
|
||||
// chat
|
||||
chatMessages: Array,
|
||||
// minecraft
|
||||
minecraftname: string,
|
||||
// if user is using touchscreen
|
||||
|
@ -48,10 +44,6 @@ const initialState: UserState = {
|
|||
mailreg: false,
|
||||
totalRanking: {},
|
||||
totalDailyRanking: {},
|
||||
chatMessages: [
|
||||
[['info', 'Welcome to the PixelPlanet Chat', 'il']],
|
||||
[['info', 'Welcome to the PixelPlanet Chat', 'il']],
|
||||
],
|
||||
minecraftname: null,
|
||||
isOnMobile: false,
|
||||
notification: null,
|
||||
|
@ -138,33 +130,6 @@ export default function user(
|
|||
};
|
||||
}
|
||||
|
||||
case 'RECEIVE_CHAT_MESSAGE': {
|
||||
const {
|
||||
name, text, country, channel,
|
||||
} = action;
|
||||
const chatMessages = state.chatMessages.slice();
|
||||
let channelMessages = chatMessages[channel];
|
||||
if (channelMessages.length > MAX_CHAT_MESSAGES) {
|
||||
channelMessages = channelMessages.slice(-50);
|
||||
}
|
||||
channelMessages = channelMessages.concat([
|
||||
[name, text, country],
|
||||
]);
|
||||
chatMessages[channel] = channelMessages;
|
||||
return {
|
||||
...state,
|
||||
chatMessages,
|
||||
};
|
||||
}
|
||||
|
||||
case 'RECEIVE_CHAT_HISTORY': {
|
||||
const { data: chatMessages } = action;
|
||||
return {
|
||||
...state,
|
||||
chatMessages,
|
||||
};
|
||||
}
|
||||
|
||||
case 'RECEIVE_ME': {
|
||||
const {
|
||||
name,
|
||||
|
|
57
src/routes/api/chathistory.js
Normal file
57
src/routes/api/chathistory.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
*
|
||||
* returns chat messages of given channel
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
|
||||
import chatProvider from '../../core/ChatProvider';
|
||||
|
||||
async function chatHistory(req: Request, res: Response) {
|
||||
let { cid, limit } = req.query;
|
||||
if (!cid || !limit) {
|
||||
res.status(400);
|
||||
res.json({
|
||||
errors: ['cid or limit not defined'],
|
||||
});
|
||||
return;
|
||||
}
|
||||
cid = parseInt(cid, 10);
|
||||
limit = parseInt(limit, 10);
|
||||
if (Number.isNaN(cid) || Number.isNaN(limit)
|
||||
|| limit <= 0 || limit > 300) {
|
||||
res.status(400);
|
||||
res.json({
|
||||
errors: ['cid or limit not a valid value'],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { user } = req;
|
||||
|
||||
if (!chatProvider.userHasChannelAccess(user, cid, false)) {
|
||||
res.status(401);
|
||||
res.json({
|
||||
errors: ['You don\'t have access to this channel'],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// try {
|
||||
const history = await chatProvider.getHistory(cid, limit);
|
||||
res.json({
|
||||
history,
|
||||
});
|
||||
/*
|
||||
} catch {
|
||||
res.status(500);
|
||||
res.json({
|
||||
errors: ['Can not fetch messages'],
|
||||
});
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
export default chatHistory;
|
|
@ -17,6 +17,7 @@ import captcha from './captcha';
|
|||
import auth from './auth';
|
||||
import ranking from './ranking';
|
||||
import history from './history';
|
||||
import chatHistory from './chathistory';
|
||||
|
||||
|
||||
const router = express.Router();
|
||||
|
@ -77,6 +78,8 @@ router.get('/me', me);
|
|||
|
||||
router.post('/mctp', mctp);
|
||||
|
||||
router.get('/chathistory', chatHistory);
|
||||
|
||||
router.use('/auth', auth(passport));
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -18,7 +18,7 @@ const CANVAS_MIN_XY = -CANVAS_MAX_XY;
|
|||
|
||||
export default async (req: Request, res: Response) => {
|
||||
const { user } = req;
|
||||
if (!user) {
|
||||
if (!user.regUser) {
|
||||
res.status(401);
|
||||
res.json({
|
||||
success: false,
|
||||
|
|
|
@ -86,8 +86,9 @@ class APISocketServer extends WebSocketEvents {
|
|||
broadcastChatMessage(
|
||||
name,
|
||||
msg,
|
||||
country,
|
||||
channelId,
|
||||
id,
|
||||
country,
|
||||
sendapi,
|
||||
ws = null,
|
||||
) {
|
||||
|
@ -249,14 +250,44 @@ class APISocketServer extends WebSocketEvents {
|
|||
const chatname = (user.id)
|
||||
? `[MC] ${user.regUser.name}`
|
||||
: `[MC] ${minecraftname}`;
|
||||
chatProvider.broadcastChatMessage(chatname, msg, 'xx', 0, false);
|
||||
this.broadcastChatMessage(chatname, msg, 'xx', 0, true, ws);
|
||||
chatProvider.broadcastChatMessage(
|
||||
chatname,
|
||||
msg,
|
||||
0,
|
||||
-1,
|
||||
'xx',
|
||||
false,
|
||||
);
|
||||
this.broadcastChatMessage(
|
||||
chatname,
|
||||
msg,
|
||||
0,
|
||||
-1,
|
||||
'xx',
|
||||
true,
|
||||
ws,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (command === 'chat') {
|
||||
const [name, msg, country, channelId] = packet;
|
||||
chatProvider.broadcastChatMessage(name, msg, country, channelId, false);
|
||||
this.broadcastChatMessage(name, msg, country, channelId, true, ws);
|
||||
chatProvider.broadcastChatMessage(
|
||||
name,
|
||||
msg,
|
||||
channelId,
|
||||
-1,
|
||||
country,
|
||||
false,
|
||||
);
|
||||
this.broadcastChatMessage(
|
||||
name,
|
||||
msg,
|
||||
channelId,
|
||||
-1,
|
||||
country,
|
||||
true,
|
||||
ws,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (command === 'linkacc') {
|
||||
|
|
|
@ -16,7 +16,6 @@ import RegisterCanvas from './packets/RegisterCanvas';
|
|||
import RegisterChunk from './packets/RegisterChunk';
|
||||
import RegisterMultipleChunks from './packets/RegisterMultipleChunks';
|
||||
import DeRegisterChunk from './packets/DeRegisterChunk';
|
||||
import RequestChatHistory from './packets/RequestChatHistory';
|
||||
import ChangedMe from './packets/ChangedMe';
|
||||
|
||||
const chunks = [];
|
||||
|
@ -26,6 +25,7 @@ class ProtocolClient extends EventEmitter {
|
|||
ws: WebSocket;
|
||||
name: string;
|
||||
canvasId: number;
|
||||
channelId: number;
|
||||
timeConnected: number;
|
||||
isConnected: number;
|
||||
isConnecting: boolean;
|
||||
|
@ -39,6 +39,7 @@ class ProtocolClient extends EventEmitter {
|
|||
this.ws = null;
|
||||
this.name = null;
|
||||
this.canvasId = '0';
|
||||
this.channelId = 0;
|
||||
this.msgQueue = [];
|
||||
}
|
||||
|
||||
|
@ -82,7 +83,6 @@ class ProtocolClient extends EventEmitter {
|
|||
this.isConnecting = false;
|
||||
this.isConnected = true;
|
||||
this.emit('open', {});
|
||||
this.requestChatHistory();
|
||||
if (this.canvasId !== null) {
|
||||
this.ws.send(RegisterCanvas.dehydrate(this.canvasId));
|
||||
}
|
||||
|
@ -143,11 +143,6 @@ class ProtocolClient extends EventEmitter {
|
|||
this.sendWhenReady(buffer);
|
||||
}
|
||||
|
||||
requestChatHistory() {
|
||||
const buffer = RequestChatHistory.dehydrate();
|
||||
if (this.isConnected) this.ws.send(buffer);
|
||||
}
|
||||
|
||||
sendChatMessage(message, channelId) {
|
||||
if (this.isConnected) {
|
||||
this.ws.send(JSON.stringify([message, channelId]));
|
||||
|
@ -174,11 +169,6 @@ class ProtocolClient extends EventEmitter {
|
|||
const data = JSON.parse(message);
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
if (Array.isArray(data[0])) {
|
||||
// Array in Array: Chat History
|
||||
this.emit('chatHistory', data);
|
||||
return;
|
||||
}
|
||||
if (data.length === 4) {
|
||||
// Ordinary array: Chat message
|
||||
const [name, text, country, channelId] = data;
|
||||
|
|
|
@ -16,7 +16,6 @@ import RegisterChunk from './packets/RegisterChunk';
|
|||
import RegisterMultipleChunks from './packets/RegisterMultipleChunks';
|
||||
import DeRegisterChunk from './packets/DeRegisterChunk';
|
||||
import DeRegisterMultipleChunks from './packets/DeRegisterMultipleChunks';
|
||||
import RequestChatHistory from './packets/RequestChatHistory';
|
||||
import ChangedMe from './packets/ChangedMe';
|
||||
import OnlineCounter from './packets/OnlineCounter';
|
||||
|
||||
|
@ -85,7 +84,7 @@ class SocketServer extends WebSocketEvents {
|
|||
ws.on('pong', heartbeat);
|
||||
const user = await authenticateClient(req);
|
||||
ws.user = user;
|
||||
ws.name = (user.regUser) ? user.regUser.name : null;
|
||||
ws.name = user.getName();
|
||||
ws.rateLimiter = new RateLimiter(20, 15, true);
|
||||
cheapDetector(user.ip);
|
||||
|
||||
|
@ -166,13 +165,16 @@ class SocketServer extends WebSocketEvents {
|
|||
broadcastChatMessage(
|
||||
name: string,
|
||||
message: string,
|
||||
channelId: number,
|
||||
id: number,
|
||||
country: string,
|
||||
channelId: number = 0,
|
||||
) {
|
||||
const text = JSON.stringify([name, message, country, channelId]);
|
||||
this.wss.clients.forEach((ws) => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(text);
|
||||
if (chatProvider.userHasChannelAccess(ws.user, channelId)) {
|
||||
ws.send(text);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -199,7 +201,9 @@ class SocketServer extends WebSocketEvents {
|
|||
// eslint-disable-next-line no-underscore-dangle
|
||||
client._socket.write(buffer);
|
||||
} catch (error) {
|
||||
logger.error('(!) Catched error on write socket:', error);
|
||||
logger.error(
|
||||
`WebSocket broadcast pixelbuffer error: ${error.message}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -313,8 +317,10 @@ class SocketServer extends WebSocketEvents {
|
|||
} else {
|
||||
logger.info('Got empty message or message from unidentified ws');
|
||||
}
|
||||
} catch {
|
||||
logger.info('Got invalid ws text message');
|
||||
} catch (error) {
|
||||
logger.error('Got invalid ws text message');
|
||||
logger.error(error.message);
|
||||
logger.error(error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -397,11 +403,6 @@ class SocketServer extends WebSocketEvents {
|
|||
}
|
||||
break;
|
||||
}
|
||||
case RequestChatHistory.OP_CODE: {
|
||||
const history = JSON.stringify(chatProvider.history);
|
||||
ws.send(history);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
/* @flow */
|
||||
|
||||
const OP_CODE = 0xA5;
|
||||
|
||||
export default {
|
||||
OP_CODE,
|
||||
dehydrate(): ArrayBuffer {
|
||||
const buffer = new ArrayBuffer(1);
|
||||
const view = new DataView(buffer);
|
||||
view.setInt8(0, OP_CODE);
|
||||
return buffer;
|
||||
},
|
||||
};
|
|
@ -22,9 +22,10 @@ function authenticateClient(req) {
|
|||
((resolve) => {
|
||||
router(req, {}, async () => {
|
||||
const country = req.headers['cf-ipcountry'] || 'xx';
|
||||
const countryCode = country.toLowerCase();
|
||||
const user = (req.user) ? req.user
|
||||
: new User(null, getIPFromRequest(req));
|
||||
user.country = country.toLowerCase();
|
||||
user.setCountry(countryCode);
|
||||
resolve(user);
|
||||
});
|
||||
}),
|
||||
|
|
|
@ -71,8 +71,9 @@ class WebSockets {
|
|||
broadcastChatMessage(
|
||||
name: string,
|
||||
message: string,
|
||||
country: string,
|
||||
channelId: number = 0,
|
||||
channelId: number = 1,
|
||||
id: number = -1,
|
||||
country: string = 'xx',
|
||||
sendapi: boolean = true,
|
||||
) {
|
||||
country = country || 'xx';
|
||||
|
@ -80,8 +81,9 @@ class WebSockets {
|
|||
(listener) => listener.broadcastChatMessage(
|
||||
name,
|
||||
message,
|
||||
country,
|
||||
channelId,
|
||||
id,
|
||||
country,
|
||||
sendapi,
|
||||
),
|
||||
);
|
||||
|
|
|
@ -223,7 +223,7 @@ class ChunkLoader {
|
|||
this.store.dispatch(requestBigChunk(center));
|
||||
chunkRGB.isBasechunk = true;
|
||||
try {
|
||||
const url = `/chunks/${this.canvasId}/${cx}/${cy}.bmp`;
|
||||
const url = `chunks/${this.canvasId}/${cx}/${cy}.bmp`;
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
|
@ -247,7 +247,7 @@ class ChunkLoader {
|
|||
const center = [zoom, cx, cy];
|
||||
this.store.dispatch(requestBigChunk(center));
|
||||
try {
|
||||
const url = `/tiles/${this.canvasId}/${zoom}/${cx}/${cy}.png`;
|
||||
const url = `tiles/${this.canvasId}/${zoom}/${cx}/${cy}.png`;
|
||||
const img = await loadImage(url);
|
||||
chunkRGB.fromImage(img);
|
||||
this.store.dispatch(receiveBigChunk(center));
|
||||
|
|
|
@ -99,7 +99,7 @@ class ChunkLoader {
|
|||
const center = [0, cx, cz];
|
||||
this.store.dispatch(requestBigChunk(center));
|
||||
try {
|
||||
const url = `/chunks/${this.canvasId}/${cx}/${cz}.bmp`;
|
||||
const url = `chunks/${this.canvasId}/${cx}/${cz}.bmp`;
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
|
|
|
@ -14,6 +14,7 @@ import assets from './assets.json'; // eslint-disable-line import/no-unresolved
|
|||
import logger from './core/logger';
|
||||
import rankings from './core/ranking';
|
||||
import models from './data/models';
|
||||
import chatProvider from './core/ChatProvider';
|
||||
|
||||
import SocketServer from './socket/SocketServer';
|
||||
import APISocketServer from './socket/APISocketServer';
|
||||
|
@ -192,10 +193,14 @@ app.get('/', async (req, res) => {
|
|||
//
|
||||
// ip config
|
||||
// -----------------------------------------------------------------------------
|
||||
const promise = models.sync().catch((err) => logger.error(err.stack));
|
||||
// use this if models changed:
|
||||
const promise = models.sync({ alter: { drop: false } })
|
||||
// const promise = models.sync()
|
||||
.catch((err) => logger.error(err.stack));
|
||||
promise.then(() => {
|
||||
server.listen(PORT, () => {
|
||||
rankings.updateRanking();
|
||||
chatProvider.initialize();
|
||||
const address = server.address();
|
||||
logger.log('info', `web is running at http://localhost:${address.port}/`);
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue
Block a user