MAJOR OVERHAUL: Working WebSocket client! No translation server, yet.

This commit is contained in:
Evert Prants 2020-02-03 21:45:12 +02:00
parent e7b9836df6
commit 394eb42fbc
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
14 changed files with 2626 additions and 3229 deletions

View File

@ -1,3 +1,4 @@
{
"presets": ["@babel/preset-env"]
"presets": ["@babel/preset-env"],
"plugins": ["@babel/plugin-transform-runtime"]
}

View File

@ -1,8 +1,8 @@
The MIT License (MIT)
Copyright (c) 2016 Evert Prants
Copyright (c) 2016-2020 Evert Prants
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

19
globals.js Normal file
View File

@ -0,0 +1,19 @@
const fs = require('fs')
const path = require('path')
const toml = require('toml')
const filename = path.join(__dirname, 'client.config.toml')
const pkg = require(path.join(__dirname, 'package.json'))
let config
try {
config = toml.parse(fs.readFileSync(filename))
} catch (e) {
console.error(e)
process.exit(1)
}
config.client.version = pkg.version
config.client.description = pkg.description
module.exports = config

4754
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "teemantirc",
"version": "2.0.0",
"description": "Web-based IRC client",
"version": "2.0.1",
"description": "A Web-based IRC client",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
@ -22,30 +22,32 @@
"author": "Evert",
"license": "MIT",
"dependencies": {
"express": "^4.16.4",
"seedrandom": "^2.4.4",
"socket.io": "^2.2.0",
"toml": "^2.3.5"
"express": "^4.17.1",
"toml": "^2.3.6",
"ws": "^7.2.1"
},
"repository": {
"type": "git",
"url": "git://gitlab.icynet.eu/IcyNetwork/teemant.git"
},
"devDependencies": {
"@babel/cli": "^7.2.3",
"@babel/core": "^7.2.2",
"@babel/preset-env": "^7.2.3",
"babel-loader": "^8.0.5",
"concurrently": "^4.1.0",
"copy-webpack-plugin": "^4.6.0",
"css-loader": "^2.1.0",
"@babel/cli": "^7.8.4",
"@babel/core": "^7.8.4",
"@babel/plugin-transform-runtime": "^7.8.3",
"@babel/preset-env": "^7.8.4",
"@babel/runtime": "^7.8.4",
"babel-loader": "^8.0.6",
"concurrently": "^4.1.2",
"copy-webpack-plugin": "^5.1.1",
"css-loader": "^2.1.1",
"file-loader": "^3.0.1",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.5.0",
"standard": "^12.0.1",
"stylus": "^0.54.5",
"stylus": "^0.54.7",
"stylus-loader": "^3.0.2",
"webpack": "^4.28.1",
"webpack-command": "^0.4.2"
"webpack": "^4.41.5",
"webpack-cli": "^3.3.10",
"seedrandom": "^2.4.4"
}
}

View File

@ -3,14 +3,8 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>TeemantIRC</title>
<script src="//twemoji.maxcdn.com/2/twemoji.min.js?11.2"></script>
<script type="text/javascript" src="/socket.io/socket.io.js"></script>
<script type="text/javascript" src="main.js"></script>
<link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet" type="text/css">
<link rel="stylesheet" type="text/css" href="style/layout.css">
<link rel="stylesheet" type="text/css" href="style/theme_default.css" id="theme_stylesheet">

View File

