2020-05-09 17:58:13 +00:00
|
|
|
import Chance from 'chance'
|
2020-05-25 21:22:27 +00:00
|
|
|
import CodeMirror from 'codemirror'
|
2020-05-09 17:58:13 +00:00
|
|
|
import cookie from 'cookie'
|
|
|
|
import cookieParser from 'cookie-parser'
|
|
|
|
import moment from 'moment'
|
|
|
|
import randomcolor from 'randomcolor'
|
2020-05-25 21:22:27 +00:00
|
|
|
import SocketIO, { Socket } from 'socket.io'
|
2020-05-09 17:58:13 +00:00
|
|
|
import { config } from './config'
|
2020-04-11 13:47:59 +00:00
|
|
|
|
2020-04-12 15:41:18 +00:00
|
|
|
import { History } from './history'
|
2020-04-12 11:11:06 +00:00
|
|
|
import { logger } from './logger'
|
2020-05-09 17:58:13 +00:00
|
|
|
import { Author, Note, Revision, User } from './models'
|
2020-05-24 20:18:44 +00:00
|
|
|
import { NoteAuthorship } from './models/note'
|
2020-06-12 20:10:57 +00:00
|
|
|
import { PhotoProfile } from './utils/PhotoProfile'
|
2020-05-25 21:22:27 +00:00
|
|
|
import { EditorSocketIOServer } from './ot/editor-socketio-server'
|
2020-06-12 20:15:04 +00:00
|
|
|
import { mapToObject } from './utils/functions'
|
2020-06-23 10:43:24 +00:00
|
|
|
import { getPermission, Permission } from './web/note/util'
|
2020-04-11 13:47:59 +00:00
|
|
|
|
2020-05-09 17:58:13 +00:00
|
|
|
export type SocketWithNoteId = Socket & { noteId: string }
|
|
|
|
|
2020-04-11 13:47:59 +00:00
|
|
|
const chance = new Chance()
|
|
|
|
|
Improve handling of termination signals
Previously, upon receiving a termination signal, the process tries to
flush all changes to the database, retrying every 0.1s until it
succeeds. However, if the database is not set up properly, this always
fails, and spams the terminal/logging with the error message 10 times a
second.
If the user sends another termination signal, the handleTermSignal
function is called once again, and we get twice the number of error
messages.
This commit changes the behaviour in various ways.
(1) It lowers the retry rate to 0.5s, and aborts after 30 seconds.
(2) If the write to the database errored, the error message explains
that this is due to us flushing the final changes.
(3) We replace realtime.maintenance with realtime.state, which is an
Enum with three possible states --- Starting, Running, and Stopping.
If a termination signal is received in the starting state, the
process simply aborts because there is nothing to clean up. This is
the case when the database is misconfigured, since the application
starts up only after connecting to the databse. If it is in the
Stopping state, the handleTermSignal function returns because
another instance of handleTermSignal is already running.
Fixes #408
Signed-off-by: Dexter Chua <dec41@srcf.net>
2020-06-27 03:06:48 +00:00
|
|
|
export enum State {
|
|
|
|
Starting,
|
|
|
|
Running,
|
|
|
|
Stopping
|
|
|
|
}
|
2020-04-11 13:47:59 +00:00
|
|
|
/* eslint-disable @typescript-eslint/no-use-before-define */
|
2020-05-25 21:22:27 +00:00
|
|
|
const realtime: {
|
|
|
|
onAuthorizeSuccess: (data, accept) => void;
|
|
|
|
onAuthorizeFail: (data, message, error, accept) => void;
|
|
|
|
io: SocketIO.Server; isReady: () => boolean;
|
|
|
|
connection: (socket: SocketWithNoteId) => void;
|
|
|
|
secure: (socket: SocketIO.Socket, next: (err?: Error) => void) => void;
|
Improve handling of termination signals
Previously, upon receiving a termination signal, the process tries to
flush all changes to the database, retrying every 0.1s until it
succeeds. However, if the database is not set up properly, this always
fails, and spams the terminal/logging with the error message 10 times a
second.
If the user sends another termination signal, the handleTermSignal
function is called once again, and we get twice the number of error
messages.
This commit changes the behaviour in various ways.
(1) It lowers the retry rate to 0.5s, and aborts after 30 seconds.
(2) If the write to the database errored, the error message explains
that this is due to us flushing the final changes.
(3) We replace realtime.maintenance with realtime.state, which is an
Enum with three possible states --- Starting, Running, and Stopping.
If a termination signal is received in the starting state, the
process simply aborts because there is nothing to clean up. This is
the case when the database is misconfigured, since the application
starts up only after connecting to the databse. If it is in the
Stopping state, the handleTermSignal function returns because
another instance of handleTermSignal is already running.
Fixes #408
Signed-off-by: Dexter Chua <dec41@srcf.net>
2020-06-27 03:06:48 +00:00
|
|
|
getStatus: (callback) => void; state: State;
|
2020-05-25 21:22:27 +00:00
|
|
|
} = {
|
|
|
|
io: SocketIO(),
|
2017-03-08 10:45:51 +00:00
|
|
|
onAuthorizeSuccess: onAuthorizeSuccess,
|
|
|
|
onAuthorizeFail: onAuthorizeFail,
|
|
|
|
secure: secure,
|
|
|
|
connection: connection,
|
|
|
|
getStatus: getStatus,
|
2017-04-12 17:57:55 +00:00
|
|
|
isReady: isReady,
|
Improve handling of termination signals
Previously, upon receiving a termination signal, the process tries to
flush all changes to the database, retrying every 0.1s until it
succeeds. However, if the database is not set up properly, this always
fails, and spams the terminal/logging with the error message 10 times a
second.
If the user sends another termination signal, the handleTermSignal
function is called once again, and we get twice the number of error
messages.
This commit changes the behaviour in various ways.
(1) It lowers the retry rate to 0.5s, and aborts after 30 seconds.
(2) If the write to the database errored, the error message explains
that this is due to us flushing the final changes.
(3) We replace realtime.maintenance with realtime.state, which is an
Enum with three possible states --- Starting, Running, and Stopping.
If a termination signal is received in the starting state, the
process simply aborts because there is nothing to clean up. This is
the case when the database is misconfigured, since the application
starts up only after connecting to the databse. If it is in the
Stopping state, the handleTermSignal function returns because
another instance of handleTermSignal is already running.
Fixes #408
Signed-off-by: Dexter Chua <dec41@srcf.net>
2020-06-27 03:06:48 +00:00
|
|
|
state: State.Starting
|
2015-06-01 10:04:25 +00:00
|
|
|
}
|
2020-04-11 13:47:59 +00:00
|
|
|
/* eslint-enable @typescript-eslint/no-use-before-define */
|
2015-06-01 10:04:25 +00:00
|
|
|
|
2020-05-09 17:58:13 +00:00
|
|
|
const disconnectSocketQueue: SocketWithNoteId[] = []
|
2020-04-11 13:47:59 +00:00
|
|
|
|
2020-05-09 17:58:13 +00:00
|
|
|
function onAuthorizeSuccess (data, accept): void {
|
2017-03-08 10:45:51 +00:00
|
|
|
accept()
|
2015-06-01 10:04:25 +00:00
|
|
|
}
|
|
|
|
|
2020-05-09 17:58:13 +00:00
|
|
|
function onAuthorizeFail (data, message, error, accept): void {
|
2017-03-08 10:45:51 +00:00
|
|
|
accept() // accept whether authorize or not to allow anonymous usage
|
|
|
|
}
|
|
|
|
|
|
|
|
// secure the origin by the cookie
|
2020-05-24 16:41:15 +00:00
|
|
|
function secure (socket: Socket, next: (err?: Error) => void): void {
|
2017-03-08 10:45:51 +00:00
|
|
|
try {
|
2020-04-11 13:47:59 +00:00
|
|
|
const handshakeData = socket.request
|
2017-03-08 10:45:51 +00:00
|
|
|
if (handshakeData.headers.cookie) {
|
|
|
|
handshakeData.cookie = cookie.parse(handshakeData.headers.cookie)
|
2018-03-07 14:17:35 +00:00
|
|
|
handshakeData.sessionID = cookieParser.signedCookie(handshakeData.cookie[config.sessionName], config.sessionSecret)
|
2017-03-08 10:45:51 +00:00
|
|
|
if (handshakeData.sessionID &&
|
2020-04-11 09:27:54 +00:00
|
|
|
handshakeData.cookie[config.sessionName] &&
|
|
|
|
handshakeData.cookie[config.sessionName] !== handshakeData.sessionID) {
|
2019-06-08 18:51:24 +00:00
|
|
|
logger.debug(`AUTH success cookie: ${handshakeData.sessionID}`)
|
2017-03-08 10:45:51 +00:00
|
|
|
return next()
|
|
|
|
} else {
|
|
|
|
next(new Error('AUTH failed: Cookie is invalid.'))
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
next(new Error('AUTH failed: No cookie transmitted.'))
|
2015-05-04 07:53:29 +00:00
|
|
|
}
|
2017-03-08 10:45:51 +00:00
|
|
|
} catch (ex) {
|
|
|
|
next(new Error('AUTH failed:' + JSON.stringify(ex)))
|
|
|
|
}
|
2015-05-04 07:53:29 +00:00
|
|
|
}
|
|
|
|
|
2020-05-24 20:18:44 +00:00
|
|
|
function emitCheck (note: NoteSession): void {
|
2020-04-11 13:47:59 +00:00
|
|
|
const out = {
|
2017-03-08 10:45:51 +00:00
|
|
|
title: note.title,
|
|
|
|
updatetime: note.updatetime,
|
|
|
|
lastchangeuser: note.lastchangeuser,
|
|
|
|
lastchangeuserprofile: note.lastchangeuserprofile,
|
2020-05-31 19:43:14 +00:00
|
|
|
authors: mapToObject(note.authors),
|
2017-03-08 10:45:51 +00:00
|
|
|
authorship: note.authorship
|
|
|
|
}
|
|
|
|
realtime.io.to(note.id).emit('check', out)
|
2015-07-11 04:43:08 +00:00
|
|
|
}
|
|
|
|
|
2020-05-24 16:41:15 +00:00
|
|
|
class UserSession {
|
2020-05-24 20:18:44 +00:00
|
|
|
id?: string
|
2020-05-25 21:22:27 +00:00
|
|
|
address?: string
|
|
|
|
login?: boolean
|
2020-05-24 20:18:44 +00:00
|
|
|
userid: string | null
|
2020-05-25 21:22:27 +00:00
|
|
|
'user-agent'?
|
|
|
|
photo: string
|
2020-05-24 20:18:44 +00:00
|
|
|
color: string
|
2020-05-25 21:22:27 +00:00
|
|
|
cursor?: CodeMirror.Position
|
2020-05-24 20:18:44 +00:00
|
|
|
name: string | null
|
2020-05-25 21:22:27 +00:00
|
|
|
idle?: boolean
|
|
|
|
type?: string
|
2020-05-24 16:41:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
class NoteSession {
|
2020-05-25 21:22:27 +00:00
|
|
|
id: string
|
|
|
|
alias: string
|
|
|
|
title: string
|
2020-06-23 10:43:24 +00:00
|
|
|
ownerId: string
|
2020-05-25 21:22:27 +00:00
|
|
|
ownerprofile: PhotoProfile | null
|
|
|
|
permission: string
|
|
|
|
lastchangeuser: string | null
|
|
|
|
lastchangeuserprofile: PhotoProfile | null
|
|
|
|
socks: SocketWithNoteId[]
|
|
|
|
users: Map<string, UserSession>
|
|
|
|
tempUsers: Map<string, number> // time value
|
|
|
|
createtime: number
|
|
|
|
updatetime: number
|
|
|
|
server: EditorSocketIOServer
|
|
|
|
authors: Map<string, UserSession>
|
|
|
|
authorship: NoteAuthorship[]
|
2020-05-24 16:41:15 +00:00
|
|
|
}
|
|
|
|
|
2017-03-08 10:45:51 +00:00
|
|
|
// actions
|
2020-05-24 20:18:44 +00:00
|
|
|
const users: Map<string, UserSession> = new Map<string, UserSession>()
|
|
|
|
const notes: Map<string, NoteSession> = new Map<string, NoteSession>()
|
2020-04-11 13:47:59 +00:00
|
|
|
|
|
|
|
let saverSleep = false
|
|
|
|
|
2020-05-24 16:41:15 +00:00
|
|
|
function finishUpdateNote (note: NoteSession, _note: Note, callback: (err: Error | null, note: Note | null) => void): void {
|
2020-04-11 13:47:59 +00:00
|
|
|
if (!note || !note.server) return callback(null, null)
|
|
|
|
const body = note.server.document
|
|
|
|
const title = note.title = Note.parseNoteTitle(body)
|
|
|
|
const values = {
|
|
|
|
title: title,
|
|
|
|
content: body,
|
|
|
|
authorship: note.authorship,
|
|
|
|
lastchangeuserId: note.lastchangeuser,
|
|
|
|
lastchangeAt: Date.now()
|
|
|
|
}
|
|
|
|
_note.update(values).then(function (_note) {
|
|
|
|
saverSleep = false
|
|
|
|
return callback(null, _note)
|
|
|
|
}).catch(function (err) {
|
|
|
|
logger.error(err)
|
|
|
|
return callback(err, null)
|
2017-03-08 10:45:51 +00:00
|
|
|
})
|
2020-04-11 13:47:59 +00:00
|
|
|
}
|
|
|
|
|
2020-05-24 16:41:15 +00:00
|
|
|
function updateHistory (userId, note: NoteSession, time?): void {
|
2020-04-11 13:47:59 +00:00
|
|
|
const noteId = note.alias ? note.alias : Note.encodeNoteId(note.id)
|
2020-04-12 15:41:18 +00:00
|
|
|
if (note.server) History.updateHistory(userId, noteId, note.server.document, time)
|
2020-04-11 13:47:59 +00:00
|
|
|
}
|
2017-03-08 10:45:51 +00:00
|
|
|
|
2020-05-24 16:41:15 +00:00
|
|
|
function updateNote (note: NoteSession, callback: (err, note) => void): void {
|
2020-04-11 09:27:54 +00:00
|
|
|
Note.findOne({
|
2017-03-08 10:45:51 +00:00
|
|
|
where: {
|
|
|
|
id: note.id
|
|
|
|
}
|
|
|
|
}).then(function (_note) {
|
|
|
|
if (!_note) return callback(null, null)
|
|
|
|
// update user note history
|
2020-05-25 21:22:27 +00:00
|
|
|
const tempUsers = new Map(note.tempUsers)
|
2020-05-24 20:18:44 +00:00
|
|
|
note.tempUsers = new Map<string, number>()
|
|
|
|
for (const [key, time] of tempUsers) {
|
|
|
|
updateHistory(key, note, time)
|
|
|
|
}
|
2017-03-08 10:45:51 +00:00
|
|
|
if (note.lastchangeuser) {
|
|
|
|
if (_note.lastchangeuserId !== note.lastchangeuser) {
|
2020-04-11 09:27:54 +00:00
|
|
|
User.findOne({
|
2017-03-08 10:45:51 +00:00
|
|
|
where: {
|
|
|
|
id: note.lastchangeuser
|
|
|
|
}
|
|
|
|
}).then(function (user) {
|
|
|
|
if (!user) return callback(null, null)
|
2020-06-04 19:25:42 +00:00
|
|
|
note.lastchangeuserprofile = PhotoProfile.fromUser(user)
|
2017-03-08 10:45:51 +00:00
|
|
|
return finishUpdateNote(note, _note, callback)
|
|
|
|
}).catch(function (err) {
|
|
|
|
logger.error(err)
|
|
|
|
return callback(err, null)
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
return finishUpdateNote(note, _note, callback)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
note.lastchangeuserprofile = null
|
|
|
|
return finishUpdateNote(note, _note, callback)
|
|
|
|
}
|
|
|
|
}).catch(function (err) {
|
|
|
|
logger.error(err)
|
|
|
|
return callback(err, null)
|
|
|
|
})
|
2016-11-16 05:58:59 +00:00
|
|
|
}
|
2017-03-08 10:45:51 +00:00
|
|
|
|
2020-04-11 13:47:59 +00:00
|
|
|
// update when the note is dirty
|
2017-03-08 10:45:51 +00:00
|
|
|
setInterval(function () {
|
2020-05-24 20:18:44 +00:00
|
|
|
for (const [key, note] of notes) {
|
2020-04-11 13:47:59 +00:00
|
|
|
if (note.server.isDirty) {
|
|
|
|
logger.debug(`updater found dirty note: ${key}`)
|
|
|
|
note.server.isDirty = false
|
|
|
|
updateNote(note, function (err, _note) {
|
|
|
|
// handle when note already been clean up
|
2020-05-24 20:18:44 +00:00
|
|
|
if (!note || !note.server) return
|
2020-04-11 13:47:59 +00:00
|
|
|
if (!_note) {
|
|
|
|
realtime.io.to(note.id).emit('info', {
|
|
|
|
code: 404
|
|
|
|
})
|
|
|
|
logger.error('note not found: ', note.id)
|
2015-09-24 03:38:55 +00:00
|
|
|
}
|
2020-04-11 13:47:59 +00:00
|
|
|
if (err || !_note) {
|
2020-05-24 20:18:44 +00:00
|
|
|
for (const sock of note.socks) {
|
|
|
|
if (sock) {
|
2020-04-11 13:47:59 +00:00
|
|
|
setTimeout(function () {
|
2020-04-12 18:54:46 +00:00
|
|
|
sock.disconnect()
|
2020-04-11 13:47:59 +00:00
|
|
|
}, 0)
|
|
|
|
}
|
|
|
|
}
|
2020-05-24 20:18:44 +00:00
|
|
|
return logger.error('updater error', err)
|
2020-04-11 13:47:59 +00:00
|
|
|
}
|
|
|
|
note.updatetime = moment(_note.lastchangeAt).valueOf()
|
|
|
|
emitCheck(note)
|
|
|
|
})
|
|
|
|
} else {
|
2020-05-24 20:18:44 +00:00
|
|
|
return
|
2017-03-08 10:45:51 +00:00
|
|
|
}
|
2020-05-24 20:18:44 +00:00
|
|
|
}
|
2020-04-11 13:47:59 +00:00
|
|
|
}, 1000)
|
2017-03-08 10:45:51 +00:00
|
|
|
|
2016-06-17 08:09:33 +00:00
|
|
|
// save note revision in interval
|
2017-03-08 10:45:51 +00:00
|
|
|
setInterval(function () {
|
|
|
|
if (saverSleep) return
|
2020-04-11 09:27:54 +00:00
|
|
|
Revision.saveAllNotesRevision(function (err, notes) {
|
2017-03-08 10:45:51 +00:00
|
|
|
if (err) return logger.error('revision saver failed: ' + err)
|
|
|
|
if (notes && notes.length <= 0) {
|
|
|
|
saverSleep = true
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}, 60000 * 5)
|
|
|
|
|
2020-05-09 17:58:13 +00:00
|
|
|
let isConnectionBusy: boolean
|
|
|
|
let isDisconnectBusy: boolean
|
2020-04-11 13:47:59 +00:00
|
|
|
|
2020-05-09 17:58:13 +00:00
|
|
|
const connectionSocketQueue: SocketWithNoteId[] = []
|
2020-04-11 13:47:59 +00:00
|
|
|
|
2020-05-24 16:41:15 +00:00
|
|
|
function getStatus (callback): void {
|
2020-04-11 09:27:54 +00:00
|
|
|
Note.count().then(function (notecount) {
|
2020-04-11 13:47:59 +00:00
|
|
|
const distinctaddresses: string[] = []
|
|
|
|
const regaddresses: string[] = []
|
|
|
|
const distinctregaddresses: string[] = []
|
2020-05-24 20:18:44 +00:00
|
|
|
for (const user of users.values()) {
|
2017-03-08 10:45:51 +00:00
|
|
|
if (!user) return
|
|
|
|
let found = false
|
2020-05-24 20:18:44 +00:00
|
|
|
for (const distinctaddress of distinctaddresses) {
|
|
|
|
if (user.address === distinctaddress) {
|
2017-03-08 10:45:51 +00:00
|
|
|
found = true
|
|
|
|
break
|
2016-06-17 08:09:33 +00:00
|
|
|
}
|
2017-03-08 10:45:51 +00:00
|
|
|
}
|
|
|
|
if (!found) {
|
2020-05-25 21:22:27 +00:00
|
|
|
if (user.address != null) {
|
|
|
|
distinctaddresses.push(user.address)
|
|
|
|
}
|
2017-03-08 10:45:51 +00:00
|
|
|
}
|
|
|
|
if (user.login) {
|
2020-05-25 21:22:27 +00:00
|
|
|
if (user.address != null) {
|
|
|
|
regaddresses.push(user.address)
|
|
|
|
}
|
2017-03-08 10:45:51 +00:00
|
|
|
let found = false
|
|
|
|
for (let i = 0; i < distinctregaddresses.length; i++) {
|
|
|
|
if (user.address === distinctregaddresses[i]) {
|
|
|
|
found = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!found) {
|
2020-05-25 21:22:27 +00:00
|
|
|
if (user.address != null) {
|
|
|
|
distinctregaddresses.push(user.address)
|
|
|
|
}
|
2017-03-08 10:45:51 +00:00
|
|
|
}
|
|
|
|
}
|
2020-05-24 20:18:44 +00:00
|
|
|
}
|
2020-04-11 09:27:54 +00:00
|
|
|
User.count().then(function (regcount) {
|
2018-11-14 13:10:14 +00:00
|
|
|
// eslint-disable-next-line standard/no-callback-literal
|
2017-03-08 10:45:51 +00:00
|
|
|
return callback ? callback({
|
|
|
|
onlineNotes: Object.keys(notes).length,
|
|
|
|
onlineUsers: Object.keys(users).length,
|
|
|
|
distinctOnlineUsers: distinctaddresses.length,
|
|
|
|
notesCount: notecount,
|
|
|
|
registeredUsers: regcount,
|
|
|
|
onlineRegisteredUsers: regaddresses.length,
|
|
|
|
distinctOnlineRegisteredUsers: distinctregaddresses.length,
|
|
|
|
isConnectionBusy: isConnectionBusy,
|
|
|
|
connectionSocketQueueLength: connectionSocketQueue.length,
|
|
|
|
isDisconnectBusy: isDisconnectBusy,
|
|
|
|
disconnectSocketQueueLength: disconnectSocketQueue.length
|
|
|
|
}) : null
|
2016-04-20 10:03:55 +00:00
|
|
|
}).catch(function (err) {
|
2017-03-08 10:45:51 +00:00
|
|
|
return logger.error('count user failed: ' + err)
|
|
|
|
})
|
|
|
|
}).catch(function (err) {
|
|
|
|
return logger.error('count note failed: ' + err)
|
|
|
|
})
|
2015-05-04 07:53:29 +00:00
|
|
|
}
|
|
|
|
|
2020-05-09 17:58:13 +00:00
|
|
|
function isReady (): boolean {
|
2017-03-08 10:45:51 +00:00
|
|
|
return realtime.io &&
|
|
|
|
Object.keys(notes).length === 0 && Object.keys(users).length === 0 &&
|
|
|
|
connectionSocketQueue.length === 0 && !isConnectionBusy &&
|
|
|
|
disconnectSocketQueue.length === 0 && !isDisconnectBusy
|
2016-06-17 08:09:33 +00:00
|
|
|
}
|
|
|
|
|
2020-05-24 16:41:15 +00:00
|
|
|
function extractNoteIdFromSocket (socket: Socket): string | boolean {
|
2017-09-13 12:48:39 +00:00
|
|
|
if (!socket || !socket.handshake) {
|
2017-03-08 10:45:51 +00:00
|
|
|
return false
|
|
|
|
}
|
2017-09-13 12:48:39 +00:00
|
|
|
if (socket.handshake.query && socket.handshake.query.noteId) {
|
|
|
|
return socket.handshake.query.noteId
|
|
|
|
} else {
|
2017-03-08 10:45:51 +00:00
|
|
|
return false
|
|
|
|
}
|
2016-04-20 10:03:55 +00:00
|
|
|
}
|
|
|
|
|
2020-05-24 16:41:15 +00:00
|
|
|
function parseNoteIdFromSocket (socket: Socket, callback: (err: string | null, noteId: string | null) => void): void {
|
2020-04-11 13:47:59 +00:00
|
|
|
const noteId = extractNoteIdFromSocket(socket)
|
2017-03-08 10:45:51 +00:00
|
|
|
if (!noteId) {
|
|
|
|
return callback(null, null)
|
|
|
|
}
|
2020-04-11 09:27:54 +00:00
|
|
|
Note.parseNoteId(noteId, function (err, id) {
|
2017-03-08 10:45:51 +00:00
|
|
|
if (err || !id) return callback(err, id)
|
|
|
|
return callback(null, id)
|
|
|
|
})
|
2015-05-04 07:53:29 +00:00
|
|
|
}
|
|
|
|
|
2020-05-24 16:41:15 +00:00
|
|
|
function buildUserOutData (user): UserSession {
|
|
|
|
return {
|
2020-04-11 13:47:59 +00:00
|
|
|
id: user.id,
|
|
|
|
login: user.login,
|
|
|
|
userid: user.userid,
|
|
|
|
photo: user.photo,
|
|
|
|
color: user.color,
|
|
|
|
cursor: user.cursor,
|
|
|
|
name: user.name,
|
|
|
|
idle: user.idle,
|
|
|
|
type: user.type
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-09 17:58:13 +00:00
|
|
|
function emitOnlineUsers (socket: SocketWithNoteId): void {
|
2020-04-11 13:47:59 +00:00
|
|
|
const noteId = socket.noteId
|
2020-05-24 20:18:44 +00:00
|
|
|
if (!noteId) return
|
|
|
|
const note = notes.get(noteId)
|
|
|
|
if (!note) return
|
2020-05-24 16:41:15 +00:00
|
|
|
const users: UserSession[] = []
|
2020-05-31 19:38:42 +00:00
|
|
|
for (const user of note.users.values()) {
|
2020-04-11 09:27:54 +00:00
|
|
|
if (user) {
|
|
|
|
users.push(buildUserOutData(user))
|
|
|
|
}
|
2020-05-24 20:18:44 +00:00
|
|
|
}
|
2020-04-11 13:47:59 +00:00
|
|
|
const out = {
|
2017-03-08 10:45:51 +00:00
|
|
|
users: users
|
|
|
|
}
|
|
|
|
realtime.io.to(noteId).emit('online users', out)
|
2015-06-01 10:04:25 +00:00
|
|
|
}
|
|
|
|
|
2020-05-09 17:58:13 +00:00
|
|
|
function emitUserStatus (socket: SocketWithNoteId): void {
|
2020-04-11 13:47:59 +00:00
|
|
|
const noteId = socket.noteId
|
2020-05-24 20:18:44 +00:00
|
|
|
if (!noteId) return
|
|
|
|
const note = notes.get(noteId)
|
|
|
|
if (!note) return
|
|
|
|
const user = users.get(socket.id)
|
|
|
|
if (!user) return
|
2020-04-11 13:47:59 +00:00
|
|
|
const out = buildUserOutData(user)
|
2017-03-08 10:45:51 +00:00
|
|
|
socket.broadcast.to(noteId).emit('user status', out)
|
2015-07-01 16:10:20 +00:00
|
|
|
}
|
|
|
|
|
2020-05-09 17:58:13 +00:00
|
|
|
function emitRefresh (socket: SocketWithNoteId): void {
|
2020-04-11 13:47:59 +00:00
|
|
|
const noteId = socket.noteId
|
2020-05-24 20:18:44 +00:00
|
|
|
if (!noteId) return
|
|
|
|
const note = notes.get(noteId)
|
|
|
|
if (!note) return
|
2020-04-11 13:47:59 +00:00
|
|
|
const out = {
|
2017-03-08 10:45:51 +00:00
|
|
|
title: note.title,
|
2018-03-07 14:17:35 +00:00
|
|
|
docmaxlength: config.documentMaxLength,
|
2020-06-23 10:43:24 +00:00
|
|
|
owner: note.ownerId,
|
2017-03-08 10:45:51 +00:00
|
|
|
ownerprofile: note.ownerprofile,
|
|
|
|
lastchangeuser: note.lastchangeuser,
|
|
|
|
lastchangeuserprofile: note.lastchangeuserprofile,
|
2020-05-31 19:43:14 +00:00
|
|
|
authors: mapToObject(note.authors),
|
2017-03-08 10:45:51 +00:00
|
|
|
authorship: note.authorship,
|
|
|
|
permission: note.permission,
|
|
|
|
createtime: note.createtime,
|
|
|
|
updatetime: note.updatetime
|
|
|
|
}
|
|
|
|
socket.emit('refresh', out)
|
2015-05-04 07:53:29 +00:00
|
|
|
}
|
|
|
|
|
2020-05-09 17:58:13 +00:00
|
|
|
function isDuplicatedInSocketQueue (queue: Socket[], socket: Socket): boolean {
|
2020-05-24 20:18:44 +00:00
|
|
|
for (const sock of queue) {
|
|
|
|
if (sock && sock.id === socket.id) {
|
2017-03-08 10:45:51 +00:00
|
|
|
return true
|
2016-10-10 12:25:48 +00:00
|
|
|
}
|
2017-03-08 10:45:51 +00:00
|
|
|
}
|
|
|
|
return false
|
2016-10-10 12:25:48 +00:00
|
|
|
}
|
|
|
|
|
2020-05-09 17:58:13 +00:00
|
|
|
function clearSocketQueue (queue: Socket[], socket: Socket): void {
|
2020-04-11 13:47:59 +00:00
|
|
|
for (let i = 0; i < queue.length; i++) {
|
2017-03-08 10:45:51 +00:00
|
|
|
if (!queue[i] || queue[i].id === socket.id) {
|
|
|
|
queue.splice(i, 1)
|
|
|
|
i--
|
2015-09-27 03:43:33 +00:00
|
|
|
}
|
2017-03-08 10:45:51 +00:00
|
|
|
}
|
2015-09-27 03:43:33 +00:00
|
|
|
}
|
|
|
|
|
2020-05-09 17:58:13 +00:00
|
|
|
function connectNextSocket (): void {
|
2017-03-08 10:45:51 +00:00
|
|
|
setTimeout(function () {
|
|
|
|
isConnectionBusy = false
|
|
|
|
if (connectionSocketQueue.length > 0) {
|
2020-04-11 13:47:59 +00:00
|
|
|
// Otherwise we get a loop startConnection - failConnection - connectNextSocket
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
2017-03-08 10:45:51 +00:00
|
|
|
startConnection(connectionSocketQueue[0])
|
|
|
|
}
|
|
|
|
}, 1)
|
2016-07-05 08:11:18 +00:00
|
|
|
}
|
|
|
|
|
2020-05-09 17:58:13 +00:00
|
|
|
function failConnection (errorCode: number, errorMessage: string, socket: Socket): void {
|
|
|
|
logger.error(errorMessage)
|
2020-04-11 13:47:59 +00:00
|
|
|
// clear error socket in queue
|
|
|
|
clearSocketQueue(connectionSocketQueue, socket)
|
|
|
|
connectNextSocket()
|
|
|
|
// emit error info
|
|
|
|
socket.emit('info', {
|
2020-05-09 17:58:13 +00:00
|
|
|
code: errorCode
|
2020-04-11 13:47:59 +00:00
|
|
|
})
|
2020-05-09 17:58:13 +00:00
|
|
|
socket.disconnect(true)
|
2020-04-11 13:47:59 +00:00
|
|
|
}
|
|
|
|
|
2020-05-09 17:58:13 +00:00
|
|
|
function interruptConnection (socket: Socket, noteId: string, socketId): void {
|
2020-05-24 20:18:44 +00:00
|
|
|
notes.delete(noteId)
|
|
|
|
users.delete(socketId)
|
2020-04-11 09:27:54 +00:00
|
|
|
if (socket) {
|
|
|
|
clearSocketQueue(connectionSocketQueue, socket)
|
|
|
|
} else {
|
|
|
|
connectionSocketQueue.shift()
|
|
|
|
}
|
2017-03-08 10:45:51 +00:00
|
|
|
connectNextSocket()
|
2016-07-30 03:10:43 +00:00
|
|
|
}
|
|
|
|
|
2020-05-09 17:58:13 +00:00
|
|
|
function finishConnection (socket: SocketWithNoteId, noteId: string, socketId: string): void {
|
2017-03-08 10:45:51 +00:00
|
|
|
// if no valid info provided will drop the client
|
2020-05-24 20:18:44 +00:00
|
|
|
if (!socket || !notes.get(noteId) || !users.get(socketId)) {
|
2017-03-08 10:45:51 +00:00
|
|
|
return interruptConnection(socket, noteId, socketId)
|
|
|
|
}
|
|
|
|
// check view permission
|
2020-05-24 20:18:44 +00:00
|
|
|
const note = notes.get(noteId)
|
|
|
|
if (!note) return
|
2020-06-23 10:43:24 +00:00
|
|
|
if (getPermission(socket.request.user, note) === Permission.None) {
|
2017-03-08 10:45:51 +00:00
|
|
|
interruptConnection(socket, noteId, socketId)
|
|
|
|
return failConnection(403, 'connection forbidden', socket)
|
|
|
|
}
|
2020-05-24 20:18:44 +00:00
|
|
|
const user = users.get(socketId)
|
2020-05-31 19:40:05 +00:00
|
|
|
if (!user) {
|
|
|
|
logger.warn('Could not find user for socketId ' + socketId)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if (user.userid) {
|
|
|
|
// update user color to author color
|
|
|
|
const author = note.authors.get(user.userid)
|
|
|
|
if (author) {
|
|
|
|
const socketIdUser = users.get(socket.id)
|
|
|
|
if (!socketIdUser) return
|
|
|
|
user.color = author.color
|
|
|
|
socketIdUser.color = author.color
|
|
|
|
users.set(socket.id, user)
|
|
|
|
}
|
2017-03-08 10:45:51 +00:00
|
|
|
}
|
2020-05-24 20:18:44 +00:00
|
|
|
note.users.set(socket.id, user)
|
2017-03-08 10:45:51 +00:00
|
|
|
note.socks.push(socket)
|
|
|
|
note.server.addClient(socket)
|
|
|
|
note.server.setName(socket, user.name)
|
|
|
|
note.server.setColor(socket, user.color)
|
|
|
|
|
|
|
|
// update user note history
|
|
|
|
updateHistory(user.userid, note)
|
|
|
|
|
|
|
|
emitOnlineUsers(socket)
|
|
|
|
emitRefresh(socket)
|
|
|
|
|
|
|
|
// clear finished socket in queue
|
|
|
|
clearSocketQueue(connectionSocketQueue, socket)
|
|
|
|
// seek for next socket
|
|
|
|
connectNextSocket()
|
|
|
|
|
|
|
|
if (config.debug) {
|
2020-04-11 13:47:59 +00:00
|
|
|
const noteId = socket.noteId
|
2019-06-08 18:51:24 +00:00
|
|
|
logger.debug(`SERVER connected a client to [${noteId}]:`)
|
|
|
|
logger.debug(JSON.stringify(user))
|
|
|
|
logger.debug(notes)
|
2017-03-08 10:45:51 +00:00
|
|
|
getStatus(function (data) {
|
2019-06-08 18:51:24 +00:00
|
|
|
logger.debug(JSON.stringify(data))
|
2017-03-08 10:45:51 +00:00
|
|
|
})
|
|
|
|
}
|
2015-05-04 07:53:29 +00:00
|
|
|
}
|
|
|
|
|
2020-05-09 17:58:13 +00:00
|
|
|
function ifMayEdit (socket: SocketWithNoteId, originIsOperation: boolean, callback: (mayEdit: boolean) => void): void {
|
2020-04-11 13:47:59 +00:00
|
|
|
const noteId = socket.noteId
|
2020-05-24 20:18:44 +00:00
|
|
|
if (!noteId) return
|
|
|
|
const note = notes.get(noteId)
|
|
|
|
if (!note) return
|
2020-06-23 10:43:24 +00:00
|
|
|
const mayEdit = (getPermission(socket.request.user, note) >= Permission.Write)
|
2020-04-11 13:47:59 +00:00
|
|
|
// if user may edit and this is a text operation
|
2020-05-09 17:58:13 +00:00
|
|
|
if (originIsOperation && mayEdit) {
|
2020-04-11 13:47:59 +00:00
|
|
|
// save for the last change user id
|
|
|
|
if (socket.request.user && socket.request.user.logged_in) {
|
|
|
|
note.lastchangeuser = socket.request.user.id
|
|
|
|
} else {
|
|
|
|
note.lastchangeuser = null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return callback(mayEdit)
|
|
|
|
}
|
|
|
|
|
2020-05-09 17:58:13 +00:00
|
|
|
function operationCallback (socket: SocketWithNoteId, operation): void {
|
2020-04-11 13:47:59 +00:00
|
|
|
const noteId = socket.noteId
|
2020-05-24 20:18:44 +00:00
|
|
|
if (!noteId) return
|
|
|
|
const note = notes.get(noteId)
|
|
|
|
if (!note) return
|
|
|
|
let userId: string | null = null
|
2020-04-11 13:47:59 +00:00
|
|
|
// save authors
|
|
|
|
if (socket.request.user && socket.request.user.logged_in) {
|
2020-05-24 20:18:44 +00:00
|
|
|
const user = users.get(socket.id)
|
2020-04-11 13:47:59 +00:00
|
|
|
if (!user) return
|
|
|
|
userId = socket.request.user.id
|
2020-05-24 20:18:44 +00:00
|
|
|
if (!userId) return
|
|
|
|
const author = note.authors.get(userId)
|
|
|
|
if (!author) {
|
2020-04-11 13:47:59 +00:00
|
|
|
Author.findOrCreate({
|
|
|
|
where: {
|
|
|
|
noteId: noteId,
|
|
|
|
userId: userId
|
|
|
|
},
|
|
|
|
defaults: {
|
|
|
|
noteId: noteId,
|
|
|
|
userId: userId,
|
|
|
|
color: user.color
|
|
|
|
}
|
2020-05-25 21:22:27 +00:00
|
|
|
}).then(function ([author, _]) {
|
2020-04-11 13:47:59 +00:00
|
|
|
if (author) {
|
2020-05-24 20:18:44 +00:00
|
|
|
note.authors.set(author.userId, {
|
2020-04-11 13:47:59 +00:00
|
|
|
userid: author.userId,
|
|
|
|
color: author.color,
|
|
|
|
photo: user.photo,
|
|
|
|
name: user.name
|
2020-05-24 20:18:44 +00:00
|
|
|
})
|
2020-04-11 13:47:59 +00:00
|
|
|
}
|
|
|
|
}).catch(function (err) {
|
2020-05-09 17:58:13 +00:00
|
|
|
logger.error('operation callback failed: ' + err)
|
2020-04-11 13:47:59 +00:00
|
|
|
})
|
|
|
|
}
|
2020-05-24 20:18:44 +00:00
|
|
|
if (userId) note.tempUsers.set(userId, Date.now())
|
2020-04-11 13:47:59 +00:00
|
|
|
}
|
|
|
|
// save authorship - use timer here because it's an O(n) complexity algorithm
|
|
|
|
setImmediate(function () {
|
|
|
|
note.authorship = Note.updateAuthorshipByOperation(operation, userId, note.authorship)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-05-09 17:58:13 +00:00
|
|
|
function startConnection (socket: SocketWithNoteId): void {
|
2017-03-08 10:45:51 +00:00
|
|
|
if (isConnectionBusy) return
|
|
|
|
isConnectionBusy = true
|
|
|
|
|
2020-05-09 17:58:13 +00:00
|
|
|
const noteId: string = socket.noteId
|
2017-03-08 10:45:51 +00:00
|
|
|
if (!noteId) {
|
|
|
|
return failConnection(404, 'note id not found', socket)
|
|
|
|
}
|
|
|
|
|
2020-05-24 20:18:44 +00:00
|
|
|
if (!notes.get(noteId)) {
|
2020-04-11 13:47:59 +00:00
|
|
|
const include = [{
|
2020-04-11 09:27:54 +00:00
|
|
|
model: User,
|
2017-03-08 10:45:51 +00:00
|
|
|
as: 'owner'
|
|
|
|
}, {
|
2020-04-11 09:27:54 +00:00
|
|
|
model: User,
|
2017-03-08 10:45:51 +00:00
|
|
|
as: 'lastchangeuser'
|
|
|
|
}, {
|
2020-04-11 09:27:54 +00:00
|
|
|
model: Author,
|
2017-03-08 10:45:51 +00:00
|
|
|
as: 'authors',
|
|
|
|
include: [{
|
2020-04-11 09:27:54 +00:00
|
|
|
model: User,
|
2017-03-08 10:45:51 +00:00
|
|
|
as: 'user'
|
|
|
|
}]
|
|
|
|
}]
|
2017-01-10 02:02:37 +00:00
|
|
|
|
2020-04-11 09:27:54 +00:00
|
|
|
Note.findOne({
|
2017-03-08 10:45:51 +00:00
|
|
|
where: {
|
|
|
|
id: noteId
|
|
|
|
},
|
|
|
|
include: include
|
|
|
|
}).then(function (note) {
|
|
|
|
if (!note) {
|
|
|
|
return failConnection(404, 'note not found', socket)
|
|
|
|
}
|
2020-06-23 10:43:24 +00:00
|
|
|
const ownerId = note.ownerId
|
2020-06-04 19:25:42 +00:00
|
|
|
const ownerprofile = note.owner ? PhotoProfile.fromUser(note.owner) : null
|
2017-03-08 10:45:51 +00:00
|
|
|
|
2020-04-11 13:47:59 +00:00
|
|
|
const lastchangeuser = note.lastchangeuserId
|
2020-06-04 19:25:42 +00:00
|
|
|
const lastchangeuserprofile = note.lastchangeuser ? PhotoProfile.fromUser(note.lastchangeuser) : null
|
2017-03-08 10:45:51 +00:00
|
|
|
|
2020-04-11 13:47:59 +00:00
|
|
|
const body = note.content
|
|
|
|
const createtime = note.createdAt
|
|
|
|
const updatetime = note.lastchangeAt
|
2020-04-13 14:21:44 +00:00
|
|
|
const server = new EditorSocketIOServer(body, [], noteId, ifMayEdit, operationCallback)
|
2017-03-08 10:45:51 +00:00
|
|
|
|
2020-05-24 20:18:44 +00:00
|
|
|
const authors = new Map<string, UserSession>()
|
|
|
|
for (const author of note.authors) {
|
2020-06-04 19:25:42 +00:00
|
|
|
const profile = PhotoProfile.fromUser(author.user)
|
2018-05-25 14:15:10 +00:00
|
|
|
if (profile) {
|
2020-05-24 20:18:44 +00:00
|
|
|
authors.set(author.userId, {
|
2018-05-25 14:15:10 +00:00
|
|
|
userid: author.userId,
|
|
|
|
color: author.color,
|
|
|
|
photo: profile.photo,
|
2020-05-25 21:22:27 +00:00
|
|
|
name: profile.name
|
2020-05-24 20:18:44 +00:00
|
|
|
})
|
2017-03-08 10:45:51 +00:00
|
|
|
}
|
|
|
|
}
|
2015-05-04 07:53:29 +00:00
|
|
|
|
2020-05-24 20:18:44 +00:00
|
|
|
notes.set(noteId, {
|
2017-03-08 10:45:51 +00:00
|
|
|
id: noteId,
|
|
|
|
alias: note.alias,
|
|
|
|
title: note.title,
|
2020-06-23 10:43:24 +00:00
|
|
|
ownerId: ownerId,
|
2017-03-08 10:45:51 +00:00
|
|
|
ownerprofile: ownerprofile,
|
|
|
|
permission: note.permission,
|
|
|
|
lastchangeuser: lastchangeuser,
|
|
|
|
lastchangeuserprofile: lastchangeuserprofile,
|
|
|
|
socks: [],
|
2020-05-24 20:18:44 +00:00
|
|
|
users: new Map<string, UserSession>(),
|
|
|
|
tempUsers: new Map<string, number>(),
|
2017-03-08 10:45:51 +00:00
|
|
|
createtime: moment(createtime).valueOf(),
|
|
|
|
updatetime: moment(updatetime).valueOf(),
|
|
|
|
server: server,
|
|
|
|
authors: authors,
|
|
|
|
authorship: note.authorship
|
2020-05-24 20:18:44 +00:00
|
|
|
})
|
2016-04-20 10:03:55 +00:00
|
|
|
|
2017-03-08 10:45:51 +00:00
|
|
|
return finishConnection(socket, noteId, socket.id)
|
|
|
|
}).catch(function (err) {
|
|
|
|
return failConnection(500, err, socket)
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
return finishConnection(socket, noteId, socket.id)
|
|
|
|
}
|
2015-05-04 07:53:29 +00:00
|
|
|
}
|
|
|
|
|
2020-04-11 13:47:59 +00:00
|
|
|
isConnectionBusy = false
|
|
|
|
isDisconnectBusy = false
|
2016-04-20 10:03:55 +00:00
|
|
|
|
2020-05-09 17:58:13 +00:00
|
|
|
function disconnect (socket: SocketWithNoteId): void {
|
2017-03-08 10:45:51 +00:00
|
|
|
if (isDisconnectBusy) return
|
|
|
|
isDisconnectBusy = true
|
|
|
|
|
2019-06-08 18:51:24 +00:00
|
|
|
logger.debug('SERVER disconnected a client')
|
2020-05-24 20:18:44 +00:00
|
|
|
logger.debug(JSON.stringify(users.get(socket.id)))
|
2017-03-08 10:45:51 +00:00
|
|
|
|
2020-05-24 20:18:44 +00:00
|
|
|
if (users.get(socket.id)) {
|
|
|
|
users.delete(socket.id)
|
2017-03-08 10:45:51 +00:00
|
|
|
}
|
2020-04-11 13:47:59 +00:00
|
|
|
const noteId = socket.noteId
|
2020-05-24 20:18:44 +00:00
|
|
|
const note = notes.get(noteId)
|
2017-03-08 10:45:51 +00:00
|
|
|
if (note) {
|
|
|
|
// delete user in users
|
2020-05-24 20:18:44 +00:00
|
|
|
if (note.users.get(socket.id)) {
|
|
|
|
note.users.delete(socket.id)
|
2015-05-04 07:53:29 +00:00
|
|
|
}
|
2017-03-08 10:45:51 +00:00
|
|
|
// remove sockets in the note socks
|
2020-04-11 13:47:59 +00:00
|
|
|
let index
|
2017-03-08 10:45:51 +00:00
|
|
|
do {
|
2020-04-11 13:47:59 +00:00
|
|
|
index = note.socks.indexOf(socket)
|
2017-03-08 10:45:51 +00:00
|
|
|
if (index !== -1) {
|
|
|
|
note.socks.splice(index, 1)
|
|
|
|
}
|
|
|
|
} while (index !== -1)
|
|
|
|
// remove note in notes if no user inside
|
2020-05-24 20:18:44 +00:00
|
|
|
if (note.users.size <= 0) {
|
2017-03-08 10:45:51 +00:00
|
|
|
if (note.server.isDirty) {
|
2020-05-24 20:18:44 +00:00
|
|
|
updateNote(note, function (err, _) {
|
2017-03-08 10:45:51 +00:00
|
|
|
if (err) return logger.error('disconnect note failed: ' + err)
|
|
|
|
// clear server before delete to avoid memory leaks
|
|
|
|
note.server.document = ''
|
|
|
|
note.server.operations = []
|
|
|
|
delete note.server
|
2020-05-24 20:18:44 +00:00
|
|
|
notes.delete(noteId)
|
2017-03-08 10:45:51 +00:00
|
|
|
if (config.debug) {
|
2019-06-08 18:51:24 +00:00
|
|
|
logger.debug(notes)
|
2017-03-08 10:45:51 +00:00
|
|
|
getStatus(function (data) {
|
2019-06-08 18:51:24 +00:00
|
|
|
logger.debug(JSON.stringify(data))
|
2017-03-08 10:45:51 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
delete note.server
|
2020-05-24 20:18:44 +00:00
|
|
|
notes.delete(noteId)
|
2017-03-08 10:45:51 +00:00
|
|
|
}
|
2015-05-04 07:53:29 +00:00
|
|
|
}
|
2017-03-08 10:45:51 +00:00
|
|
|
}
|
|
|
|
emitOnlineUsers(socket)
|
|
|
|
|
|
|
|
// clear finished socket in queue
|
|
|
|
clearSocketQueue(disconnectSocketQueue, socket)
|
|
|
|
// seek for next socket
|
|
|
|
isDisconnectBusy = false
|
2020-04-11 09:27:54 +00:00
|
|
|
if (disconnectSocketQueue.length > 0) {
|
|
|
|
disconnect(disconnectSocketQueue[0])
|
|
|
|
}
|
2017-03-08 10:45:51 +00:00
|
|
|
|
|
|
|
if (config.debug) {
|
2019-06-08 18:51:24 +00:00
|
|
|
logger.debug(notes)
|
2017-03-08 10:45:51 +00:00
|
|
|
getStatus(function (data) {
|
2019-06-08 18:51:24 +00:00
|
|
|
logger.debug(JSON.stringify(data))
|
2017-03-08 10:45:51 +00:00
|
|
|
})
|
|
|
|
}
|
2015-05-04 07:53:29 +00:00
|
|
|
}
|
|
|
|
|
2020-04-11 13:47:59 +00:00
|
|
|
// clean when user not in any rooms or user not in connected list
|
|
|
|
setInterval(function () {
|
2020-05-24 20:18:44 +00:00
|
|
|
for (const [key, user] of users) {
|
2020-05-25 21:22:27 +00:00
|
|
|
let socket = realtime.io.sockets.connected[key] as SocketWithNoteId
|
2020-05-24 20:18:44 +00:00
|
|
|
if ((!socket && user) ||
|
2020-05-25 21:22:27 +00:00
|
|
|
(socket && (!socket.rooms || Object.keys(socket.rooms).length <= 0))) {
|
2020-04-11 13:47:59 +00:00
|
|
|
logger.debug(`cleaner found redundant user: ${key}`)
|
|
|
|
if (!socket) {
|
|
|
|
socket = {
|
|
|
|
id: key
|
2020-05-25 21:22:27 +00:00
|
|
|
} as SocketWithNoteId
|
2020-04-11 13:47:59 +00:00
|
|
|
}
|
|
|
|
disconnectSocketQueue.push(socket)
|
|
|
|
disconnect(socket)
|
|
|
|
}
|
2020-05-24 20:18:44 +00:00
|
|
|
}
|
2020-04-11 13:47:59 +00:00
|
|
|
}, 60000)
|
2015-06-01 10:04:25 +00:00
|
|
|
|
2020-05-09 17:58:13 +00:00
|
|
|
function updateUserData (socket: Socket, user): void {
|
2017-03-08 10:45:51 +00:00
|
|
|
// retrieve user data from passport
|
|
|
|
if (socket.request.user && socket.request.user.logged_in) {
|
2020-06-04 19:25:42 +00:00
|
|
|
const profile = PhotoProfile.fromUser(socket.request.user)
|
2020-04-11 13:47:59 +00:00
|
|
|
user.photo = profile?.photo
|
|
|
|
user.name = profile?.name
|
2017-03-08 10:45:51 +00:00
|
|
|
user.userid = socket.request.user.id
|
|
|
|
user.login = true
|
|
|
|
} else {
|
|
|
|
user.userid = null
|
|
|
|
user.name = 'Guest ' + chance.last()
|
|
|
|
user.login = false
|
|
|
|
}
|
2015-06-01 10:04:25 +00:00
|
|
|
}
|
2015-05-04 07:53:29 +00:00
|
|
|
|
2020-05-09 17:58:13 +00:00
|
|
|
function connection (socket: SocketWithNoteId): void {
|
Improve handling of termination signals
Previously, upon receiving a termination signal, the process tries to
flush all changes to the database, retrying every 0.1s until it
succeeds. However, if the database is not set up properly, this always
fails, and spams the terminal/logging with the error message 10 times a
second.
If the user sends another termination signal, the handleTermSignal
function is called once again, and we get twice the number of error
messages.
This commit changes the behaviour in various ways.
(1) It lowers the retry rate to 0.5s, and aborts after 30 seconds.
(2) If the write to the database errored, the error message explains
that this is due to us flushing the final changes.
(3) We replace realtime.maintenance with realtime.state, which is an
Enum with three possible states --- Starting, Running, and Stopping.
If a termination signal is received in the starting state, the
process simply aborts because there is nothing to clean up. This is
the case when the database is misconfigured, since the application
starts up only after connecting to the databse. If it is in the
Stopping state, the handleTermSignal function returns because
another instance of handleTermSignal is already running.
Fixes #408
Signed-off-by: Dexter Chua <dec41@srcf.net>
2020-06-27 03:06:48 +00:00
|
|
|
if (realtime.state !== State.Running) return
|
2017-03-08 10:45:51 +00:00
|
|
|
parseNoteIdFromSocket(socket, function (err, noteId) {
|
|
|
|
if (err) {
|
|
|
|
return failConnection(500, err, socket)
|
|
|
|
}
|
|
|
|
if (!noteId) {
|
|
|
|
return failConnection(404, 'note id not found', socket)
|
|
|
|
}
|
2016-10-10 12:25:48 +00:00
|
|
|
|
2018-01-16 07:51:24 +00:00
|
|
|
if (isDuplicatedInSocketQueue(connectionSocketQueue, socket)) return
|
2017-03-08 10:45:51 +00:00
|
|
|
|
|
|
|
// store noteId in this socket session
|
|
|
|
socket.noteId = noteId
|
|
|
|
|
|
|
|
// initialize user data
|
|
|
|
// random color
|
2020-04-11 13:47:59 +00:00
|
|
|
let color = randomcolor()
|
2017-03-08 10:45:51 +00:00
|
|
|
// make sure color not duplicated or reach max random count
|
2020-05-24 20:18:44 +00:00
|
|
|
const note = notes.get(noteId)
|
|
|
|
if (note) {
|
2020-04-11 13:47:59 +00:00
|
|
|
let randomcount = 0
|
|
|
|
const maxrandomcount = 10
|
|
|
|
let found = false
|
2017-03-08 10:45:51 +00:00
|
|
|
do {
|
2020-05-24 20:18:44 +00:00
|
|
|
for (const user of note.users.values()) {
|
|
|
|
if (user.color === color) {
|
2017-03-08 10:45:51 +00:00
|
|
|
found = true
|
|
|
|
}
|
2020-05-24 20:18:44 +00:00
|
|
|
}
|
2017-03-08 10:45:51 +00:00
|
|
|
if (found) {
|
|
|
|
color = randomcolor()
|
|
|
|
randomcount++
|
2016-04-20 10:03:55 +00:00
|
|
|
}
|
2017-03-08 10:45:51 +00:00
|
|
|
} while (found && randomcount < maxrandomcount)
|
|
|
|
}
|
|
|
|
// create user data
|
2020-05-24 20:18:44 +00:00
|
|
|
users.set(socket.id, {
|
2017-03-08 10:45:51 +00:00
|
|
|
id: socket.id,
|
|
|
|
address: socket.handshake.headers['x-forwarded-for'] || socket.handshake.address,
|
|
|
|
'user-agent': socket.handshake.headers['user-agent'],
|
|
|
|
color: color,
|
2020-05-25 21:22:27 +00:00
|
|
|
cursor: undefined,
|
2017-03-08 10:45:51 +00:00
|
|
|
login: false,
|
|
|
|
userid: null,
|
|
|
|
name: null,
|
|
|
|
idle: false,
|
2020-05-25 21:22:27 +00:00
|
|
|
type: '',
|
|
|
|
photo: ''
|
2020-05-24 20:18:44 +00:00
|
|
|
})
|
|
|
|
updateUserData(socket, users.get(socket.id))
|
2017-03-08 10:45:51 +00:00
|
|
|
|
|
|
|
// start connection
|
|
|
|
connectionSocketQueue.push(socket)
|
|
|
|
startConnection(socket)
|
|
|
|
})
|
|
|
|
|
|
|
|
// received client refresh request
|
|
|
|
socket.on('refresh', function () {
|
|
|
|
emitRefresh(socket)
|
|
|
|
})
|
|
|
|
|
|
|
|
// received user status
|
|
|
|
socket.on('user status', function (data) {
|
2020-04-11 13:47:59 +00:00
|
|
|
const noteId = socket.noteId
|
2020-05-24 20:18:44 +00:00
|
|
|
const user = users.get(socket.id)
|
|
|
|
if (!noteId || !notes.get(noteId) || !user) return
|
2019-06-08 18:51:24 +00:00
|
|
|
logger.debug(`SERVER received [${noteId}] user status from [${socket.id}]: ${JSON.stringify(data)}`)
|
2017-03-08 10:45:51 +00:00
|
|
|
if (data) {
|
|
|
|
user.idle = data.idle
|
|
|
|
user.type = data.type
|
|
|
|
}
|
|
|
|
emitUserStatus(socket)
|
|
|
|
})
|
|
|
|
|
|
|
|
// received note permission change request
|
|
|
|
socket.on('permission', function (permission) {
|
|
|
|
// need login to do more actions
|
|
|
|
if (socket.request.user && socket.request.user.logged_in) {
|
2020-04-11 13:47:59 +00:00
|
|
|
const noteId = socket.noteId
|
2020-05-24 20:18:44 +00:00
|
|
|
if (!noteId) return
|
|
|
|
const note = notes.get(noteId)
|
|
|
|
if (!note) return
|
2017-03-08 10:45:51 +00:00
|
|
|
// Only owner can change permission
|
2020-06-23 10:43:24 +00:00
|
|
|
if (getPermission(socket.request.user, note) === Permission.Owner) {
|
2018-04-10 12:38:39 +00:00
|
|
|
if (permission === 'freely' && !config.allowAnonymous && !config.allowAnonymousEdits) return
|
2017-03-08 10:45:51 +00:00
|
|
|
note.permission = permission
|
2020-04-11 09:27:54 +00:00
|
|
|
Note.update({
|
2017-03-08 10:45:51 +00:00
|
|
|
permission: permission
|
|
|
|
}, {
|
|
|
|
where: {
|
|
|
|
id: noteId
|
|
|
|
}
|
|
|
|
}).then(function (count) {
|
|
|
|
if (!count) {
|
|
|
|
return
|
|
|
|
}
|
2020-04-11 13:47:59 +00:00
|
|
|
const out = {
|
2017-03-08 10:45:51 +00:00
|
|
|
permission: permission
|
|
|
|
}
|
|
|
|
realtime.io.to(note.id).emit('permission', out)
|
2020-04-11 13:47:59 +00:00
|
|
|
for (let i = 0, l = note.socks.length; i < l; i++) {
|
|
|
|
const sock = note.socks[i]
|
2017-03-08 10:45:51 +00:00
|
|
|
if (typeof sock !== 'undefined' && sock) {
|
|
|
|
// check view permission
|
2020-06-23 10:43:24 +00:00
|
|
|
if (getPermission(sock.request.user, note) === Permission.None) {
|
2017-03-08 10:45:51 +00:00
|
|
|
sock.emit('info', {
|
|
|
|
code: 403
|
|
|
|
})
|
|
|
|
setTimeout(function () {
|
|
|
|
sock.disconnect(true)
|
|
|
|
}, 0)
|
|
|
|
}
|
2015-07-01 16:10:20 +00:00
|
|
|
}
|
2017-03-08 10:45:51 +00:00
|
|
|
}
|
|
|
|
}).catch(function (err) {
|
|
|
|
return logger.error('update note permission failed: ' + err)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// delete a note
|
|
|
|
socket.on('delete', function () {
|
|
|
|
// need login to do more actions
|
|
|
|
if (socket.request.user && socket.request.user.logged_in) {
|
2020-04-11 13:47:59 +00:00
|
|
|
const noteId = socket.noteId
|
2020-05-24 20:18:44 +00:00
|
|
|
if (!noteId) return
|
|
|
|
const note = notes.get(noteId)
|
|
|
|
if (!note) return
|
2017-03-08 10:45:51 +00:00
|
|
|
// Only owner can delete note
|
2020-06-23 10:43:24 +00:00
|
|
|
if (getPermission(socket.request.user, note) === Permission.Owner) {
|
2020-04-11 09:27:54 +00:00
|
|
|
Note.destroy({
|
2017-03-08 10:45:51 +00:00
|
|
|
where: {
|
|
|
|
id: noteId
|
|
|
|
}
|
|
|
|
}).then(function (count) {
|
|
|
|
if (!count) return
|
2020-04-11 13:47:59 +00:00
|
|
|
for (let i = 0, l = note.socks.length; i < l; i++) {
|
|
|
|
const sock = note.socks[i]
|
2017-03-08 10:45:51 +00:00
|
|
|
if (typeof sock !== 'undefined' && sock) {
|
|
|
|
sock.emit('delete')
|
|
|
|
setTimeout(function () {
|
|
|
|
sock.disconnect(true)
|
|
|
|
}, 0)
|
2016-10-10 13:04:24 +00:00
|
|
|
}
|
2017-03-08 10:45:51 +00:00
|
|
|
}
|
|
|
|
}).catch(function (err) {
|
|
|
|
return logger.error('delete note failed: ' + err)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// reveiced when user logout or changed
|
|
|
|
socket.on('user changed', function () {
|
|
|
|
logger.info('user changed')
|
2020-04-11 13:47:59 +00:00
|
|
|
const noteId = socket.noteId
|
2020-05-24 20:18:44 +00:00
|
|
|
if (!noteId) return
|
|
|
|
const note = notes.get(noteId)
|
|
|
|
if (!note) return
|
|
|
|
const user = note.users.get(socket.id)
|
2017-03-08 10:45:51 +00:00
|
|
|
if (!user) return
|
|
|
|
updateUserData(socket, user)
|
|
|
|
emitOnlineUsers(socket)
|
|
|
|
})
|
|
|
|
|
|
|
|
// received sync of online users request
|
|
|
|
socket.on('online users', function () {
|
2020-04-11 13:47:59 +00:00
|
|
|
const noteId = socket.noteId
|
2020-05-24 20:18:44 +00:00
|
|
|
if (!noteId) return
|
|
|
|
const note = notes.get(noteId)
|
|
|
|
if (!note) return
|
2020-05-24 16:41:15 +00:00
|
|
|
const users: UserSession[] = []
|
2020-05-24 20:18:44 +00:00
|
|
|
for (const user of note.users.values()) {
|
2020-04-11 09:27:54 +00:00
|
|
|
if (user) {
|
|
|
|
users.push(buildUserOutData(user))
|
|
|
|
}
|
2020-05-24 20:18:44 +00:00
|
|
|
}
|
2020-04-11 13:47:59 +00:00
|
|
|
const out = {
|
2017-03-08 10:45:51 +00:00
|
|
|
users: users
|
|
|
|
}
|
|
|
|
socket.emit('online users', out)
|
|
|
|
})
|
|
|
|
|
|
|
|
// check version
|
|
|
|
socket.on('version', function () {
|
|
|
|
socket.emit('version', {
|
2018-10-05 17:33:40 +00:00
|
|
|
version: config.fullversion,
|
2017-03-08 10:45:51 +00:00
|
|
|
minimumCompatibleVersion: config.minimumCompatibleVersion
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
// received cursor focus
|
|
|
|
socket.on('cursor focus', function (data) {
|
2020-04-11 13:47:59 +00:00
|
|
|
const noteId = socket.noteId
|
2020-05-24 20:18:44 +00:00
|
|
|
const user = users.get(socket.id)
|
|
|
|
if (!noteId || !notes.get(noteId) || !user) return
|
2017-03-08 10:45:51 +00:00
|
|
|
user.cursor = data
|
2020-04-11 13:47:59 +00:00
|
|
|
const out = buildUserOutData(user)
|
2017-03-08 10:45:51 +00:00
|
|
|
socket.broadcast.to(noteId).emit('cursor focus', out)
|
|
|
|
})
|
|
|
|
|
|
|
|
// received cursor activity
|
2020-05-25 21:22:27 +00:00
|
|
|
socket.on('cursor activity', function (data: CodeMirror.Position) {
|
2020-04-11 13:47:59 +00:00
|
|
|
const noteId = socket.noteId
|
2020-05-24 20:18:44 +00:00
|
|
|
const user = users.get(socket.id)
|
|
|
|
if (!noteId || !notes.get(noteId) || !user) return
|
2017-03-08 10:45:51 +00:00
|
|
|
user.cursor = data
|
2020-04-11 13:47:59 +00:00
|
|
|
const out = buildUserOutData(user)
|
2017-03-08 10:45:51 +00:00
|
|
|
socket.broadcast.to(noteId).emit('cursor activity', out)
|
|
|
|
})
|
|
|
|
|
|
|
|
// received cursor blur
|
|
|
|
socket.on('cursor blur', function () {
|
2020-04-11 13:47:59 +00:00
|
|
|
const noteId = socket.noteId
|
2020-05-24 20:18:44 +00:00
|
|
|
const user = users.get(socket.id)
|
|
|
|
if (!noteId || !notes.get(noteId) || !user) return
|
2020-05-25 21:22:27 +00:00
|
|
|
user.cursor = undefined
|
2020-04-11 13:47:59 +00:00
|
|
|
const out = {
|
2017-03-08 10:45:51 +00:00
|
|
|
id: socket.id
|
|
|
|
}
|
|
|
|
socket.broadcast.to(noteId).emit('cursor blur', out)
|
|
|
|
})
|
|
|
|
|
|
|
|
// when a new client disconnect
|
|
|
|
socket.on('disconnect', function () {
|
2020-05-09 16:42:36 +00:00
|
|
|
if (isDuplicatedInSocketQueue(disconnectSocketQueue, socket)) return
|
2017-03-08 10:45:51 +00:00
|
|
|
disconnectSocketQueue.push(socket)
|
|
|
|
disconnect(socket)
|
|
|
|
})
|
2015-05-04 07:53:29 +00:00
|
|
|
}
|
|
|
|
|
2020-04-12 18:54:46 +00:00
|
|
|
export { realtime }
|