@ -1,8 +1,134 @@
/* global WebSocket */
import { EventEmitter } from 'events'
import net from 'net'
import tls from 'tls'
import util from 'util'
import parse from './parser'
import { format } from 'util'
let translatorSocket
class FakeSocket {
constructor (server, socket) {
this.open = true
this.server = server
this.socket = socket
this.handlers = []
this.socket.addEventListener('message', (msg) => {
let tm = msg.split(' ')
if (tm[0] === 'TRANSLATOR') {
if ((tm[1] === 'CLOSE' || tm[1] === 'ERROR') && tm[2] === this.server) {
return this._socketClosed()
}
}
if (tm[0] === server) {
this._handle('message', tm.slice(1).join(' '))
}
})
this.socket.addEventListener('close', () => {
this._socketClosed()
})
}
send () {
if (!this.open) return
this.socket.send(this.server + ' ' + format.apply(Array.prototype.slice.call(arguments)))
}
addEventListener (type, fn) {
this.handlers.push({ target: type, fn })
}
_socketClosed () {
this.open = false
for (let a in this.handlers) {
let at = this.handlers[a]
if (at.target === 'close') {
at.fn.apply(this, [])
}
}
}
_handle (ev) {
if (!this.open) return
for (let a in this.handlers) {
let ap = this.handlers[a]
if (ap.target === ev) {
ap.fn.apply(this.socket, Array.prototype.slice.call(arguments, 1))
}
}
}
close () {
this.open = false
this.socket.send('TRANSLATOR DISCONNECT ' + this.server)
}
static addListenerPassthrough (fs, type) {
fs.socket.addEventListener(type, () => {
this._handle.apply(this, [type].concat(arguments))
})
}
}
async function _waitOpen (s) {
let o
let c
try {
await new Promise((resolve, reject) => {
o = function () {
console.log('removing events', o, c)
s.removeEventListener('close', c)
s.removeEventListener('open', o)
resolve()
}
s.addEventListener('open', o)
c = function (err) {
reject(err)
}
s.addEventListener('close', c)
})
} catch (e) {
throw e
}
return true
}
async function _trySocket (address, port, ssl, useTranslator) {
// Attempt a direct ws connection
let proto = 'ws'
if (ssl) proto = 'wss'
if (!useTranslator) {
let conn = address
if (port && (port < 6000 || port > 7000)) {
conn = address + ':' + port
}
let tSock = new WebSocket(proto + '://' + conn)
try {
console.log('Waiting to open')
await _waitOpen(tSock)
} catch (e) {
console.error(e)
console.error('Waiting to open failed')
return _trySocket(address, port, ssl, true)
}
tSock.open = true
tSock.addEventListener('close', () => { tSock.open = false })
return tSock
}
if (translatorSocket) {
translatorSocket.send('TRANSLATOR SERVER ' + address + ' ' + port + (ssl ? ' SSL' : ''))
return new FakeSocket(address, translatorSocket)
}
translatorSocket = new WebSocket(`ws${window.location.protocol === 'https:' ? 's' : ''}://${window.location.host}`)
console.log('Opening translator socket')
await _waitOpen(translatorSocket)
return new FakeSocket(address, translatorSocket)
}
class IRCConnectionHandler {
constructor (connection) {
@ -122,11 +248,11 @@ class IRCConnectionHandler {
let resp = '\x01' + line[0] + ' %s\x01'
if (line[0] === 'PING' && line[1] != null && line[1] !== '') {
resp = util.format(resp, line.slice(1).join(' '))
resp = format(resp, line.slice(1).join(' '))
} else if (line[0] === 'CLIENTINFO') {
resp = util.format(resp, 'CLIENTINFO PING ' + Object.keys(this.conn.extras.ctcps).join(' '))
resp = format(resp, 'CLIENTINFO PING ' + Object.keys(this.conn.extras.ctcps).join(' '))
} else if (this.conn.extras.ctcps && this.conn.extras.ctcps[line[0]] != null) {
resp = util.format(resp, this.conn.extras.ctcps[line[0]](data, this.conn))
resp = format(resp, this.conn.extras.ctcps[line[0]](data, this.conn))
} else {
resp = null
}
@ -376,6 +502,7 @@ class IRCConnectionHandler {
case '323':
case '351':
case '381':
case '489':
this.conn.emit('pass_to_client', {
type: 'server_message', messageType: 'regular', message: line.trailing, server: serverName, from: realServerName
})
@ -471,7 +598,7 @@ class IRCConnectionHandler {
// start whois queue
list = {
nickname: line.arguments[1],
hostmask: util.format('%s!%s@%s', line.arguments[1], line.arguments[2], line.arguments[3]),
hostmask: format('%s!%s@%s', line.arguments[1], line.arguments[2], line.arguments[3]),
realname: line.trailing || ''
}
this.whoisManage(line.arguments[1], list)
@ -620,39 +747,19 @@ class IRCConnection extends EventEmitter {
}
connect () {
this.socket = (this.config.secure ? tls : net).connect({
port: this.config.port,
host: this.config.server,
rejectUnauthorized: this.config.rejectUnauthorized
}, () => {
this.connected = true
this.authenticate()
})
this.socket.setEncoding(this.globalConfig.encoding)
this.socket.setTimeout(this.globalConfig.timeout)
this.socket.on('error', (data) => {
this.emit('connerror', { type: 'sock_error', message: 'A socket error occured.', raw: data })
})
this.socket.on('lookup', (err, address, family, host) => {
if (err) {
this.emit('connerror', { type: 'resolve_error', message: 'Failed to resolve host.' })
} else {
this.emit('lookup', { address: address, family: address, host: host })
this.config.address = address
async function wrapped (argument) {
try {
this.socket = await _trySocket(this.config.server, this.config.port, this.config.secure)
} catch (e) {
this.emit('connerror', { type: 'sock_error', message: 'A socket error occured.' })
throw e
}
})
let buffer = ''
this.socket.on('data', (chunk) => {
buffer += chunk
let data = buffer.split('\r\n')
buffer = data.pop()
data.forEach((line) => {
this.socket.addEventListener('message', (e) => {
let line = e.data
if (line.indexOf('PING') === 0) {
this.write('PONG %s\r\n', line.substring(4))
console.log('%s server pinged us (%s)', new Date(), line)
this.write('PONG %s', line.substring(4))
return
}
@ -668,21 +775,26 @@ class IRCConnection extends EventEmitter {
console.error(e.stack)
}
})
})
this.socket.on('close', (data) => {
if (!this.queue['close']) {
this.emit('closed', { type: 'sock_closed', raw: data, message: 'Connection closed.' })
}
this.socket.addEventListener('close', (data) => {
if (!this.queue['close']) {
this.emit('closed', { type: 'sock_closed', raw: data, message: 'Connection closed.' })
}
this.connected = false
this.authenticated = false
})
this.connected = false
this.authenticated = false
})
this.connected = true
this.authenticate()
}
return wrapped.call(this)
}
authenticate () {
if (this.config.password) {
this.write('PASS %s\r\n', this.config.password)
this.write('PASS %s', this.config.password)
}
if (this.extras.authenticationSteps) {
@ -692,8 +804,23 @@ class IRCConnection extends EventEmitter {
}
}
this.write('USER %s 8 * :%s\r\n', this.config.username, this.config.realname)
this.write('NICK %s\r\n', this.config.nickname)
this.write('USER %s 8 * :%s', this.config.username, this.config.realname)
this.write('NICK %s', this.config.nickname)
this.bindFinal()
this.sendPing()
}
bindFinal () {
this.on('userinput', (data) => {
return this.handler.handleUserLine(data)
})
}
sendPing () {
if (!this.connected) return
this.write('PING :' + this.data.actualServer)
setTimeout(() => this.sendPing(), 5000)
}
disconnect (message) {
@ -703,17 +830,17 @@ class IRCConnection extends EventEmitter {
}
this.queue['close'] = true
this.write('QUIT :%s\r\n', (message != null ? message : this.globalConfig.default_quit_msg))
this.write('QUIT :%s', (message != null ? message : this.globalConfig.default_quit_msg))
}
write () {
let message = util.format.apply(null, arguments)
let message = format.apply(null, arguments)
if (!this.connected) {
return this.emit('connerror', { type: 'sock_closed', message: 'Connection is closed.' })
}
this.socket.write(message + '\r\n')
this.socket.send(message + '\r\n')
}
}
module.exports = IRCConnection
export { IRCConnection }

View File

@ -1,13 +1,16 @@
/* global irc, twemoji, io */
/* global irc, twemoji, globalConfig */
/* eslint-disable no-useless-escape, no-control-regex */
import { el, span, sf, formatDate, removeStr, match, serialize, objectGetKey, rand, addClass, removeClass, toggleClass } from './utility'
import stylize from './colorparser'
import seedrandom from 'seedrandom'
const themes = require('./theme.js')
import { el, span, sf, formatDate, removeStr, match, serialize, objectGetKey, rand, addClass, removeClass, toggleClass } from './utility'
import stylize from './colorparser'
import themes from './theme'
import { IRCConnection } from './irc'
const gcfg = Object.assign({}, globalConfig)
window.irc = {
socketUp: false,
primaryFrame: null,
config: {
colors: true,
@ -25,7 +28,17 @@ window.irc = {
serverData: {},
serverChatQueue: {},
chatType: 'simple',
documentTitle: 'TeemantIRC'
documentTitle: 'TeemantIRC',
handler: null
}
const customCTCPs = {
VERSION: function (data, connection) {
return `TeemantIRC - ${gcfg.description} ver. ${gcfg.version} - https://teemant.icynet.eu/`
},
SOURCE: function (data, connection) {
return 'https://gitlab.icynet.eu/IcyNetwork/teemant'
}
}
const clientdom = { connector: {}, settings: {} }
@ -302,12 +315,18 @@ let composer = {
}
}
function sendToServer (server, obj) {
let i = irc.handler.getClientByActualServer(server)
if (!i) throw new Error('Invalid connection ' + server)
i.emit('userinput', obj)
}
// onclick food
window.irc.joinChannel = (channel) => {
let buf = irc.chat.getActiveChatBuffer()
if (!buf || !buf.server) return
irc.socket.emit('userinput', { command: 'join', server: buf.server, message: '', arguments: [channel] })
sendToServer(buf.server, { command: 'join', message: '', arguments: [channel] })
return false
}
@ -333,12 +352,12 @@ let commands = {
execute: function (buffer, handler, command, message, listargs) {
if (!listargs[1]) {
if (!buffer.alive) {
irc.socket.emit('userinput', { command: 'join', server: buffer.server, message: '', arguments: [buffer.name] })
sendToServer(buffer.server, { command: 'join', message: '', arguments: [buffer.name] })
} else {
handler.commandError(buffer, listargs[0].toUpperCase() + ': Missing parameters!')
}
} else {
irc.socket.emit('userinput', { command: 'join', server: buffer.server, message: '', arguments: [listargs[1]] })
sendToServer(buffer.server, { command: 'join', message: '', arguments: [listargs[1]] })
}
},
description: '<channel> - Join a channel'
@ -347,7 +366,7 @@ let commands = {
part: {
execute: function (buffer, handler, command, message, listargs) {
if (!listargs[1] && buffer.type === 'channel') {
irc.socket.emit('userinput', { command: 'part', server: buffer.server, message: '', arguments: [buffer.name] })
sendToServer(buffer.server, { command: 'part', message: '', arguments: [buffer.name] })
} else if (buffer.type !== 'channel') {
handler.commandError(buffer, listargs[0].toUpperCase() + ': Buffer is not a channel.')
} else if (listargs[1]) {
@ -356,10 +375,10 @@ let commands = {
if (listargs[2]) {
msg = listargs.slice(2).join(' ')
}
irc.socket.emit('userinput', { command: 'part', server: buffer.server, message: msg, arguments: [listargs[1]] })
sendToServer(buffer.server, { command: 'part', message: msg, arguments: [listargs[1]] })
} else {
if (buffer.type === 'channel') {
irc.socket.emit('userinput', { command: 'part', server: buffer.server, message: listargs.slice(1).join(' '), arguments: [buffer.name] })
sendToServer(buffer.server, { command: 'part', message: listargs.slice(1).join(' '), arguments: [buffer.name] })
} else {
handler.commandError(buffer, listargs[0].toUpperCase() + ': Buffer is not a channel.')
}
@ -373,7 +392,7 @@ let commands = {
topic: {
execute: function (buffer, handler, command, message, listargs) {
if (!listargs[1] && buffer.type === 'channel') {
irc.socket.emit('userinput', { command: 'topic', server: buffer.server, message: '', arguments: [buffer.name] })
sendToServer(buffer.server, { command: 'topic', message: '', arguments: [buffer.name] })
} else if (buffer.type !== 'channel') {
handler.commandError(buffer, listargs[0].toUpperCase() + ': Buffer is not a channel.')
} else if (listargs[1]) {
@ -382,10 +401,10 @@ let commands = {
if (listargs[2]) {
msg = listargs.slice(2).join(' ')
}
irc.socket.emit('userinput', { command: 'topic', server: buffer.server, message: msg, arguments: [listargs[1]] })
sendToServer(buffer.server, { command: 'topic', message: msg, arguments: [listargs[1]] })
} else {
if (buffer.type === 'channel') {
irc.socket.emit('userinput', { command: 'topic', server: buffer.server, message: listargs.slice(1).join(' '), arguments: [buffer.name] })
sendToServer(buffer.server, { command: 'topic', message: listargs.slice(1).join(' '), arguments: [buffer.name] })
} else {
handler.commandError(buffer, listargs[0].toUpperCase() + ': Buffer is not a channel.')
}
@ -406,9 +425,8 @@ let commands = {
return handler.commandError(buffer, listargs[0].toUpperCase() + ': Missing parameter <user>!')
}
irc.socket.emit('userinput', {
sendToServer(buffer.server, {
command: 'kick',
server: buffer.server,
message: listargs.slice(2).join(' '),
arguments: [buffer.name, listargs[1]]
})
@ -418,7 +436,7 @@ let commands = {
quit: {
execute: function (buffer, handler, command, message, listargs) {
irc.socket.emit('userinput', { command: 'quit', server: buffer.server, message: listargs.slice(1).join(' '), arguments: [] })
sendToServer(buffer.server, { command: 'quit', message: listargs.slice(1).join(' '), arguments: [] })
},
description: '[<message>] - Quit the current server with message.',
aliases: ['exit']
@ -426,7 +444,7 @@ let commands = {
mode: {
execute: function (buffer, handler, command, message, listargs) {
irc.socket.emit('userinput', { command: 'mode', server: buffer.server, message: listargs.slice(1).join(' '), arguments: [] })
sendToServer(buffer.server, { command: 'mode', message: listargs.slice(1).join(' '), arguments: [] })
},
description: '[target] [mode] - Set/remove mode of target.'
},
@ -441,7 +459,7 @@ let commands = {
listargs[1] = buffer.name
}
irc.socket.emit('userinput', { command: 'privmsg', server: buffer.server, message: listargs.slice(2).join(' '), arguments: [listargs[1]] })
sendToServer(buffer.server, { command: 'privmsg', message: listargs.slice(2).join(' '), arguments: [listargs[1]] })
},
description: '<target> <message> - Sends a message to target.',
aliases: ['privmsg', 'q', 'query', 'say']
@ -459,7 +477,7 @@ let commands = {
listargs[2] = listargs[2].toUpperCase()
irc.socket.emit('userinput', { command: 'ctcp', server: buffer.server, message: listargs.slice(2).join(' '), arguments: listargs.slice(1) })
sendToServer(buffer.server, { command: 'ctcp', message: listargs.slice(2).join(' '), arguments: listargs.slice(1) })
},
description: '<target> <type> [arguments] - Sends a CTCP request to target.'
},
@ -474,9 +492,8 @@ let commands = {
listargs[1] = buffer.name
}
irc.socket.emit('userinput', {
sendToServer(buffer.server, {
command: 'notice',
server: buffer.server,
message: listargs.slice(2).join(' '),
arguments: [listargs[1]]
})
@ -486,9 +503,8 @@ let commands = {
action: {
execute: function (buffer, handler, command, message, listargs) {
irc.socket.emit('userinput', {
sendToServer(buffer.server, {
command: 'privmsg',
server: buffer.server,
message: '\x01ACTION ' + message.substring(command.length + 2) + '\x01',
arguments: [buffer.name]
})
@ -499,7 +515,7 @@ let commands = {
list: {
execute: function (buffer, handler, command, message, listargs) {
irc.socket.emit('userinput', { command: 'list', server: buffer.server, message: '', arguments: listargs.splice(1) })
sendToServer(buffer.server, { command: 'list', message: '', arguments: listargs.splice(1) })
},
description: '- List all channels on the current server.'
},
@ -513,7 +529,7 @@ let commands = {
}
return
}
irc.socket.emit('userinput', { command: 'nick', server: buffer.server, message: '', arguments: listargs.splice(1) })
sendToServer(buffer.server, { command: 'nick', message: '', arguments: listargs.splice(1) })
},
description: '- List all channels on the current server.',
aliases: ['nickname']
@ -533,7 +549,7 @@ let commands = {
} else {
return handler.commandError(buffer, '/' + command.toUpperCase() + ': Invalid channel name!')
}
irc.socket.emit('userinput', { command: 'names', server: buffer.server, message: '', arguments: [channel] })
sendToServer(buffer.server, { command: 'names', message: '', arguments: [channel] })
},
description: '- List all users on the current channel.',
aliases: ['nicknames']
@ -541,9 +557,8 @@ let commands = {
quote: {
execute: function (buffer, handler, command, message, listargs) {
irc.socket.emit('userinput', {
sendToServer(buffer.server, {
command: listargs[1],
server: buffer.server,
message: listargs.slice(2).join(' '),
arguments: listargs.splice(2)
})
@ -558,7 +573,7 @@ let commands = {
return handler.commandError(buffer, listargs[0].toUpperCase() + ': Missing parameters!')
}
irc.socket.emit('userinput', { command: 'whois', server: buffer.server, message: '', arguments: [listargs[1]] })
sendToServer(buffer.server, { command: 'whois', message: '', arguments: [listargs[1]] })
},
description: '<nickname> - Display information about a user.'
},
@ -1269,6 +1284,147 @@ class Settings extends AppletChatBuffer {
}
}
class ConnectionHandler {
constructor () {
this.clients = {}
}
getClientByActualServer (actual) {
for (let i in this.clients) {
let a = this.clients[i]
if (a.data.actualServer === actual) return a
}
return null
}
createConnection (data) {
let client = new IRCConnection(data, gcfg, { ctcps: customCTCPs })
irc.auther.authMessage('Connecting to server...', false)
client.on('authenticated', () => {
irc.auther.authComplete()
this.clients[data.server] = client
this.bindAll(data.server, client)
irc.chat.newServerChatBuffer(client)
irc.chat.joinChannels(client.data.actualServer, data.channels)
})
client.connect().catch(e => {
console.error(e)
irc.auther.authMessage('Socket connection failed!', true)
})
}
bindAll (srv, client) {
let server = client.data.actualServer
client.on('closed', () => {
this.clients[srv] = null
})
client.on('pass_to_client', (data) => {
switch (data.type) {
case 'event_join_channel':
if (data.user.nickname === irc.serverData[server].my_nick) {
irc.chat.createChatBuffer(server, data.channel, 'channel', true)
}
irc.chat.handleJoin(server, data.user, data.channel)
break
case 'event_kick_channel':
irc.chat.handleLeave(server, data.kickee, data.channel, data.reason, data.user)
break
case 'event_part_channel':
irc.chat.handleLeave(server, data.user, data.channel, data.reason)
break
case 'event_quit':
irc.chat.handleQuit(server, data.user, data.reason)
break
case 'message':
if (data.to === irc.serverData[server].my_nick) {
irc.chat.messageChatBuffer(data.user.nickname, server, { message: data.message, type: data.messageType, from: data.user.nickname })
} else if (data.to == null) {
let atest = irc.chat.getActiveChatBuffer()
if (atest.server !== server) {
atest = irc.chat.getServerChatBuffer(server)
}
atest.addMessage(data.message, data.user.nickname, data.messageType)
} else {
irc.chat.messageChatBuffer(data.to, server, { message: data.message, type: data.messageType, from: data.user.nickname })
}
break
case 'channel_nicks':
irc.chat.buildNicklist(data.channel, server, data.nicks)
break
case 'channel_topic':
if (data['topic'] && data['set_by']) {
irc.chat.topicChange(data.channel, server, data.topic, data['set_by'])
} else if (data['topic']) {
irc.chat.topicChange(data.channel, server, data.topic, null)
} else if (data['set_by']) {
irc.chat.messageChatBuffer(data.channel, server, {
message: 'Topic set by ' + data.set_by + ' on ' + (new Date(data.time * 1000)),
type: 'topic',
from: null
})
}
break
case 'nick_change':
irc.chat.nickChange(server, data.nick, data.newNick)
break
case 'mode_add':
case 'mode_del':
case 'mode':
irc.chat.handleMode(server, data)
break
case 'server_message':
if (data['error']) data.messageType = 'error'
if (irc.chat.getServerChatBuffer(server) == null) {
if (!irc.serverChatQueue[server]) {
irc.serverChatQueue[server] = []
} else {
irc.serverChatQueue[server].push({ type: data.messageType, message: data.message, from: data.from || null, time: new Date() })
}
} else {
irc.chat.messageChatBuffer(server, server, { message: data.message, type: data.messageType, from: data.from || null })
}
break
case 'connect_message':
irc.auther.authMessage(data.message, data.error)
break
case 'whoisResponse':
whoisMessage(data.whois, irc.chat.getActiveChatBuffer())
break
case 'listedchan':
irc.chat.messageChatBuffer(server, server,
{
message: span(data.channel, ['channel']) + '&nbsp;' +
span(data.users, ['usercount']) + '&nbsp;' +
span(data.topic, ['topic']),
type: 'listentry',
from: data.from
})
break
}
})
client.on('closed', () => {
let serverz = irc.chat.getChatBuffersByServer(server)
for (let a in serverz) {
let serv = serverz[a]
serv.addMessage('You are no longer talking on this server.', null, 'error')
serv.setAliveStatus(false)
}
if (irc.serverData[server]) {
delete irc.serverData[server]
}
stopWarnings()
})
}
}
class IRCConnector {
constructor () {
this.formLocked = false
@ -1318,7 +1474,7 @@ class IRCConnector {
case 'nick':
case 'nickname':
if (validators.nickname(value)) {
clientdom.connector.nickname.value = value.replace(/\?/g, rand(10000, 99999))
clientdom.connector.nickname.value = value.replace(/\?/g, rand(seedrandom(1), 10000, 99999))
}
break
case 'secure':
@ -1438,7 +1594,7 @@ class IRCConnector {
return {
nickname: nickname,
autojoin: channel,
channels: channel,
server: server,
port: port,
password: password,
@ -1452,7 +1608,8 @@ class IRCConnector {
let data = this.getDataFromForm()
if (!data) return
irc.socket.emit('irc_create', data)
irc.handler.createConnection(data)
return true
}
@ -1633,7 +1790,7 @@ class InputHandler {
}
}
} else {
irc.socket.emit('userinput', { command: 'privmsg', server: buffer.server, message: message, arguments: [buffer.name] })
sendToServer(buffer.server, { command: 'privmsg', message: message, arguments: [buffer.name] })
}
this.history.push(message)
@ -1757,29 +1914,29 @@ class IRCChatWindow {
let prefixes = ''
for (let v in serverinfo.supportedModes) {
prefixes += serverinfo.supportedModes[v]
for (let v in serverinfo.data.supportedModes) {
prefixes += serverinfo.data.supportedModes[v]
}
irc.serverData[serverinfo.address] = {
modeTranslation: serverinfo.supportedModes,
irc.serverData[serverinfo.data.actualServer] = {
modeTranslation: serverinfo.data.supportedModes,
supportedPrefixes: prefixes,
network: serverinfo.network,
my_nick: serverinfo.nickname,
max_channel_length: serverinfo.max_channel_length
network: serverinfo.data.network,
my_nick: serverinfo.config.nickname,
max_channel_length: serverinfo.data.max_channel_length
}
let newServer = new ChatBuffer(serverinfo.address, serverinfo.address, serverinfo.network, 'server')
let newServer = new ChatBuffer(serverinfo.data.actualServer, serverinfo.data.actualServer, serverinfo.data.network, 'server')
this.buffers.push(newServer)
this.render(newServer)
this.firstServer = false
if (irc.serverChatQueue[serverinfo.address]) {
for (let a in irc.serverChatQueue[serverinfo.address]) {
let mesg = irc.serverChatQueue[serverinfo.address][a]
if (irc.serverChatQueue[serverinfo.data.actualServer]) {
for (let a in irc.serverChatQueue[serverinfo.data.actualServer]) {
let mesg = irc.serverChatQueue[serverinfo.data.actualServer][a]
newServer.addMessage(mesg.message, mesg.from, mesg.type, mesg.time)
}
delete irc.serverChatQueue[serverinfo.address]
delete irc.serverChatQueue[serverinfo.data.actualServer]
}
}
@ -1804,9 +1961,9 @@ class IRCChatWindow {
closeChatBuffer (buffer) {
if (buffer.type === 'server') {
irc.socket.emit('userinput', { command: 'quit', server: buffer.server, message: 'Server tab closed', arguments: [] })
sendToServer(buffer.server, { command: 'quit', message: 'Server tab closed', arguments: [] })
} else if (buffer.type === 'channel' && buffer.alive) {
irc.socket.emit('userinput', { command: 'part', server: buffer.server, message: 'Tab closed', arguments: [buffer.name] })
sendToServer(buffer.server, { command: 'part', message: 'Tab closed', arguments: [buffer.name] })
}
let bufIndex = this.buffers.indexOf(buffer)
@ -1971,12 +2128,12 @@ class IRCChatWindow {
}
}
handleMode (data) {
handleMode (server, data) {
let buf = null
if (data.target === irc.serverData[data.server].my_nick) {
buf = this.getServerChatBuffer(data.server)
if (data.target === irc.serverData[server].my_nick) {
buf = this.getServerChatBuffer(server)
} else {
buf = this.getChatBufferByServerName(data.server, data.target)
buf = this.getChatBufferByServerName(server, data.target)
}
if (!buf) return
@ -1996,29 +2153,31 @@ class IRCChatWindow {
}
joinChannels (server, channel) {
if (channel.indexOf(',') !== -1) {
channel = channel.trim().split(',')
if (typeof channel !== 'object') {
if (channel.indexOf(',') !== -1) {
channel = channel.trim().split(',')
for (let t in channel) {
let chan = channel[t]
for (let t in channel) {
let chan = channel[t]
channel[t] = chan.trim()
channel[t] = chan.trim()
if (chan.indexOf('#') !== 0) {
channel[t] = '#' + chan
if (chan.indexOf('#') !== 0) {
channel[t] = '#' + chan
}
}
} else if (channel !== '') {
channel = channel.trim()
if (channel.indexOf('#') !== 0) {
channel = '#' + channel
}
channel = [channel]
} else {
channel = []
}
} else if (channel !== '') {
channel = channel.trim()
if (channel.indexOf('#') !== 0) {
channel = '#' + channel
}
channel = [channel]
} else {
channel = []
}
irc.socket.emit('userinput', { command: 'join', server: server, message: '', arguments: channel })
sendToServer(server, { command: 'join', message: '', arguments: channel })
}
changeTitle (title) {
@ -2104,140 +2263,13 @@ window.onload = function () {
clientdom['smsctrig'] = clientdom.chat.querySelector('.smsc-nicklistbtn')
clientdom.settings['open'] = irc.primaryFrame.querySelector('.open_settings')
irc.socket = io.connect()
irc.settings = new Settings()
irc.handler = new ConnectionHandler()
irc.auther = new IRCConnector()
irc.chat = new IRCChatWindow()
irc.settings.setInitialValues()
irc.socket.on('defaults', function (data) {
irc.auther.defaultTo(data)
parseURL()
window.onpopstate = parseURL
})
irc.socket.on('connect', function (data) {
irc.socketUp = true
})
irc.socket.on('disconnect', function (data) {
irc.socketUp = false
irc.chat.destroyAllChatBuffers()
clientdom.connector.frame.style.display = 'block'
})
// Does everything
irc.socket.on('act_client', function (data) {
if (data['message']) {
data.message = validators.escapeHTML(data.message)
}
if (data['reason']) {
data.reason = validators.escapeHTML(data.reason)
}
switch (data.type) {
case 'event_connect':
irc.auther.authComplete()
irc.chat.newServerChatBuffer(data)
break
case 'event_join_channel':
if (data.user.nickname === irc.serverData[data.server].my_nick) {
irc.chat.createChatBuffer(data.server, data.channel, 'channel', true)
}
irc.chat.handleJoin(data.server, data.user, data.channel)
break
case 'event_kick_channel':
irc.chat.handleLeave(data.server, data.kickee, data.channel, data.reason, data.user)
break
case 'event_part_channel':
irc.chat.handleLeave(data.server, data.user, data.channel, data.reason)
break
case 'event_quit':
irc.chat.handleQuit(data.server, data.user, data.reason)
break
case 'event_server_quit':
let serverz = irc.chat.getChatBuffersByServer(data.server)
for (let a in serverz) {
let serv = serverz[a]
serv.addMessage('You are no longer talking on this server.', null, 'error')
serv.setAliveStatus(false)
}
if (irc.serverData[data.server]) {
delete irc.serverData[data.server]
}
stopWarnings()
break
case 'message':
if (data.to === irc.serverData[data.server].my_nick) {
irc.chat.messageChatBuffer(data.user.nickname, data.server, { message: data.message, type: data.messageType, from: data.user.nickname })
} else if (data.to == null) {
let atest = irc.chat.getActiveChatBuffer()
if (atest.server !== data.server) {
atest = irc.chat.getServerChatBuffer(data.server)
}
atest.addMessage(data.message, data.user.nickname, data.messageType)
} else {
irc.chat.messageChatBuffer(data.to, data.server, { message: data.message, type: data.messageType, from: data.user.nickname })
}
break
case 'channel_nicks':
irc.chat.buildNicklist(data.channel, data.server, data.nicks)
break
case 'channel_topic':
if (data['topic'] && data['set_by']) {
irc.chat.topicChange(data.channel, data.server, data.topic, data['set_by'])
} else if (data['topic']) {
irc.chat.topicChange(data.channel, data.server, data.topic, null)
} else if (data['set_by']) {
irc.chat.messageChatBuffer(data.channel, data.server, {
message: 'Topic set by ' + data.set_by + ' on ' + (new Date(data.time * 1000)),
type: 'topic',
from: null
})
}
break
case 'nick_change':
irc.chat.nickChange(data.server, data.nick, data.newNick)
break
case 'mode_add':
case 'mode_del':
case 'mode':
irc.chat.handleMode(data)
break
case 'server_message':
if (data['error']) data.messageType = 'error'
if (irc.chat.getServerChatBuffer(data.server) == null) {
if (!irc.serverChatQueue[data.server]) {
irc.serverChatQueue[data.server] = []
} else {
irc.serverChatQueue[data.server].push({ type: data.messageType, message: data.message, from: data.from || null, time: new Date() })
}
} else {
irc.chat.messageChatBuffer(data.server, data.server, { message: data.message, type: data.messageType, from: data.from || null })
}
break
case 'connect_message':
irc.auther.authMessage(data.message, data.error)
break
case 'whoisResponse':
whoisMessage(data.whois, irc.chat.getActiveChatBuffer())
break
case 'listedchan':
irc.chat.messageChatBuffer(data.server, data.server,
{
message: span(data.channel, ['channel']) + '&nbsp;' +
span(data.users, ['usercount']) + '&nbsp;' +
span(data.topic, ['topic']),
type: 'listentry',
from: data.from
})
break
}
})
parseURL()
window.onpopstate = parseURL
}

73
src/js/previews.js Normal file
View File

@ -0,0 +1,73 @@
const imgs = ['.png', '.jpg', '.jpeg', '.svg']
const embeds = [
{
match: /youtu.?be\//is,
exec: function (pgurl) {
let dat = pgurl.match(/(?:be|com)\/([^?&#]+)/i)
if (!dat) {
dat = pgurl.match('[\\?&]v=([^&#]*)')
}
if (!dat) return null
return 'https://www.youtube.com/embed/' + dat[1] + '?autoplay=1'
}
}
]
export function handleUrlElement (elem) {
let cover = null
let ext = elem.href.split('.')
ext = ext[ext.length - 1]
if (ext && imgs.indexOf(ext) !== -1) {
cover = document.createElement('img')
cover.src = elem.href
}
if (!cover) {
for (let a in embeds) {
if (cover) break
let fn = embeds[a]
if (elem.href.match(fn.match)) {
let r = fn.exec(elem.href)
if (r) {
cover = document.createElement('iframe')
cover.src = r
}
}
}
}
if (cover) {
cover.className = 'preview'
let contelem = document.createElement('span')
contelem.className = 'preview-box'
let contbutton = document.createElement('button')
contbutton.className = 'preview-btn'
contbutton.innerHTML = 'Show Preview'
elem.parentNode.appendChild(contelem)
contelem.appendChild(elem)
contelem.appendChild(contbutton)
contbutton.addEventListener('click', function (e) {
e.preventDefault()
if (cover.parentNode) {
cover.remove()
contbutton.innerHTML = 'Show Preview'
} else {
contelem.appendChild(cover)
contbutton.innerHTML = 'Hide Preview'
}
}, false)
return contelem
}
return elem
}

View File

@ -1,15 +0,0 @@
import fs from 'fs'
import path from 'path'
import toml from 'toml'
const filename = path.join(__dirname, '..', 'client.config.toml')
let config
try {
config = toml.parse(fs.readFileSync(filename))
} catch (e) {
throw new Error('config.toml parse error: ', e.message)
}
module.exports = config

View File

@ -1,14 +1,8 @@
'use strict'
import express from 'express'
import path from 'path'
import sockio from 'socket.io'
import dns from 'dns'
import pkginfo from '../package.json'
import irclib from './irc'
import webirc from './webirc'
import config from './config'
import config from '../globals'
import logger from './logger'
const app = express()
@ -17,21 +11,6 @@ const pubdir = path.join(__dirname, 'public')
const port = config.server.port || 8080
const cacheAge = 365 * 24 * 60 * 60 * 1000
let runtimeStats = {
connectionsMade: 0
}
let connections = {}
let customCTCPs = {
VERSION: function (data, connection) {
return `TeemantIRC ver. ${pkginfo.version} - ${pkginfo.description} - https://teemant.icynet.eu/`
},
SOURCE: function (data, connection) {
return 'https://gitlab.icynet.eu/IcyNetwork/teemant'
}
}
process.stdin.resume()
router.get('/', function (req, res) {
@ -51,186 +30,6 @@ app.use('/:server', express.static(path.join(pubdir, 'icons'), { maxAge: cacheAg
app.use('/:server', express.static(path.join(pubdir, 'static'), { maxAge: cacheAge }))
app.use('/', router)
const io = sockio.listen(app.listen(port, function () {
app.listen(port, function () {
logger.log(`*** Listening on http://localhost:${port}/`)
setInterval(() => {
logger.printRuntimeStats(runtimeStats, connections)
}, 3600000)
}))
function resolveHostname (ipaddr) {
return new Promise(function (resolve, reject) {
dns.reverse(ipaddr, function (err, hostnames) {
if (err != null) return reject(err)
resolve(hostnames)
})
})
}
io.sockets.on('connection', function (socket) {
let userip = socket.handshake.headers['x-real-ip'] || socket.handshake.headers['x-forwarded-for'] ||
socket.request.connection._peername.address || '127.0.0.1'
if (userip.indexOf('::ffff:') === 0) {
userip = userip.substring(7)
}
logger.debugLog(`clientID: ${socket.id} from: ${userip}`)
socket.emit('defaults', {
server: config.client.default_server,
port: config.client.default_port,
ssl: config.client.secure_by_default
})
// New object for connections
connections[socket.id] = {
host: {
ipaddr: userip,
hostname: userip
}
}
// Get the hostname of the connecting user
let hostQuery = resolveHostname(userip)
hostQuery.then((arr) => {
if (arr.length > 0) {
connections[socket.id].host.hostname = arr[0]
}
logger.debugLog(`Hostname of ${socket.id} was determined to be ${connections[socket.id].host.hostname}`)
}).catch((err) => {
logger.debugLog(`Host resolve for ${socket.id} failed:`, err.message)
})
socket.on('disconnect', function () {
for (let d in connections[socket.id]) {
if (connections[socket.id][d].ipaddr) continue
if (connections[socket.id][d].connected === true) {
connections[socket.id][d].disconnect()
}
}
delete connections[socket.id]
logger.debugLog(`clientID: ${socket.id} disconnect`)
})
socket.on('error', (e) => {
logger.errorLog(e, 'Socket error')
})
socket.on('userinput', (data) => {
let serv = connections[socket.id][data.server]
if (!serv) return
if (serv.authenticated === false) return
logger.debugLog(`[${socket.id}] ->`, data)
try {
serv.handler.handleUserLine(data)
} catch (e) {
console.error('An error occured while handling message from client', data)
console.error(e.stack)
}
})
socket.on('irc_create', function (connectiondata) {
logger.debugLog(`${socket.id} created irc connection: `, connectiondata)
socket.emit('act_client', { type: 'connect_message', message: 'Connecting to server..', error: false })
if (connectiondata.port === null || connectiondata.server === null) {
return socket.emit('act_client', {
type: 'connect_message',
server: connectiondata.server,
message: 'Failed to connect to the server!',
error: true
})
}
let newConnection = new irclib.IRCConnection(connectiondata, config.client,
{
authenticationSteps: [
new webirc.Authenticator(connections[socket.id].host)
],
ctcps: customCTCPs
})
newConnection.connect()
connections[socket.id][connectiondata.server] = newConnection
newConnection.on('authenticated', () => {
socket.emit('act_client', {
type: 'event_connect',
address: connectiondata.server,
network: newConnection.data.network,
supportedModes: newConnection.data.supportedModes,
nickname: newConnection.config.nickname,
max_channel_length: newConnection.data.max_channel_length
})
runtimeStats.connectionsMade += 1
})
if (config.server.debug) {
newConnection.on('line', function (line) {
logger.debugLog(`[${socket.id}] <-`, line)
})
newConnection.on('debug_log', function (data) {
logger.debugLog(`[${socket.id}] <-`, data)
})
}
newConnection.on('connerror', (data) => {
logger.debugLog(data)
if (newConnection.authenticated === false) {
socket.emit('act_client', {
type: 'connect_message',
server: connectiondata.server,
message: 'Failed to connect to the server!',
error: true
})
}
})
newConnection.on('pass_to_client', (data) => {
socket.emit('act_client', data)
})
newConnection.on('closed', (data) => {
logger.debugLog(data)
if (newConnection.authenticated === false) {
socket.emit('act_client', {
type: 'connect_message',
server: connectiondata.server,
message: 'Failed to connect to the server!',
error: true
})
} else {
socket.emit('act_client', {
type: 'event_server_quit',
server: connectiondata.server
})
}
})
})
})
process.on('SIGINT', () => {
logger.log('!!! Received SIGINT; Terminating all IRC connections and exiting.. !!!')
logger.printRuntimeStats(runtimeStats, connections)
for (let c in connections) {
for (let ircconn in connections[c]) {
if (connections[c][ircconn].ipaddr) continue
connections[c][ircconn].disconnect()
}
}
process.exit(0)
})

View File

@ -1,7 +0,0 @@
import connector from './irc.js'
import parser from './parser.js'
module.exports = {
IRCConnection: connector,
Parser: parser
}

View File

@ -3,42 +3,48 @@
const webpack = require('webpack')
const path = require('path')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const glb = require(path.join(__dirname, 'globals'))
module.exports = {
entry: {
main: './src/js/main'
},
output: {
path: path.join(__dirname, 'app', 'public'),
filename: '[name].js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: [ '@babel/preset-env' ]
module.exports = (env, options) => {
return {
entry: {
main: './src/js/main'
},
output: {
path: path.join(__dirname, 'app', 'public'),
filename: '[name].js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: [ '@babel/preset-env' ]
}
}
}
}
]
},
plugins: [
new webpack.ProvidePlugin({
// Detect and inject
_: 'underscore'
}),
new CopyWebpackPlugin([{
from: 'src/document/index.html',
to: '.'
]
},
{
from: 'static',
to: 'static'
}])
],
devtool: 'inline-source-map'
plugins: [
new webpack.ProvidePlugin({
// Detect and inject
_: 'underscore'
}),
new HtmlWebpackPlugin({
template: 'src/document/index.html'
}),
new CopyWebpackPlugin([{
from: 'static',
to: 'static'
}]),
new webpack.DefinePlugin({
globalConfig: JSON.stringify(glb.client)
})
],
devtool: (options.mode === 'development' ? 'inline-source-map' : undefined)
}
}