mirror of
https://github.com/thelounge/thelounge.git
synced 2024-06-01 05:12:24 +02:00
ts progress
This commit is contained in:
parent
2e3d9a6265
commit
01d640db47
81
.eslintrc.yml
Normal file
81
.eslintrc.yml
Normal file
|
@ -0,0 +1,81 @@
|
|||
---
|
||||
root: true
|
||||
|
||||
parserOptions:
|
||||
ecmaVersion: 2022
|
||||
parser: "babel-eslint"
|
||||
|
||||
env:
|
||||
es6: true
|
||||
browser: true
|
||||
mocha: true
|
||||
node: true
|
||||
|
||||
rules:
|
||||
block-scoped-var: error
|
||||
curly: [error, all]
|
||||
dot-notation: error
|
||||
eqeqeq: error
|
||||
handle-callback-err: error
|
||||
no-alert: error
|
||||
no-catch-shadow: error
|
||||
no-control-regex: off
|
||||
no-console: error
|
||||
no-duplicate-imports: error
|
||||
no-else-return: error
|
||||
no-implicit-globals: error
|
||||
no-restricted-globals:
|
||||
- error
|
||||
- event
|
||||
- fdescribe
|
||||
no-shadow: error
|
||||
no-template-curly-in-string: error
|
||||
no-unsafe-negation: error
|
||||
no-useless-computed-key: error
|
||||
no-useless-constructor: error
|
||||
no-useless-return: error
|
||||
no-use-before-define:
|
||||
- error
|
||||
- functions: false
|
||||
no-var: error
|
||||
object-shorthand:
|
||||
- error
|
||||
- methods
|
||||
- avoidExplicitReturnArrows: true
|
||||
padding-line-between-statements:
|
||||
- error
|
||||
- blankLine: always
|
||||
prev:
|
||||
- block
|
||||
- block-like
|
||||
next: "*"
|
||||
- blankLine: always
|
||||
prev: "*"
|
||||
next:
|
||||
- block
|
||||
- block-like
|
||||
prefer-const: error
|
||||
prefer-rest-params: error
|
||||
prefer-spread: error
|
||||
spaced-comment: [error, always]
|
||||
strict: off
|
||||
yoda: error
|
||||
vue/component-tags-order:
|
||||
- error
|
||||
- order:
|
||||
- template
|
||||
- style
|
||||
- script
|
||||
vue/no-mutating-props: off
|
||||
vue/no-v-html: off
|
||||
vue/require-default-prop: off
|
||||
vue/v-slot-style: [error, longform]
|
||||
vue/multi-word-component-names: off
|
||||
|
||||
plugins:
|
||||
- vue
|
||||
|
||||
extends:
|
||||
- eslint:recommended
|
||||
- plugin:vue/recommended
|
||||
- prettier
|
|
@ -16,21 +16,30 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const constants = require("../js/constants");
|
||||
import eventbus from "../js/eventbus";
|
||||
<script lang="ts">
|
||||
import constants from "@/js/constants";
|
||||
import eventbus from "@/js/eventbus";
|
||||
import Mousetrap from "mousetrap";
|
||||
import throttle from "lodash/throttle";
|
||||
import storage from "../js/localStorage";
|
||||
import isIgnoredKeybind from "../js/helpers/isIgnoredKeybind";
|
||||
import storage from "@/js/localStorage";
|
||||
import isIgnoredKeybind from "@/js/helpers/isIgnoredKeybind";
|
||||
|
||||
import Sidebar from "./Sidebar.vue";
|
||||
import ImageViewer from "./ImageViewer.vue";
|
||||
import ContextMenu from "./ContextMenu.vue";
|
||||
import ConfirmDialog from "./ConfirmDialog.vue";
|
||||
import Mentions from "./Mentions.vue";
|
||||
import Vue from "vue";
|
||||
|
||||
export default {
|
||||
// This stops Vue from complaining about adding objects to the component context
|
||||
declare module "vue/types/vue" {
|
||||
interface Vue {
|
||||
debouncedResize: () => void;
|
||||
dayChangeTimeout: number;
|
||||
}
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "App",
|
||||
components: {
|
||||
Sidebar,
|
||||
|
@ -40,7 +49,7 @@ export default {
|
|||
Mentions,
|
||||
},
|
||||
computed: {
|
||||
viewportClasses() {
|
||||
viewportClasses(): Object {
|
||||
return {
|
||||
notified: this.$store.getters.highlightCount > 0,
|
||||
"menu-open": this.$store.state.appLoaded && this.$store.state.sidebarOpen,
|
||||
|
@ -58,7 +67,6 @@ export default {
|
|||
Mousetrap.bind("alt+s", this.toggleSidebar);
|
||||
Mousetrap.bind("alt+m", this.toggleMentions);
|
||||
|
||||
// Make a single throttled resize listener available to all components
|
||||
this.debouncedResize = throttle(() => {
|
||||
eventbus.emit("resize");
|
||||
}, 100);
|
||||
|
@ -75,19 +83,19 @@ export default {
|
|||
this.dayChangeTimeout = setTimeout(emitDayChange, this.msUntilNextDay());
|
||||
},
|
||||
beforeDestroy() {
|
||||
Mousetrap.unbind("esc", this.escapeKey);
|
||||
Mousetrap.unbind("alt+u", this.toggleUserList);
|
||||
Mousetrap.unbind("alt+s", this.toggleSidebar);
|
||||
Mousetrap.unbind("alt+m", this.toggleMentions);
|
||||
Mousetrap.unbind("esc");
|
||||
Mousetrap.unbind("alt+u");
|
||||
Mousetrap.unbind("alt+s");
|
||||
Mousetrap.unbind("alt+m");
|
||||
|
||||
window.removeEventListener("resize", this.debouncedResize);
|
||||
clearTimeout(this.dayChangeTimeout);
|
||||
},
|
||||
methods: {
|
||||
escapeKey() {
|
||||
escapeKey(): void {
|
||||
eventbus.emit("escapekey");
|
||||
},
|
||||
toggleSidebar(e) {
|
||||
toggleSidebar(e): boolean {
|
||||
if (isIgnoredKeybind(e)) {
|
||||
return true;
|
||||
}
|
||||
|
@ -96,7 +104,7 @@ export default {
|
|||
|
||||
return false;
|
||||
},
|
||||
toggleUserList(e) {
|
||||
toggleUserList(e): boolean {
|
||||
if (isIgnoredKeybind(e)) {
|
||||
return true;
|
||||
}
|
||||
|
@ -105,19 +113,23 @@ export default {
|
|||
|
||||
return false;
|
||||
},
|
||||
toggleMentions() {
|
||||
toggleMentions(): void {
|
||||
if (this.$store.state.networks.length !== 0) {
|
||||
eventbus.emit("mentions:toggle");
|
||||
}
|
||||
},
|
||||
msUntilNextDay() {
|
||||
msUntilNextDay(): number {
|
||||
// Compute how many milliseconds are remaining until the next day starts
|
||||
const today = new Date();
|
||||
const tommorow = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
const tommorow = new Date(
|
||||
today.getFullYear(),
|
||||
today.getMonth(),
|
||||
today.getDate() + 1
|
||||
).getTime();
|
||||
|
||||
return tommorow - today;
|
||||
return tommorow - today.getTime();
|
||||
},
|
||||
prepareOpenStates() {
|
||||
prepareOpenStates(): void {
|
||||
const viewportWidth = window.innerWidth;
|
||||
let isUserlistOpen = storage.get("thelounge.state.userlist");
|
||||
|
||||
|
@ -137,5 +149,5 @@ export default {
|
|||
this.$store.commit("userlistOpen", isUserlistOpen === "true");
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -28,8 +28,7 @@ const timeFormats = {
|
|||
msg12hWithSeconds: "hh:mm:ss A",
|
||||
};
|
||||
|
||||
// This file is required by server, can't use es6 export
|
||||
module.exports = {
|
||||
export default {
|
||||
colorCodeMap,
|
||||
commands: [],
|
||||
condensedTypes,
|
||||
|
|
|
@ -176,7 +176,7 @@ export function generateChannelContextMenu($root, channel, network) {
|
|||
query: "conversation",
|
||||
};
|
||||
|
||||
// We don't allow the muting of Chan.Type.SPECIAL channels
|
||||
// We don't allow the muting of ChanType.SPECIAL channels
|
||||
const mutableChanTypes = Object.keys(humanFriendlyChanTypeMap);
|
||||
|
||||
if (mutableChanTypes.includes(channel.type)) {
|
||||
|
|
|
@ -11,14 +11,14 @@
|
|||
// https://www.chromium.org/for-testers/bug-reporting-guidelines/uncaught-securityerror-failed-to-read-the-localstorage-property-from-window-access-is-denied-for-this-document
|
||||
|
||||
export default {
|
||||
set(key, value) {
|
||||
set(key: string, value: string) {
|
||||
try {
|
||||
window.localStorage.setItem(key, value);
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
},
|
||||
get(key) {
|
||||
get(key: string) {
|
||||
try {
|
||||
return window.localStorage.getItem(key);
|
||||
} catch (e) {
|
||||
|
@ -26,7 +26,7 @@ export default {
|
|||
return null;
|
||||
}
|
||||
},
|
||||
remove(key) {
|
||||
remove(key: string) {
|
||||
try {
|
||||
window.localStorage.removeItem(key);
|
||||
} catch (e) {
|
|
@ -15,9 +15,11 @@ import "./socket-events";
|
|||
import "./webpush";
|
||||
import "./keybinds";
|
||||
|
||||
import type {Channel} from "@/backend/models/channel";
|
||||
|
||||
const favicon = document.getElementById("favicon");
|
||||
const faviconNormal = favicon.getAttribute("href");
|
||||
const faviconAlerted = favicon.dataset.other;
|
||||
const faviconNormal = favicon?.getAttribute("href") || "";
|
||||
const faviconAlerted = favicon?.dataset.other || "";
|
||||
|
||||
new Vue({
|
||||
el: "#viewport",
|
||||
|
@ -26,10 +28,10 @@ new Vue({
|
|||
socket.open();
|
||||
},
|
||||
methods: {
|
||||
switchToChannel(channel) {
|
||||
switchToChannel(channel: Channel) {
|
||||
navigate("RoutedChat", {id: channel.id});
|
||||
},
|
||||
closeChannel(channel) {
|
||||
closeChannel(channel: Channel) {
|
||||
if (channel.type === "lobby") {
|
||||
eventbus.emit(
|
||||
"confirm-dialog",
|
||||
|
@ -38,7 +40,7 @@ new Vue({
|
|||
text: `Are you sure you want to quit and remove ${channel.name}? This cannot be undone.`,
|
||||
button: "Remove network",
|
||||
},
|
||||
(result) => {
|
||||
(result: boolean) => {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
@ -75,7 +77,7 @@ store.watch(
|
|||
(state) => state.sidebarOpen,
|
||||
(sidebarOpen) => {
|
||||
if (window.innerWidth > constants.mobileViewportPixels) {
|
||||
storage.set("thelounge.state.sidebar", sidebarOpen);
|
||||
storage.set("thelounge.state.sidebar", sidebarOpen.toString());
|
||||
eventbus.emit("resize");
|
||||
}
|
||||
}
|
||||
|
@ -84,7 +86,7 @@ store.watch(
|
|||
store.watch(
|
||||
(state) => state.userlistOpen,
|
||||
(userlistOpen) => {
|
||||
storage.set("thelounge.state.userlist", userlistOpen);
|
||||
storage.set("thelounge.state.userlist", userlistOpen.toString());
|
||||
eventbus.emit("resize");
|
||||
}
|
||||
);
|
||||
|
@ -100,13 +102,15 @@ store.watch(
|
|||
store.watch(
|
||||
(_, getters) => getters.highlightCount,
|
||||
(highlightCount) => {
|
||||
favicon.setAttribute("href", highlightCount > 0 ? faviconAlerted : faviconNormal);
|
||||
favicon?.setAttribute("href", highlightCount > 0 ? faviconAlerted : faviconNormal);
|
||||
|
||||
if (navigator.setAppBadge) {
|
||||
// TODO: investigate types
|
||||
const nav = navigate as any;
|
||||
if (nav.setAppBadge) {
|
||||
if (highlightCount > 0) {
|
||||
navigator.setAppBadge(highlightCount);
|
||||
nav.setAppBadge(highlightCount);
|
||||
} else {
|
||||
navigator.clearAppBadge();
|
||||
nav.clearAppBadge();
|
||||
}
|
||||
}
|
||||
}
|
28
client/tsconfig.json
Normal file
28
client/tsconfig.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
// https://v2.vuejs.org/v2/guide/typescript.html?redirect=true#Recommended-Configuration
|
||||
// this aligns with Vue's browser support
|
||||
"target": "es5",
|
||||
// this enables stricter inference for data properties on `this`
|
||||
"strict": true,
|
||||
// if using webpack 2+ or rollup, to leverage tree shaking:
|
||||
"module": "es2015",
|
||||
"moduleResolution": "node",
|
||||
"lib": ["es2019", "dom"],
|
||||
"sourceMap": true,
|
||||
"rootDir": "./",
|
||||
"outDir": "./dist",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/js/*": ["./js/*"],
|
||||
"@/css/*": ["./css/*"],
|
||||
"@/img/*": ["./img/*"],
|
||||
"@/components/*": ["./components/*"],
|
||||
"@/backend/*": ["../src/types/*"]
|
||||
},
|
||||
"jsx": "preserve"
|
||||
}
|
||||
}
|
4
client/types.d.ts
vendored
Normal file
4
client/types.d.ts
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
declare module "*.vue" {
|
||||
import Vue from "vue";
|
||||
export default Vue;
|
||||
}
|
17
package.json
17
package.json
|
@ -14,7 +14,7 @@
|
|||
"scripts": {
|
||||
"build": "webpack",
|
||||
"coverage": "run-s test:* && nyc --nycrc-path=test/.nycrc-report.json report",
|
||||
"dev": "node index start --dev",
|
||||
"dev": "ts-node index start --dev",
|
||||
"format:prettier": "prettier --write \"**/*.*\"",
|
||||
"lint:check-eslint": "eslint-config-prettier .eslintrc.cjs",
|
||||
"lint:eslint": "eslint . --ext .js,.vue --report-unused-disable-directives --color",
|
||||
|
@ -41,6 +41,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@fastify/busboy": "1.0.0",
|
||||
"@types/ldapjs": "2.2.2",
|
||||
"bcryptjs": "2.4.3",
|
||||
"chalk": "4.1.2",
|
||||
"cheerio": "1.0.0-rc.10",
|
||||
|
@ -74,13 +75,23 @@
|
|||
"devDependencies": {
|
||||
"@babel/core": "7.17.10",
|
||||
"@babel/preset-env": "7.17.10",
|
||||
"@babel/preset-typescript": "7.16.7",
|
||||
"@fortawesome/fontawesome-free": "5.15.4",
|
||||
"@textcomplete/core": "0.1.11",
|
||||
"@textcomplete/textarea": "0.1.10",
|
||||
"@types/express": "4.17.13",
|
||||
"@types/lodash": "4.14.182",
|
||||
"@types/mousetrap": "1.6.9",
|
||||
"@types/sqlite3": "3.1.8",
|
||||
"@types/ua-parser-js": "0.7.36",
|
||||
"@types/uuid": "8.3.4",
|
||||
"@types/ws": "8.5.3",
|
||||
"@vue/runtime-dom": "3.2.33",
|
||||
"@vue/server-test-utils": "1.3.0",
|
||||
"@vue/test-utils": "1.3.0",
|
||||
"babel-loader": "8.2.5",
|
||||
"babel-plugin-istanbul": "6.1.1",
|
||||
"babel-preset-typescript-vue": "1.1.1",
|
||||
"chai": "4.3.6",
|
||||
"copy-webpack-plugin": "10.2.4",
|
||||
"css-loader": "6.5.1",
|
||||
|
@ -109,6 +120,10 @@
|
|||
"socket.io-client": "4.4.1",
|
||||
"stylelint": "14.3.0",
|
||||
"stylelint-config-standard": "24.0.0",
|
||||
"ts-loader": "9.3.0",
|
||||
"ts-node": "10.7.0",
|
||||
"tsconfig-paths-webpack-plugin": "3.5.2",
|
||||
"typescript": "4.6.4",
|
||||
"undate": "0.3.0",
|
||||
"vue": "2.6.14",
|
||||
"vue-loader": "15.9.8",
|
||||
|
|
771
src/client.js
771
src/client.js
|
@ -1,771 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
const _ = require("lodash");
|
||||
const log = require("./log");
|
||||
const colors = require("chalk");
|
||||
const Chan = require("./models/chan");
|
||||
const crypto = require("crypto");
|
||||
const Msg = require("./models/msg");
|
||||
const Network = require("./models/network");
|
||||
const Config = require("./config");
|
||||
const UAParser = require("ua-parser-js");
|
||||
const {v4: uuidv4} = require("uuid");
|
||||
const escapeRegExp = require("lodash/escapeRegExp");
|
||||
const constants = require("../client/js/constants.js");
|
||||
const inputs = require("./plugins/inputs");
|
||||
const PublicClient = require("./plugins/packages/publicClient");
|
||||
|
||||
const MessageStorage = require("./plugins/messageStorage/sqlite");
|
||||
const TextFileMessageStorage = require("./plugins/messageStorage/text");
|
||||
|
||||
module.exports = Client;
|
||||
|
||||
const events = [
|
||||
"away",
|
||||
"cap",
|
||||
"connection",
|
||||
"unhandled",
|
||||
"ctcp",
|
||||
"chghost",
|
||||
"error",
|
||||
"help",
|
||||
"info",
|
||||
"invite",
|
||||
"join",
|
||||
"kick",
|
||||
"list",
|
||||
"mode",
|
||||
"modelist",
|
||||
"motd",
|
||||
"message",
|
||||
"names",
|
||||
"nick",
|
||||
"part",
|
||||
"quit",
|
||||
"sasl",
|
||||
"topic",
|
||||
"welcome",
|
||||
"whois",
|
||||
];
|
||||
|
||||
function Client(manager, name, config = {}) {
|
||||
_.merge(this, {
|
||||
awayMessage: "",
|
||||
lastActiveChannel: -1,
|
||||
attachedClients: {},
|
||||
config: config,
|
||||
id: uuidv4(),
|
||||
idChan: 1,
|
||||
idMsg: 1,
|
||||
name: name,
|
||||
networks: [],
|
||||
mentions: [],
|
||||
manager: manager,
|
||||
messageStorage: [],
|
||||
highlightRegex: null,
|
||||
highlightExceptionRegex: null,
|
||||
messageProvider: undefined,
|
||||
});
|
||||
|
||||
const client = this;
|
||||
|
||||
client.config.log = Boolean(client.config.log);
|
||||
client.config.password = String(client.config.password);
|
||||
|
||||
if (!Config.values.public && client.config.log) {
|
||||
if (Config.values.messageStorage.includes("sqlite")) {
|
||||
client.messageProvider = new MessageStorage(client);
|
||||
client.messageStorage.push(client.messageProvider);
|
||||
}
|
||||
|
||||
if (Config.values.messageStorage.includes("text")) {
|
||||
client.messageStorage.push(new TextFileMessageStorage(client));
|
||||
}
|
||||
|
||||
for (const messageStorage of client.messageStorage) {
|
||||
messageStorage.enable();
|
||||
}
|
||||
}
|
||||
|
||||
if (!_.isPlainObject(client.config.sessions)) {
|
||||
client.config.sessions = {};
|
||||
}
|
||||
|
||||
if (!_.isPlainObject(client.config.clientSettings)) {
|
||||
client.config.clientSettings = {};
|
||||
}
|
||||
|
||||
if (!_.isPlainObject(client.config.browser)) {
|
||||
client.config.browser = {};
|
||||
}
|
||||
|
||||
// TODO: Backwards compatibility with older versions, remove in a future release?
|
||||
if (client.config.awayMessage) {
|
||||
client.config.clientSettings.awayMessage = client.config.awayMessage;
|
||||
delete client.config.awayMessage;
|
||||
}
|
||||
|
||||
if (client.config.clientSettings.awayMessage) {
|
||||
client.awayMessage = client.config.clientSettings.awayMessage;
|
||||
}
|
||||
|
||||
client.config.clientSettings.searchEnabled = client.messageProvider !== undefined;
|
||||
|
||||
client.compileCustomHighlights();
|
||||
|
||||
_.forOwn(client.config.sessions, (session) => {
|
||||
if (session.pushSubscription) {
|
||||
this.registerPushSubscription(session, session.pushSubscription, true);
|
||||
}
|
||||
});
|
||||
|
||||
(client.config.networks || []).forEach((network) => client.connect(network, true));
|
||||
|
||||
// Networks are stored directly in the client object
|
||||
// We don't need to keep it in the config object
|
||||
delete client.config.networks;
|
||||
|
||||
if (client.name) {
|
||||
log.info(`User ${colors.bold(client.name)} loaded`);
|
||||
|
||||
// Networks are created instantly, but to reduce server load on startup
|
||||
// We randomize the IRC connections and channel log loading
|
||||
let delay = manager.clients.length * 500;
|
||||
client.networks.forEach((network) => {
|
||||
setTimeout(() => {
|
||||
network.channels.forEach((channel) => channel.loadMessages(client, network));
|
||||
|
||||
if (!network.userDisconnected && network.irc) {
|
||||
network.irc.connect();
|
||||
}
|
||||
}, delay);
|
||||
|
||||
delay += 1000 + Math.floor(Math.random() * 1000);
|
||||
});
|
||||
|
||||
client.fileHash = manager.getDataToSave(client).newHash;
|
||||
}
|
||||
}
|
||||
|
||||
Client.prototype.createChannel = function (attr) {
|
||||
const chan = new Chan(attr);
|
||||
chan.id = this.idChan++;
|
||||
|
||||
return chan;
|
||||
};
|
||||
|
||||
Client.prototype.emit = function (event, data) {
|
||||
if (this.manager !== null) {
|
||||
this.manager.sockets.in(this.id).emit(event, data);
|
||||
}
|
||||
};
|
||||
|
||||
Client.prototype.find = function (channelId) {
|
||||
let network = null;
|
||||
let chan = null;
|
||||
|
||||
for (const i in this.networks) {
|
||||
const n = this.networks[i];
|
||||
chan = _.find(n.channels, {id: channelId});
|
||||
|
||||
if (chan) {
|
||||
network = n;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (network && chan) {
|
||||
return {network, chan};
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
Client.prototype.connect = function (args, isStartup = false) {
|
||||
const client = this;
|
||||
let channels = [];
|
||||
|
||||
// Get channel id for lobby before creating other channels for nicer ids
|
||||
const lobbyChannelId = client.idChan++;
|
||||
|
||||
if (args.channels) {
|
||||
let badName = false;
|
||||
|
||||
args.channels.forEach((chan) => {
|
||||
if (!chan.name) {
|
||||
badName = true;
|
||||
return;
|
||||
}
|
||||
|
||||
channels.push(
|
||||
client.createChannel({
|
||||
name: chan.name,
|
||||
key: chan.key || "",
|
||||
type: chan.type,
|
||||
muted: chan.muted,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
if (badName && client.name) {
|
||||
log.warn(
|
||||
"User '" +
|
||||
client.name +
|
||||
"' on network '" +
|
||||
args.name +
|
||||
"' has an invalid channel which has been ignored"
|
||||
);
|
||||
}
|
||||
// `join` is kept for backwards compatibility when updating from versions <2.0
|
||||
// also used by the "connect" window
|
||||
} else if (args.join) {
|
||||
channels = args.join
|
||||
.replace(/,/g, " ")
|
||||
.split(/\s+/g)
|
||||
.map((chan) => {
|
||||
if (!chan.match(/^[#&!+]/)) {
|
||||
chan = `#${chan}`;
|
||||
}
|
||||
|
||||
return client.createChannel({
|
||||
name: chan,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const network = new Network({
|
||||
uuid: args.uuid,
|
||||
name: String(
|
||||
args.name || (Config.values.lockNetwork ? Config.values.defaults.name : "") || ""
|
||||
),
|
||||
host: String(args.host || ""),
|
||||
port: parseInt(args.port, 10),
|
||||
tls: !!args.tls,
|
||||
userDisconnected: !!args.userDisconnected,
|
||||
rejectUnauthorized: !!args.rejectUnauthorized,
|
||||
password: String(args.password || ""),
|
||||
nick: String(args.nick || ""),
|
||||
username: String(args.username || ""),
|
||||
realname: String(args.realname || ""),
|
||||
leaveMessage: String(args.leaveMessage || ""),
|
||||
sasl: String(args.sasl || ""),
|
||||
saslAccount: String(args.saslAccount || ""),
|
||||
saslPassword: String(args.saslPassword || ""),
|
||||
commands: args.commands || [],
|
||||
channels: channels,
|
||||
ignoreList: args.ignoreList ? args.ignoreList : [],
|
||||
|
||||
proxyEnabled: !!args.proxyEnabled,
|
||||
proxyHost: String(args.proxyHost || ""),
|
||||
proxyPort: parseInt(args.proxyPort, 10),
|
||||
proxyUsername: String(args.proxyUsername || ""),
|
||||
proxyPassword: String(args.proxyPassword || ""),
|
||||
});
|
||||
|
||||
// Set network lobby channel id
|
||||
network.channels[0].id = lobbyChannelId;
|
||||
|
||||
client.networks.push(network);
|
||||
client.emit("network", {
|
||||
networks: [network.getFilteredClone(this.lastActiveChannel, -1)],
|
||||
});
|
||||
|
||||
if (!network.validate(client)) {
|
||||
return;
|
||||
}
|
||||
|
||||
network.createIrcFramework(client);
|
||||
|
||||
events.forEach((plugin) => {
|
||||
require(`./plugins/irc-events/${plugin}`).apply(client, [network.irc, network]);
|
||||
});
|
||||
|
||||
if (network.userDisconnected) {
|
||||
network.channels[0].pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
text: "You have manually disconnected from this network before, use the /connect command to connect again.",
|
||||
}),
|
||||
true
|
||||
);
|
||||
} else if (!isStartup) {
|
||||
network.irc.connect();
|
||||
}
|
||||
|
||||
if (!isStartup) {
|
||||
client.save();
|
||||
channels.forEach((channel) => channel.loadMessages(client, network));
|
||||
}
|
||||
};
|
||||
|
||||
Client.prototype.generateToken = function (callback) {
|
||||
crypto.randomBytes(64, (err, buf) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
callback(buf.toString("hex"));
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.calculateTokenHash = function (token) {
|
||||
return crypto.createHash("sha512").update(token).digest("hex");
|
||||
};
|
||||
|
||||
Client.prototype.updateSession = function (token, ip, request) {
|
||||
const client = this;
|
||||
const agent = UAParser(request.headers["user-agent"] || "");
|
||||
let friendlyAgent = "";
|
||||
|
||||
if (agent.browser.name) {
|
||||
friendlyAgent = `${agent.browser.name} ${agent.browser.major}`;
|
||||
} else {
|
||||
friendlyAgent = "Unknown browser";
|
||||
}
|
||||
|
||||
if (agent.os.name) {
|
||||
friendlyAgent += ` on ${agent.os.name}`;
|
||||
|
||||
if (agent.os.version) {
|
||||
friendlyAgent += ` ${agent.os.version}`;
|
||||
}
|
||||
}
|
||||
|
||||
client.config.sessions[token] = _.assign(client.config.sessions[token], {
|
||||
lastUse: Date.now(),
|
||||
ip: ip,
|
||||
agent: friendlyAgent,
|
||||
});
|
||||
|
||||
client.save();
|
||||
};
|
||||
|
||||
Client.prototype.setPassword = function (hash, callback) {
|
||||
const client = this;
|
||||
|
||||
const oldHash = client.config.password;
|
||||
client.config.password = hash;
|
||||
client.manager.saveUser(client, function (err) {
|
||||
if (err) {
|
||||
// If user file fails to write, reset it back
|
||||
client.config.password = oldHash;
|
||||
return callback(false);
|
||||
}
|
||||
|
||||
return callback(true);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.input = function (data) {
|
||||
const client = this;
|
||||
data.text.split("\n").forEach((line) => {
|
||||
data.text = line;
|
||||
client.inputLine(data);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.inputLine = function (data) {
|
||||
const client = this;
|
||||
const target = client.find(data.target);
|
||||
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sending a message to a channel is higher priority than merely opening one
|
||||
// so that reloading the page will open this channel
|
||||
this.lastActiveChannel = target.chan.id;
|
||||
|
||||
let text = data.text;
|
||||
|
||||
// This is either a normal message or a command escaped with a leading '/'
|
||||
if (text.charAt(0) !== "/" || text.charAt(1) === "/") {
|
||||
if (target.chan.type === Chan.Type.LOBBY) {
|
||||
target.chan.pushMessage(
|
||||
this,
|
||||
new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: "Messages can not be sent to lobbies.",
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
text = "say " + text.replace(/^\//, "");
|
||||
} else {
|
||||
text = text.substr(1);
|
||||
}
|
||||
|
||||
const args = text.split(" ");
|
||||
const cmd = args.shift().toLowerCase();
|
||||
|
||||
const irc = target.network.irc;
|
||||
let connected = irc && irc.connection && irc.connection.connected;
|
||||
|
||||
if (inputs.userInputs.has(cmd)) {
|
||||
const plugin = inputs.userInputs.get(cmd);
|
||||
|
||||
if (typeof plugin.input === "function" && (connected || plugin.allowDisconnected)) {
|
||||
connected = true;
|
||||
plugin.input.apply(client, [target.network, target.chan, cmd, args]);
|
||||
}
|
||||
} else if (inputs.pluginCommands.has(cmd)) {
|
||||
const plugin = inputs.pluginCommands.get(cmd);
|
||||
|
||||
if (typeof plugin.input === "function" && (connected || plugin.allowDisconnected)) {
|
||||
connected = true;
|
||||
plugin.input(
|
||||
new PublicClient(client, plugin.packageInfo),
|
||||
{network: target.network, chan: target.chan},
|
||||
cmd,
|
||||
args
|
||||
);
|
||||
}
|
||||
} else if (connected) {
|
||||
irc.raw(text);
|
||||
}
|
||||
|
||||
if (!connected) {
|
||||
target.chan.pushMessage(
|
||||
this,
|
||||
new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: "You are not connected to the IRC network, unable to send your command.",
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Client.prototype.compileCustomHighlights = function () {
|
||||
this.highlightRegex = compileHighlightRegex(this.config.clientSettings.highlights);
|
||||
this.highlightExceptionRegex = compileHighlightRegex(
|
||||
this.config.clientSettings.highlightExceptions
|
||||
);
|
||||
};
|
||||
|
||||
function compileHighlightRegex(customHighlightString) {
|
||||
if (typeof customHighlightString !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure we don't have empty strings in the list of highlights
|
||||
const highlightsTokens = customHighlightString
|
||||
.split(",")
|
||||
.map((highlight) => escapeRegExp(highlight.trim()))
|
||||
.filter((highlight) => highlight.length > 0);
|
||||
|
||||
if (highlightsTokens.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new RegExp(
|
||||
`(?:^|[ .,+!?|/:<>(){}'"@&~-])(?:${highlightsTokens.join("|")})(?:$|[ .,+!?|/:<>(){}'"-])`,
|
||||
"i"
|
||||
);
|
||||
}
|
||||
|
||||
Client.prototype.more = function (data) {
|
||||
const client = this;
|
||||
const target = client.find(data.target);
|
||||
|
||||
if (!target) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const chan = target.chan;
|
||||
let messages = [];
|
||||
let index = 0;
|
||||
|
||||
// If client requests -1, send last 100 messages
|
||||
if (data.lastId < 0) {
|
||||
index = chan.messages.length;
|
||||
} else {
|
||||
index = chan.messages.findIndex((val) => val.id === data.lastId);
|
||||
}
|
||||
|
||||
// If requested id is not found, an empty array will be sent
|
||||
if (index > 0) {
|
||||
let startIndex = index;
|
||||
|
||||
if (data.condensed) {
|
||||
// Limit to 1000 messages (that's 10x normal limit)
|
||||
const indexToStop = Math.max(0, index - 1000);
|
||||
let realMessagesLeft = 100;
|
||||
|
||||
for (let i = index - 1; i >= indexToStop; i--) {
|
||||
startIndex--;
|
||||
|
||||
// Do not count condensed messages towards the 100 messages
|
||||
if (constants.condensedTypes.has(chan.messages[i].type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Count up actual 100 visible messages
|
||||
if (--realMessagesLeft === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
startIndex = Math.max(0, index - 100);
|
||||
}
|
||||
|
||||
messages = chan.messages.slice(startIndex, index);
|
||||
}
|
||||
|
||||
return {
|
||||
chan: chan.id,
|
||||
messages: messages,
|
||||
totalMessages: chan.messages.length,
|
||||
};
|
||||
};
|
||||
|
||||
Client.prototype.clearHistory = function (data) {
|
||||
const client = this;
|
||||
const target = client.find(data.target);
|
||||
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
target.chan.messages = [];
|
||||
target.chan.unread = 0;
|
||||
target.chan.highlight = 0;
|
||||
target.chan.firstUnread = 0;
|
||||
|
||||
client.emit("history:clear", {
|
||||
target: target.chan.id,
|
||||
});
|
||||
|
||||
if (!target.chan.isLoggable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const messageStorage of this.messageStorage) {
|
||||
messageStorage.deleteChannel(target.network, target.chan);
|
||||
}
|
||||
};
|
||||
|
||||
Client.prototype.search = function (query) {
|
||||
if (this.messageProvider === undefined) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
return this.messageProvider.search(query);
|
||||
};
|
||||
|
||||
Client.prototype.open = function (socketId, target) {
|
||||
// Due to how socket.io works internally, normal events may arrive later than
|
||||
// the disconnect event, and because we can't control this timing precisely,
|
||||
// process this event normally even if there is no attached client anymore.
|
||||
const attachedClient = this.attachedClients[socketId] || {};
|
||||
|
||||
// Opening a window like settings
|
||||
if (target === null) {
|
||||
attachedClient.openChannel = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
target = this.find(target);
|
||||
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
target.chan.unread = 0;
|
||||
target.chan.highlight = 0;
|
||||
|
||||
if (target.chan.messages.length > 0) {
|
||||
target.chan.firstUnread = target.chan.messages[target.chan.messages.length - 1].id;
|
||||
}
|
||||
|
||||
attachedClient.openChannel = target.chan.id;
|
||||
this.lastActiveChannel = target.chan.id;
|
||||
|
||||
this.emit("open", target.chan.id);
|
||||
};
|
||||
|
||||
Client.prototype.sort = function (data) {
|
||||
const order = data.order;
|
||||
|
||||
if (!_.isArray(order)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (data.type) {
|
||||
case "networks":
|
||||
this.networks.sort((a, b) => order.indexOf(a.uuid) - order.indexOf(b.uuid));
|
||||
|
||||
// Sync order to connected clients
|
||||
this.emit("sync_sort", {
|
||||
order: this.networks.map((obj) => obj.uuid),
|
||||
type: data.type,
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
case "channels": {
|
||||
const network = _.find(this.networks, {uuid: data.target});
|
||||
|
||||
if (!network) {
|
||||
return;
|
||||
}
|
||||
|
||||
network.channels.sort((a, b) => {
|
||||
// Always sort lobby to the top regardless of what the client has sent
|
||||
// Because there's a lot of code that presumes channels[0] is the lobby
|
||||
if (a.type === Chan.Type.LOBBY) {
|
||||
return -1;
|
||||
} else if (b.type === Chan.Type.LOBBY) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return order.indexOf(a.id) - order.indexOf(b.id);
|
||||
});
|
||||
|
||||
// Sync order to connected clients
|
||||
this.emit("sync_sort", {
|
||||
order: network.channels.map((obj) => obj.id),
|
||||
type: data.type,
|
||||
target: network.uuid,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.save();
|
||||
};
|
||||
|
||||
Client.prototype.names = function (data) {
|
||||
const client = this;
|
||||
const target = client.find(data.target);
|
||||
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.emit("names", {
|
||||
id: target.chan.id,
|
||||
users: target.chan.getSortedUsers(target.network.irc),
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.part = function (network, chan) {
|
||||
const client = this;
|
||||
network.channels = _.without(network.channels, chan);
|
||||
client.mentions = client.mentions.filter((msg) => !(msg.chanId === chan.id));
|
||||
chan.destroy();
|
||||
client.save();
|
||||
client.emit("part", {
|
||||
chan: chan.id,
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.quit = function (signOut) {
|
||||
const sockets = this.manager.sockets.sockets;
|
||||
const room = sockets.adapter.rooms.get(this.id);
|
||||
|
||||
if (room) {
|
||||
for (const user of room) {
|
||||
const socket = sockets.sockets.get(user);
|
||||
|
||||
if (socket) {
|
||||
if (signOut) {
|
||||
socket.emit("sign-out");
|
||||
}
|
||||
|
||||
socket.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.networks.forEach((network) => {
|
||||
network.quit();
|
||||
network.destroy();
|
||||
});
|
||||
|
||||
for (const messageStorage of this.messageStorage) {
|
||||
messageStorage.close();
|
||||
}
|
||||
};
|
||||
|
||||
Client.prototype.clientAttach = function (socketId, token) {
|
||||
const client = this;
|
||||
|
||||
if (client.awayMessage && _.size(client.attachedClients) === 0) {
|
||||
client.networks.forEach(function (network) {
|
||||
// Only remove away on client attachment if
|
||||
// there is no away message on this network
|
||||
if (network.irc && !network.awayMessage) {
|
||||
network.irc.raw("AWAY");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const openChannel = client.lastActiveChannel;
|
||||
client.attachedClients[socketId] = {token, openChannel};
|
||||
};
|
||||
|
||||
Client.prototype.clientDetach = function (socketId) {
|
||||
const client = this;
|
||||
|
||||
delete this.attachedClients[socketId];
|
||||
|
||||
if (client.awayMessage && _.size(client.attachedClients) === 0) {
|
||||
client.networks.forEach(function (network) {
|
||||
// Only set away on client deattachment if
|
||||
// there is no away message on this network
|
||||
if (network.irc && !network.awayMessage) {
|
||||
network.irc.raw("AWAY", client.awayMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Client.prototype.registerPushSubscription = function (session, subscription, noSave) {
|
||||
if (
|
||||
!_.isPlainObject(subscription) ||
|
||||
!_.isPlainObject(subscription.keys) ||
|
||||
typeof subscription.endpoint !== "string" ||
|
||||
!/^https?:\/\//.test(subscription.endpoint) ||
|
||||
typeof subscription.keys.p256dh !== "string" ||
|
||||
typeof subscription.keys.auth !== "string"
|
||||
) {
|
||||
session.pushSubscription = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
endpoint: subscription.endpoint,
|
||||
keys: {
|
||||
p256dh: subscription.keys.p256dh,
|
||||
auth: subscription.keys.auth,
|
||||
},
|
||||
};
|
||||
|
||||
session.pushSubscription = data;
|
||||
|
||||
if (!noSave) {
|
||||
this.save();
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
Client.prototype.unregisterPushSubscription = function (token) {
|
||||
this.config.sessions[token].pushSubscription = null;
|
||||
this.save();
|
||||
};
|
||||
|
||||
Client.prototype.save = _.debounce(
|
||||
function SaveClient() {
|
||||
if (Config.values.public) {
|
||||
return;
|
||||
}
|
||||
|
||||
const client = this;
|
||||
client.manager.saveUser(client);
|
||||
},
|
||||
5000,
|
||||
{maxWait: 20000}
|
||||
);
|
787
src/client.ts
Normal file
787
src/client.ts
Normal file
|
@ -0,0 +1,787 @@
|
|||
"use strict";
|
||||
|
||||
import _ from "lodash";
|
||||
import UAParser from "ua-parser-js";
|
||||
import {v4 as uuidv4} from "uuid";
|
||||
import escapeRegExp from "lodash/escapeRegExp";
|
||||
import crypto from "crypto";
|
||||
import colors from "chalk";
|
||||
|
||||
import log from "./log";
|
||||
import Chan from "./models/chan";
|
||||
import Msg from "./models/msg";
|
||||
import Config from "./config";
|
||||
import constants from "../client/js/constants.js";
|
||||
|
||||
import inputs from "./plugins/inputs";
|
||||
import PublicClient from "./plugins/packages/publicClient";
|
||||
import SqliteMessageStorage from "./plugins/messageStorage/sqlite";
|
||||
import TextFileMessageStorage from "./plugins/messageStorage/text";
|
||||
import {ClientConfig, Mention, PushSubscription} from "src/types/client";
|
||||
import Network from "./models/network";
|
||||
import ClientManager from "./clientManager";
|
||||
import {MessageType} from "./types/models/message";
|
||||
import {ChanType} from "./types/models/channel";
|
||||
import {MessageStorage} from "./types/plugins/messageStorage";
|
||||
|
||||
const events = [
|
||||
"away",
|
||||
"cap",
|
||||
"connection",
|
||||
"unhandled",
|
||||
"ctcp",
|
||||
"chghost",
|
||||
"error",
|
||||
"help",
|
||||
"info",
|
||||
"invite",
|
||||
"join",
|
||||
"kick",
|
||||
"list",
|
||||
"mode",
|
||||
"modelist",
|
||||
"motd",
|
||||
"message",
|
||||
"names",
|
||||
"nick",
|
||||
"part",
|
||||
"quit",
|
||||
"sasl",
|
||||
"topic",
|
||||
"welcome",
|
||||
"whois",
|
||||
];
|
||||
class Client {
|
||||
awayMessage: string;
|
||||
lastActiveChannel: number;
|
||||
attachedClients: {
|
||||
[socketId: string]: {token: string; openChannel: number};
|
||||
};
|
||||
config: ClientConfig & {
|
||||
networks: Network[];
|
||||
};
|
||||
id: number;
|
||||
idMsg: number;
|
||||
idChan: number;
|
||||
name: string;
|
||||
networks: Network[];
|
||||
mentions: Mention[];
|
||||
manager: ClientManager;
|
||||
messageStorage: MessageStorage[];
|
||||
highlightRegex?: RegExp;
|
||||
highlightExceptionRegex?: RegExp;
|
||||
messageProvider?: SqliteMessageStorage;
|
||||
|
||||
fileHash: string;
|
||||
|
||||
constructor(manager: ClientManager, name?: string, config = {} as ClientConfig) {
|
||||
_.merge(this, {
|
||||
awayMessage: "",
|
||||
lastActiveChannel: -1,
|
||||
attachedClients: {},
|
||||
config: config,
|
||||
id: uuidv4(),
|
||||
idChan: 1,
|
||||
idMsg: 1,
|
||||
name: name,
|
||||
networks: [],
|
||||
mentions: [],
|
||||
manager: manager,
|
||||
messageStorage: [],
|
||||
highlightRegex: null,
|
||||
highlightExceptionRegex: null,
|
||||
messageProvider: undefined,
|
||||
});
|
||||
|
||||
const client = this;
|
||||
|
||||
client.config.log = Boolean(client.config.log);
|
||||
client.config.password = String(client.config.password);
|
||||
|
||||
if (!Config.values.public && client.config.log) {
|
||||
if (Config.values.messageStorage.includes("sqlite")) {
|
||||
client.messageProvider = new SqliteMessageStorage(client);
|
||||
client.messageStorage.push(client.messageProvider);
|
||||
}
|
||||
|
||||
if (Config.values.messageStorage.includes("text")) {
|
||||
client.messageStorage.push(new TextFileMessageStorage(client));
|
||||
}
|
||||
|
||||
for (const messageStorage of client.messageStorage) {
|
||||
messageStorage.enable();
|
||||
}
|
||||
}
|
||||
|
||||
if (!_.isPlainObject(client.config.sessions)) {
|
||||
client.config.sessions = {};
|
||||
}
|
||||
|
||||
if (!_.isPlainObject(client.config.clientSettings)) {
|
||||
client.config.clientSettings = {};
|
||||
}
|
||||
|
||||
if (!_.isPlainObject(client.config.browser)) {
|
||||
client.config.browser = {};
|
||||
}
|
||||
|
||||
if (client.config.clientSettings.awayMessage) {
|
||||
client.awayMessage = client.config.clientSettings.awayMessage;
|
||||
}
|
||||
|
||||
client.config.clientSettings.searchEnabled = client.messageProvider !== undefined;
|
||||
|
||||
client.compileCustomHighlights();
|
||||
|
||||
_.forOwn(client.config.sessions, (session) => {
|
||||
if (session.pushSubscription) {
|
||||
this.registerPushSubscription(session, session.pushSubscription, true);
|
||||
}
|
||||
});
|
||||
|
||||
(client.config.networks || []).forEach((network) => client.connect(network, true));
|
||||
|
||||
// Networks are stored directly in the client object
|
||||
// We don't need to keep it in the config object
|
||||
delete client.config.networks;
|
||||
|
||||
if (client.name) {
|
||||
log.info(`User ${colors.bold(client.name)} loaded`);
|
||||
|
||||
// Networks are created instantly, but to reduce server load on startup
|
||||
// We randomize the IRC connections and channel log loading
|
||||
let delay = manager.clients.length * 500;
|
||||
client.networks.forEach((network) => {
|
||||
setTimeout(() => {
|
||||
network.channels.forEach((channel) => channel.loadMessages(client, network));
|
||||
|
||||
if (!network.userDisconnected && network.irc) {
|
||||
network.irc.connect();
|
||||
}
|
||||
}, delay);
|
||||
|
||||
delay += 1000 + Math.floor(Math.random() * 1000);
|
||||
});
|
||||
|
||||
client.fileHash = manager.getDataToSave(client).newHash;
|
||||
}
|
||||
}
|
||||
|
||||
createChannel(attr: Partial<Chan>) {
|
||||
const chan = new Chan(attr);
|
||||
chan.id = this.idChan++;
|
||||
|
||||
return chan;
|
||||
}
|
||||
|
||||
emit(event: string, data: any) {
|
||||
if (this.manager !== null) {
|
||||
this.manager.sockets.in(this.id.toString()).emit(event, data);
|
||||
}
|
||||
}
|
||||
|
||||
find(channelId: number) {
|
||||
let network = null;
|
||||
let chan = null;
|
||||
|
||||
for (const i in this.networks) {
|
||||
const n = this.networks[i];
|
||||
chan = _.find(n.channels, {id: channelId});
|
||||
|
||||
if (chan) {
|
||||
network = n;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (network && chan) {
|
||||
return {network, chan};
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
connect(args: any, isStartup = false) {
|
||||
const client = this;
|
||||
let channels = [];
|
||||
|
||||
// Get channel id for lobby before creating other channels for nicer ids
|
||||
const lobbyChannelId = client.idChan++;
|
||||
|
||||
if (args.channels) {
|
||||
let badName = false;
|
||||
|
||||
args.channels.forEach((chan: Chan) => {
|
||||
if (!chan.name) {
|
||||
badName = true;
|
||||
return;
|
||||
}
|
||||
|
||||
channels.push(
|
||||
client.createChannel({
|
||||
name: chan.name,
|
||||
key: chan.key || "",
|
||||
type: chan.type,
|
||||
muted: chan.muted,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
if (badName && client.name) {
|
||||
log.warn(
|
||||
"User '" +
|
||||
client.name +
|
||||
"' on network '" +
|
||||
args.name +
|
||||
"' has an invalid channel which has been ignored"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const network = new Network({
|
||||
uuid: args.uuid,
|
||||
name: String(
|
||||
args.name || (Config.values.lockNetwork ? Config.values.defaults.name : "") || ""
|
||||
),
|
||||
host: String(args.host || ""),
|
||||
port: parseInt(args.port, 10),
|
||||
tls: !!args.tls,
|
||||
userDisconnected: !!args.userDisconnected,
|
||||
rejectUnauthorized: !!args.rejectUnauthorized,
|
||||
password: String(args.password || ""),
|
||||
nick: String(args.nick || ""),
|
||||
username: String(args.username || ""),
|
||||
realname: String(args.realname || ""),
|
||||
leaveMessage: String(args.leaveMessage || ""),
|
||||
sasl: String(args.sasl || ""),
|
||||
saslAccount: String(args.saslAccount || ""),
|
||||
saslPassword: String(args.saslPassword || ""),
|
||||
commands: args.commands || [],
|
||||
channels: channels,
|
||||
ignoreList: args.ignoreList ? args.ignoreList : [],
|
||||
|
||||
proxyEnabled: !!args.proxyEnabled,
|
||||
proxyHost: String(args.proxyHost || ""),
|
||||
proxyPort: parseInt(args.proxyPort, 10),
|
||||
proxyUsername: String(args.proxyUsername || ""),
|
||||
proxyPassword: String(args.proxyPassword || ""),
|
||||
});
|
||||
|
||||
// Set network lobby channel id
|
||||
network.channels[0].id = lobbyChannelId;
|
||||
|
||||
client.networks.push(network);
|
||||
client.emit("network", {
|
||||
networks: [network.getFilteredClone(this.lastActiveChannel, -1)],
|
||||
});
|
||||
|
||||
if (!network.validate(client)) {
|
||||
return;
|
||||
}
|
||||
|
||||
network.createIrcFramework(client);
|
||||
|
||||
events.forEach((plugin) => {
|
||||
require(`./plugins/irc-events/${plugin}`).apply(client, [network.irc, network]);
|
||||
});
|
||||
|
||||
if (network.userDisconnected) {
|
||||
network.channels[0].pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
text: "You have manually disconnected from this network before, use the /connect command to connect again.",
|
||||
}),
|
||||
true
|
||||
);
|
||||
} else if (!isStartup) {
|
||||
network.irc.connect();
|
||||
}
|
||||
|
||||
if (!isStartup) {
|
||||
client.save();
|
||||
channels.forEach((channel) => channel.loadMessages(client, network));
|
||||
}
|
||||
}
|
||||
|
||||
generateToken(callback: (token: string) => void) {
|
||||
crypto.randomBytes(64, (err, buf) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
callback(buf.toString("hex"));
|
||||
});
|
||||
}
|
||||
|
||||
calculateTokenHash(token: string) {
|
||||
return crypto.createHash("sha512").update(token).digest("hex");
|
||||
}
|
||||
|
||||
updateSession(token: string, ip: string, request: any) {
|
||||
const client = this;
|
||||
const agent = UAParser(request.headers["user-agent"] || "");
|
||||
let friendlyAgent = "";
|
||||
|
||||
if (agent.browser.name) {
|
||||
friendlyAgent = `${agent.browser.name} ${agent.browser.major}`;
|
||||
} else {
|
||||
friendlyAgent = "Unknown browser";
|
||||
}
|
||||
|
||||
if (agent.os.name) {
|
||||
friendlyAgent += ` on ${agent.os.name}`;
|
||||
|
||||
if (agent.os.version) {
|
||||
friendlyAgent += ` ${agent.os.version}`;
|
||||
}
|
||||
}
|
||||
|
||||
client.config.sessions[token] = _.assign(client.config.sessions[token], {
|
||||
lastUse: Date.now(),
|
||||
ip: ip,
|
||||
agent: friendlyAgent,
|
||||
});
|
||||
|
||||
client.save();
|
||||
}
|
||||
|
||||
setPassword(hash: string, callback: (success: boolean) => void) {
|
||||
const client = this;
|
||||
|
||||
const oldHash = client.config.password;
|
||||
client.config.password = hash;
|
||||
client.manager.saveUser(client, function (err) {
|
||||
if (err) {
|
||||
// If user file fails to write, reset it back
|
||||
client.config.password = oldHash;
|
||||
return callback(false);
|
||||
}
|
||||
|
||||
return callback(true);
|
||||
});
|
||||
}
|
||||
|
||||
input(data) {
|
||||
const client = this;
|
||||
data.text.split("\n").forEach((line) => {
|
||||
data.text = line;
|
||||
client.inputLine(data);
|
||||
});
|
||||
}
|
||||
|
||||
inputLine(data) {
|
||||
const client = this;
|
||||
const target = client.find(data.target);
|
||||
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sending a message to a channel is higher priority than merely opening one
|
||||
// so that reloading the page will open this channel
|
||||
this.lastActiveChannel = target.chan.id;
|
||||
|
||||
let text = data.text;
|
||||
|
||||
// This is either a normal message or a command escaped with a leading '/'
|
||||
if (text.charAt(0) !== "/" || text.charAt(1) === "/") {
|
||||
if (target.chan.type === ChanType.LOBBY) {
|
||||
target.chan.pushMessage(
|
||||
this,
|
||||
new Msg({
|
||||
type: MessageType.ERROR,
|
||||
text: "Messages can not be sent to lobbies.",
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
text = "say " + text.replace(/^\//, "");
|
||||
} else {
|
||||
text = text.substr(1);
|
||||
}
|
||||
|
||||
const args = text.split(" ");
|
||||
const cmd = args.shift().toLowerCase();
|
||||
|
||||
const irc = target.network.irc;
|
||||
let connected = irc && irc.connection && irc.connection.connected;
|
||||
|
||||
if (inputs.userInputs.has(cmd)) {
|
||||
const plugin = inputs.userInputs.get(cmd);
|
||||
|
||||
if (typeof plugin.input === "function" && (connected || plugin.allowDisconnected)) {
|
||||
connected = true;
|
||||
plugin.input.apply(client, [target.network, target.chan, cmd, args]);
|
||||
}
|
||||
} else if (inputs.pluginCommands.has(cmd)) {
|
||||
const plugin = inputs.pluginCommands.get(cmd);
|
||||
|
||||
if (typeof plugin.input === "function" && (connected || plugin.allowDisconnected)) {
|
||||
connected = true;
|
||||
plugin.input(
|
||||
new PublicClient(client, plugin.packageInfo),
|
||||
{network: target.network, chan: target.chan},
|
||||
cmd,
|
||||
args
|
||||
);
|
||||
}
|
||||
} else if (connected) {
|
||||
irc.raw(text);
|
||||
}
|
||||
|
||||
if (!connected) {
|
||||
target.chan.pushMessage(
|
||||
this,
|
||||
new Msg({
|
||||
type: MessageType.ERROR,
|
||||
text: "You are not connected to the IRC network, unable to send your command.",
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
compileCustomHighlights() {
|
||||
function compileHighlightRegex(customHighlightString) {
|
||||
if (typeof customHighlightString !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure we don't have empty strings in the list of highlights
|
||||
const highlightsTokens = customHighlightString
|
||||
.split(",")
|
||||
.map((highlight) => escapeRegExp(highlight.trim()))
|
||||
.filter((highlight) => highlight.length > 0);
|
||||
|
||||
if (highlightsTokens.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new RegExp(
|
||||
`(?:^|[ .,+!?|/:<>(){}'"@&~-])(?:${highlightsTokens.join(
|
||||
"|"
|
||||
)})(?:$|[ .,+!?|/:<>(){}'"-])`,
|
||||
"i"
|
||||
);
|
||||
}
|
||||
|
||||
this.highlightRegex = compileHighlightRegex(this.config.clientSettings.highlights);
|
||||
this.highlightExceptionRegex = compileHighlightRegex(
|
||||
this.config.clientSettings.highlightExceptions
|
||||
);
|
||||
}
|
||||
|
||||
more(data) {
|
||||
const client = this;
|
||||
const target = client.find(data.target);
|
||||
|
||||
if (!target) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const chan = target.chan;
|
||||
let messages = [];
|
||||
let index = 0;
|
||||
|
||||
// If client requests -1, send last 100 messages
|
||||
if (data.lastId < 0) {
|
||||
index = chan.messages.length;
|
||||
} else {
|
||||
index = chan.messages.findIndex((val) => val.id === data.lastId);
|
||||
}
|
||||
|
||||
// If requested id is not found, an empty array will be sent
|
||||
if (index > 0) {
|
||||
let startIndex = index;
|
||||
|
||||
if (data.condensed) {
|
||||
// Limit to 1000 messages (that's 10x normal limit)
|
||||
const indexToStop = Math.max(0, index - 1000);
|
||||
let realMessagesLeft = 100;
|
||||
|
||||
for (let i = index - 1; i >= indexToStop; i--) {
|
||||
startIndex--;
|
||||
|
||||
// Do not count condensed messages towards the 100 messages
|
||||
if (constants.condensedTypes.has(chan.messages[i].type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Count up actual 100 visible messages
|
||||
if (--realMessagesLeft === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
startIndex = Math.max(0, index - 100);
|
||||
}
|
||||
|
||||
messages = chan.messages.slice(startIndex, index);
|
||||
}
|
||||
|
||||
return {
|
||||
chan: chan.id,
|
||||
messages: messages,
|
||||
totalMessages: chan.messages.length,
|
||||
};
|
||||
}
|
||||
|
||||
clearHistory(data) {
|
||||
const client = this;
|
||||
const target = client.find(data.target);
|
||||
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
target.chan.messages = [];
|
||||
target.chan.unread = 0;
|
||||
target.chan.highlight = 0;
|
||||
target.chan.firstUnread = 0;
|
||||
|
||||
client.emit("history:clear", {
|
||||
target: target.chan.id,
|
||||
});
|
||||
|
||||
if (!target.chan.isLoggable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const messageStorage of this.messageStorage) {
|
||||
messageStorage.deleteChannel(target.network, target.chan);
|
||||
}
|
||||
}
|
||||
|
||||
search(query: string) {
|
||||
if (this.messageProvider === undefined) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
return this.messageProvider.search(query);
|
||||
}
|
||||
|
||||
open(socketId: string, target: number) {
|
||||
// Due to how socket.io works internally, normal events may arrive later than
|
||||
// the disconnect event, and because we can't control this timing precisely,
|
||||
// process this event normally even if there is no attached client anymore.
|
||||
const attachedClient = this.attachedClients[socketId] || ({} as any);
|
||||
|
||||
// Opening a window like settings
|
||||
if (target === null) {
|
||||
attachedClient.openChannel = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
const targetNetChan = this.find(target);
|
||||
|
||||
if (!targetNetChan) {
|
||||
return;
|
||||
}
|
||||
|
||||
targetNetChan.chan.unread = 0;
|
||||
targetNetChan.chan.highlight = 0;
|
||||
|
||||
if (targetNetChan.chan.messages.length > 0) {
|
||||
targetNetChan.chan.firstUnread =
|
||||
targetNetChan.chan.messages[targetNetChan.chan.messages.length - 1].id;
|
||||
}
|
||||
|
||||
attachedClient.openChannel = targetNetChan.chan.id;
|
||||
this.lastActiveChannel = targetNetChan.chan.id;
|
||||
|
||||
this.emit("open", targetNetChan.chan.id);
|
||||
}
|
||||
|
||||
sort(data) {
|
||||
const order = data.order;
|
||||
|
||||
if (!_.isArray(order)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (data.type) {
|
||||
case "networks":
|
||||
this.networks.sort((a, b) => order.indexOf(a.uuid) - order.indexOf(b.uuid));
|
||||
|
||||
// Sync order to connected clients
|
||||
this.emit("sync_sort", {
|
||||
order: this.networks.map((obj) => obj.uuid),
|
||||
type: data.type,
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
case "channels": {
|
||||
const network = _.find(this.networks, {uuid: data.target});
|
||||
|
||||
if (!network) {
|
||||
return;
|
||||
}
|
||||
|
||||
network.channels.sort((a, b) => {
|
||||
// Always sort lobby to the top regardless of what the client has sent
|
||||
// Because there's a lot of code that presumes channels[0] is the lobby
|
||||
if (a.type === ChanType.LOBBY) {
|
||||
return -1;
|
||||
} else if (b.type === ChanType.LOBBY) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return order.indexOf(a.id) - order.indexOf(b.id);
|
||||
});
|
||||
|
||||
// Sync order to connected clients
|
||||
this.emit("sync_sort", {
|
||||
order: network.channels.map((obj) => obj.id),
|
||||
type: data.type,
|
||||
target: network.uuid,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.save();
|
||||
}
|
||||
|
||||
names(data) {
|
||||
const client = this;
|
||||
const target = client.find(data.target);
|
||||
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.emit("names", {
|
||||
id: target.chan.id,
|
||||
users: target.chan.getSortedUsers(target.network.irc),
|
||||
});
|
||||
}
|
||||
|
||||
part(network: Network, chan: Chan) {
|
||||
const client = this;
|
||||
network.channels = _.without(network.channels, chan);
|
||||
client.mentions = client.mentions.filter((msg) => !(msg.chanId === chan.id));
|
||||
chan.destroy();
|
||||
client.save();
|
||||
client.emit("part", {
|
||||
chan: chan.id,
|
||||
});
|
||||
}
|
||||
|
||||
quit(signOut: boolean) {
|
||||
const sockets = this.manager.sockets;
|
||||
const room = sockets.adapter.rooms.get(this.id.toString());
|
||||
|
||||
if (room) {
|
||||
for (const user of room) {
|
||||
const socket = sockets.sockets.get(user);
|
||||
|
||||
if (socket) {
|
||||
if (signOut) {
|
||||
socket.emit("sign-out");
|
||||
}
|
||||
|
||||
socket.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.networks.forEach((network) => {
|
||||
network.quit();
|
||||
network.destroy();
|
||||
});
|
||||
|
||||
for (const messageStorage of this.messageStorage) {
|
||||
messageStorage.close();
|
||||
}
|
||||
}
|
||||
|
||||
clientAttach(socketId: string, token: string) {
|
||||
const client = this;
|
||||
|
||||
if (client.awayMessage && _.size(client.attachedClients) === 0) {
|
||||
client.networks.forEach(function (network) {
|
||||
// Only remove away on client attachment if
|
||||
// there is no away message on this network
|
||||
if (network.irc && !network.awayMessage) {
|
||||
network.irc.raw("AWAY");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const openChannel = client.lastActiveChannel;
|
||||
client.attachedClients[socketId] = {token, openChannel};
|
||||
}
|
||||
|
||||
clientDetach(socketId: string) {
|
||||
const client = this;
|
||||
|
||||
delete this.attachedClients[socketId];
|
||||
|
||||
if (client.awayMessage && _.size(client.attachedClients) === 0) {
|
||||
client.networks.forEach(function (network) {
|
||||
// Only set away on client deattachment if
|
||||
// there is no away message on this network
|
||||
if (network.irc && !network.awayMessage) {
|
||||
network.irc.raw("AWAY", client.awayMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: type session to this.attachedClients
|
||||
registerPushSubscription(
|
||||
session: any,
|
||||
subscription: PushSubscription,
|
||||
noSave: boolean = false
|
||||
) {
|
||||
if (
|
||||
!_.isPlainObject(subscription) ||
|
||||
!_.isPlainObject(subscription.keys) ||
|
||||
typeof subscription.endpoint !== "string" ||
|
||||
!/^https?:\/\//.test(subscription.endpoint) ||
|
||||
typeof subscription.keys.p256dh !== "string" ||
|
||||
typeof subscription.keys.auth !== "string"
|
||||
) {
|
||||
session.pushSubscription = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
endpoint: subscription.endpoint,
|
||||
keys: {
|
||||
p256dh: subscription.keys.p256dh,
|
||||
auth: subscription.keys.auth,
|
||||
},
|
||||
};
|
||||
|
||||
session.pushSubscription = data;
|
||||
|
||||
if (!noSave) {
|
||||
this.save();
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
unregisterPushSubscription(token: string) {
|
||||
this.config.sessions[token].pushSubscription = null;
|
||||
this.save();
|
||||
}
|
||||
|
||||
save = _.debounce(
|
||||
function SaveClient() {
|
||||
if (Config.values.public) {
|
||||
return;
|
||||
}
|
||||
|
||||
const client = this;
|
||||
client.manager.saveUser(client);
|
||||
},
|
||||
5000,
|
||||
{maxWait: 20000}
|
||||
);
|
||||
}
|
||||
|
||||
export default Client;
|
|
@ -1,284 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
const _ = require("lodash");
|
||||
const log = require("./log");
|
||||
const colors = require("chalk");
|
||||
const crypto = require("crypto");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const Auth = require("./plugins/auth");
|
||||
const Client = require("./client");
|
||||
const Config = require("./config");
|
||||
const WebPush = require("./plugins/webpush");
|
||||
|
||||
module.exports = ClientManager;
|
||||
|
||||
function ClientManager() {
|
||||
this.clients = [];
|
||||
}
|
||||
|
||||
ClientManager.prototype.init = function (identHandler, sockets) {
|
||||
this.sockets = sockets;
|
||||
this.identHandler = identHandler;
|
||||
this.webPush = new WebPush();
|
||||
|
||||
if (!Config.values.public) {
|
||||
this.loadUsers();
|
||||
|
||||
// LDAP does not have user commands, and users are dynamically
|
||||
// created upon logon, so we don't need to watch for new files
|
||||
if (!Config.values.ldap.enable) {
|
||||
this.autoloadUsers();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ClientManager.prototype.findClient = function (name) {
|
||||
name = name.toLowerCase();
|
||||
return this.clients.find((u) => u.name.toLowerCase() === name);
|
||||
};
|
||||
|
||||
ClientManager.prototype.loadUsers = function () {
|
||||
let users = this.getUsers();
|
||||
|
||||
if (users.length === 0) {
|
||||
log.info(
|
||||
`There are currently no users. Create one with ${colors.bold("thelounge add <name>")}.`
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const alreadySeenUsers = new Set();
|
||||
users = users.filter((user) => {
|
||||
user = user.toLowerCase();
|
||||
|
||||
if (alreadySeenUsers.has(user)) {
|
||||
log.error(
|
||||
`There is more than one user named "${colors.bold(
|
||||
user
|
||||
)}". Usernames are now case insensitive, duplicate users will not load.`
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
alreadySeenUsers.add(user);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// This callback is used by Auth plugins to load users they deem acceptable
|
||||
const callbackLoadUser = (user) => {
|
||||
this.loadUser(user);
|
||||
};
|
||||
|
||||
if (!Auth.loadUsers(users, callbackLoadUser)) {
|
||||
// Fallback to loading all users
|
||||
users.forEach((name) => this.loadUser(name));
|
||||
}
|
||||
};
|
||||
|
||||
ClientManager.prototype.autoloadUsers = function () {
|
||||
fs.watch(
|
||||
Config.getUsersPath(),
|
||||
_.debounce(
|
||||
() => {
|
||||
const loaded = this.clients.map((c) => c.name);
|
||||
const updatedUsers = this.getUsers();
|
||||
|
||||
if (updatedUsers.length === 0) {
|
||||
log.info(
|
||||
`There are currently no users. Create one with ${colors.bold(
|
||||
"thelounge add <name>"
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
|
||||
// Reload all users. Existing users will only have their passwords reloaded.
|
||||
updatedUsers.forEach((name) => this.loadUser(name));
|
||||
|
||||
// Existing users removed since last time users were loaded
|
||||
_.difference(loaded, updatedUsers).forEach((name) => {
|
||||
const client = _.find(this.clients, {name});
|
||||
|
||||
if (client) {
|
||||
client.quit(true);
|
||||
this.clients = _.without(this.clients, client);
|
||||
log.info(`User ${colors.bold(name)} disconnected and removed.`);
|
||||
}
|
||||
});
|
||||
},
|
||||
1000,
|
||||
{maxWait: 10000}
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
ClientManager.prototype.loadUser = function (name) {
|
||||
const userConfig = readUserConfig(name);
|
||||
|
||||
if (!userConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
let client = this.findClient(name);
|
||||
|
||||
if (client) {
|
||||
if (userConfig.password !== client.config.password) {
|
||||
/**
|
||||
* If we happen to reload an existing client, make super duper sure we
|
||||
* have their latest password. We're not replacing the entire config
|
||||
* object, because that could have undesired consequences.
|
||||
*
|
||||
* @see https://github.com/thelounge/thelounge/issues/598
|
||||
*/
|
||||
client.config.password = userConfig.password;
|
||||
log.info(`Password for user ${colors.bold(name)} was reset.`);
|
||||
}
|
||||
} else {
|
||||
client = new Client(this, name, userConfig);
|
||||
this.clients.push(client);
|
||||
}
|
||||
|
||||
return client;
|
||||
};
|
||||
|
||||
ClientManager.prototype.getUsers = function () {
|
||||
if (!fs.existsSync(Config.getUsersPath())) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fs
|
||||
.readdirSync(Config.getUsersPath())
|
||||
.filter((file) => file.endsWith(".json"))
|
||||
.map((file) => file.slice(0, -5));
|
||||
};
|
||||
|
||||
ClientManager.prototype.addUser = function (name, password, enableLog) {
|
||||
if (path.basename(name) !== name) {
|
||||
throw new Error(`${name} is an invalid username.`);
|
||||
}
|
||||
|
||||
const userPath = Config.getUserConfigPath(name);
|
||||
|
||||
if (fs.existsSync(userPath)) {
|
||||
log.error(`User ${colors.green(name)} already exists.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const user = {
|
||||
password: password || "",
|
||||
log: enableLog,
|
||||
};
|
||||
|
||||
try {
|
||||
fs.writeFileSync(userPath, JSON.stringify(user, null, "\t"), {
|
||||
mode: 0o600,
|
||||
});
|
||||
} catch (e) {
|
||||
log.error(`Failed to create user ${colors.green(name)} (${e})`);
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
const userFolderStat = fs.statSync(Config.getUsersPath());
|
||||
const userFileStat = fs.statSync(userPath);
|
||||
|
||||
if (
|
||||
userFolderStat &&
|
||||
userFileStat &&
|
||||
(userFolderStat.uid !== userFileStat.uid || userFolderStat.gid !== userFileStat.gid)
|
||||
) {
|
||||
log.warn(
|
||||
`User ${colors.green(
|
||||
name
|
||||
)} has been created, but with a different uid (or gid) than expected.`
|
||||
);
|
||||
log.warn(
|
||||
"The file owner has been changed to the expected user. " +
|
||||
"To prevent any issues, please run thelounge commands " +
|
||||
"as the correct user that owns the config folder."
|
||||
);
|
||||
log.warn(
|
||||
"See https://thelounge.chat/docs/usage#using-the-correct-system-user for more information."
|
||||
);
|
||||
fs.chownSync(userPath, userFolderStat.uid, userFolderStat.gid);
|
||||
}
|
||||
} catch (e) {
|
||||
// We're simply verifying file owner as a safe guard for users
|
||||
// that run `thelounge add` as root, so we don't care if it fails
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
ClientManager.prototype.getDataToSave = function (client) {
|
||||
const json = Object.assign({}, client.config, {
|
||||
networks: client.networks.map((n) => n.export()),
|
||||
});
|
||||
const newUser = JSON.stringify(json, null, "\t");
|
||||
const newHash = crypto.createHash("sha256").update(newUser).digest("hex");
|
||||
|
||||
return {newUser, newHash};
|
||||
};
|
||||
|
||||
ClientManager.prototype.saveUser = function (client, callback) {
|
||||
const {newUser, newHash} = this.getDataToSave(client);
|
||||
|
||||
// Do not write to disk if the exported data hasn't actually changed
|
||||
if (client.fileHash === newHash) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pathReal = Config.getUserConfigPath(client.name);
|
||||
const pathTemp = pathReal + ".tmp";
|
||||
|
||||
try {
|
||||
// Write to a temp file first, in case the write fails
|
||||
// we do not lose the original file (for example when disk is full)
|
||||
fs.writeFileSync(pathTemp, newUser, {
|
||||
mode: 0o600,
|
||||
});
|
||||
fs.renameSync(pathTemp, pathReal);
|
||||
|
||||
return callback ? callback() : true;
|
||||
} catch (e) {
|
||||
log.error(`Failed to update user ${colors.green(client.name)} (${e})`);
|
||||
|
||||
if (callback) {
|
||||
callback(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ClientManager.prototype.removeUser = function (name) {
|
||||
const userPath = Config.getUserConfigPath(name);
|
||||
|
||||
if (!fs.existsSync(userPath)) {
|
||||
log.error(`Tried to remove non-existing user ${colors.green(name)}.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
fs.unlinkSync(userPath);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
function readUserConfig(name) {
|
||||
const userPath = Config.getUserConfigPath(name);
|
||||
|
||||
if (!fs.existsSync(userPath)) {
|
||||
log.error(`Tried to read non-existing user ${colors.green(name)}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = fs.readFileSync(userPath, "utf-8");
|
||||
return JSON.parse(data);
|
||||
} catch (e) {
|
||||
log.error(`Failed to read user ${colors.bold(name)}: ${e}`);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
295
src/clientManager.ts
Normal file
295
src/clientManager.ts
Normal file
|
@ -0,0 +1,295 @@
|
|||
"use strict";
|
||||
|
||||
import _ from "lodash";
|
||||
import colors from "chalk";
|
||||
import crypto from "crypto";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import Auth from "./plugins/auth";
|
||||
import Client from "./client";
|
||||
import Config from "./config";
|
||||
import WebPush from "./plugins/webpush";
|
||||
import log from "./log";
|
||||
import {Namespace, Server, Socket} from "socket.io";
|
||||
|
||||
class ClientManager {
|
||||
clients: Client[];
|
||||
sockets: Namespace;
|
||||
identHandler: any;
|
||||
webPush: WebPush;
|
||||
|
||||
constructor() {
|
||||
this.clients = [];
|
||||
}
|
||||
|
||||
init(identHandler, sockets: Namespace) {
|
||||
this.sockets = sockets;
|
||||
this.identHandler = identHandler;
|
||||
this.webPush = new WebPush();
|
||||
|
||||
if (!Config.values.public) {
|
||||
this.loadUsers();
|
||||
|
||||
// LDAP does not have user commands, and users are dynamically
|
||||
// created upon logon, so we don't need to watch for new files
|
||||
if (!Config.values.ldap.enable) {
|
||||
this.autoloadUsers();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findClient(name: string) {
|
||||
name = name.toLowerCase();
|
||||
return this.clients.find((u) => u.name.toLowerCase() === name);
|
||||
}
|
||||
|
||||
loadUsers() {
|
||||
let users = this.getUsers();
|
||||
|
||||
if (users.length === 0) {
|
||||
log.info(
|
||||
`There are currently no users. Create one with ${colors.bold(
|
||||
"thelounge add <name>"
|
||||
)}.`
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const alreadySeenUsers = new Set();
|
||||
users = users.filter((user) => {
|
||||
user = user.toLowerCase();
|
||||
|
||||
if (alreadySeenUsers.has(user)) {
|
||||
log.error(
|
||||
`There is more than one user named "${colors.bold(
|
||||
user
|
||||
)}". Usernames are now case insensitive, duplicate users will not load.`
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
alreadySeenUsers.add(user);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// This callback is used by Auth plugins to load users they deem acceptable
|
||||
const callbackLoadUser = (user) => {
|
||||
this.loadUser(user);
|
||||
};
|
||||
|
||||
if (!Auth.loadUsers(users, callbackLoadUser)) {
|
||||
// Fallback to loading all users
|
||||
users.forEach((name) => this.loadUser(name));
|
||||
}
|
||||
}
|
||||
|
||||
autoloadUsers() {
|
||||
fs.watch(
|
||||
Config.getUsersPath(),
|
||||
_.debounce(
|
||||
() => {
|
||||
const loaded = this.clients.map((c) => c.name);
|
||||
const updatedUsers = this.getUsers();
|
||||
|
||||
if (updatedUsers.length === 0) {
|
||||
log.info(
|
||||
`There are currently no users. Create one with ${colors.bold(
|
||||
"thelounge add <name>"
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
|
||||
// Reload all users. Existing users will only have their passwords reloaded.
|
||||
updatedUsers.forEach((name) => this.loadUser(name));
|
||||
|
||||
// Existing users removed since last time users were loaded
|
||||
_.difference(loaded, updatedUsers).forEach((name) => {
|
||||
const client = _.find(this.clients, {name});
|
||||
|
||||
if (client) {
|
||||
client.quit(true);
|
||||
this.clients = _.without(this.clients, client);
|
||||
log.info(`User ${colors.bold(name)} disconnected and removed.`);
|
||||
}
|
||||
});
|
||||
},
|
||||
1000,
|
||||
{maxWait: 10000}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
loadUser(name: string) {
|
||||
const userConfig = this.readUserConfig(name);
|
||||
|
||||
if (!userConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
let client = this.findClient(name);
|
||||
|
||||
if (client) {
|
||||
if (userConfig.password !== client.config.password) {
|
||||
/**
|
||||
* If we happen to reload an existing client, make super duper sure we
|
||||
* have their latest password. We're not replacing the entire config
|
||||
* object, because that could have undesired consequences.
|
||||
*
|
||||
* @see https://github.com/thelounge/thelounge/issues/598
|
||||
*/
|
||||
client.config.password = userConfig.password;
|
||||
log.info(`Password for user ${colors.bold(name)} was reset.`);
|
||||
}
|
||||
} else {
|
||||
client = new Client(this, name, userConfig);
|
||||
this.clients.push(client);
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
getUsers = function () {
|
||||
if (!fs.existsSync(Config.getUsersPath())) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fs
|
||||
.readdirSync(Config.getUsersPath())
|
||||
.filter((file) => file.endsWith(".json"))
|
||||
.map((file) => file.slice(0, -5));
|
||||
};
|
||||
|
||||
addUser(name: string, password: string, enableLog: boolean) {
|
||||
if (path.basename(name) !== name) {
|
||||
throw new Error(`${name} is an invalid username.`);
|
||||
}
|
||||
|
||||
const userPath = Config.getUserConfigPath(name);
|
||||
|
||||
if (fs.existsSync(userPath)) {
|
||||
log.error(`User ${colors.green(name)} already exists.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const user = {
|
||||
password: password || "",
|
||||
log: enableLog,
|
||||
};
|
||||
|
||||
try {
|
||||
fs.writeFileSync(userPath, JSON.stringify(user, null, "\t"), {
|
||||
mode: 0o600,
|
||||
});
|
||||
} catch (e) {
|
||||
log.error(`Failed to create user ${colors.green(name)} (${e})`);
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
const userFolderStat = fs.statSync(Config.getUsersPath());
|
||||
const userFileStat = fs.statSync(userPath);
|
||||
|
||||
if (
|
||||
userFolderStat &&
|
||||
userFileStat &&
|
||||
(userFolderStat.uid !== userFileStat.uid || userFolderStat.gid !== userFileStat.gid)
|
||||
) {
|
||||
log.warn(
|
||||
`User ${colors.green(
|
||||
name
|
||||
)} has been created, but with a different uid (or gid) than expected.`
|
||||
);
|
||||
log.warn(
|
||||
"The file owner has been changed to the expected user. " +
|
||||
"To prevent any issues, please run thelounge commands " +
|
||||
"as the correct user that owns the config folder."
|
||||
);
|
||||
log.warn(
|
||||
"See https://thelounge.chat/docs/usage#using-the-correct-system-user for more information."
|
||||
);
|
||||
fs.chownSync(userPath, userFolderStat.uid, userFolderStat.gid);
|
||||
}
|
||||
} catch (e) {
|
||||
// We're simply verifying file owner as a safe guard for users
|
||||
// that run `thelounge add` as root, so we don't care if it fails
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getDataToSave(client: Client) {
|
||||
const json = Object.assign({}, client.config, {
|
||||
networks: client.networks.map((n) => n.export()),
|
||||
});
|
||||
const newUser = JSON.stringify(json, null, "\t");
|
||||
const newHash = crypto.createHash("sha256").update(newUser).digest("hex");
|
||||
|
||||
return {newUser, newHash};
|
||||
}
|
||||
|
||||
saveUser(client: Client, callback: (err?: Error) => void) {
|
||||
const {newUser, newHash} = this.getDataToSave(client);
|
||||
|
||||
// Do not write to disk if the exported data hasn't actually changed
|
||||
if (client.fileHash === newHash) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pathReal = Config.getUserConfigPath(client.name);
|
||||
const pathTemp = pathReal + ".tmp";
|
||||
|
||||
try {
|
||||
// Write to a temp file first, in case the write fails
|
||||
// we do not lose the original file (for example when disk is full)
|
||||
fs.writeFileSync(pathTemp, newUser, {
|
||||
mode: 0o600,
|
||||
});
|
||||
fs.renameSync(pathTemp, pathReal);
|
||||
|
||||
return callback ? callback() : true;
|
||||
} catch (e) {
|
||||
log.error(`Failed to update user ${colors.green(client.name)} (${e})`);
|
||||
|
||||
if (callback) {
|
||||
callback(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeUser(name) {
|
||||
const userPath = Config.getUserConfigPath(name);
|
||||
|
||||
if (!fs.existsSync(userPath)) {
|
||||
log.error(`Tried to remove non-existing user ${colors.green(name)}.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
fs.unlinkSync(userPath);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private readUserConfig(name: string) {
|
||||
const userPath = Config.getUserConfigPath(name);
|
||||
|
||||
if (!fs.existsSync(userPath)) {
|
||||
log.error(`Tried to read non-existing user ${colors.green(name)}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = fs.readFileSync(userPath, "utf-8");
|
||||
return JSON.parse(data);
|
||||
} catch (e) {
|
||||
log.error(`Failed to read user ${colors.bold(name)}: ${e}`);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default ClientManager;
|
|
@ -1,14 +1,15 @@
|
|||
"use strict";
|
||||
|
||||
const log = require("../log");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const colors = require("chalk");
|
||||
const program = require("commander");
|
||||
const Helper = require("../helper");
|
||||
const Config = require("../config");
|
||||
const Utils = require("./utils");
|
||||
import log from "../log";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import colors from "chalk";
|
||||
import {Command} from "commander";
|
||||
import Helper from "../helper";
|
||||
import Config from "../config";
|
||||
import Utils from "./utils";
|
||||
|
||||
const program = new Command();
|
||||
program
|
||||
.version(Helper.getVersion(), "-v, --version")
|
||||
.option(
|
|
@ -1,13 +1,14 @@
|
|||
"use strict";
|
||||
|
||||
const log = require("../log");
|
||||
const colors = require("chalk");
|
||||
const semver = require("semver");
|
||||
const program = require("commander");
|
||||
const Helper = require("../helper");
|
||||
const Config = require("../config");
|
||||
const Utils = require("./utils");
|
||||
import log from "../log";
|
||||
import colors from "chalk";
|
||||
import semver from "semver";
|
||||
import Helper from "../helper";
|
||||
import Config from "../config";
|
||||
import Utils from "./utils";
|
||||
import {Command} from "commander";
|
||||
|
||||
const program = new Command();
|
||||
program
|
||||
.command("install <package>")
|
||||
.description("Install a theme or a package")
|
|
@ -1,10 +1,11 @@
|
|||
"use strict";
|
||||
|
||||
const program = require("commander");
|
||||
const Utils = require("./utils");
|
||||
const packageManager = require("../plugins/packages");
|
||||
const log = require("../log");
|
||||
import {Command} from "commander";
|
||||
import Utils from "./utils";
|
||||
import packageManager from "../plugins/packages";
|
||||
import log from "../log";
|
||||
|
||||
const program = new Command();
|
||||
program
|
||||
.command("outdated")
|
||||
.description("Check for any outdated packages")
|
|
@ -1,12 +1,14 @@
|
|||
"use strict";
|
||||
|
||||
const log = require("../log");
|
||||
const colors = require("chalk");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const program = require("commander");
|
||||
const Config = require("../config");
|
||||
const Utils = require("./utils");
|
||||
import log from "../log";
|
||||
import colors from "chalk";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import {Command} from "commander";
|
||||
import Config from "../config";
|
||||
import Utils from "./utils";
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.command("start")
|
|
@ -1,11 +1,12 @@
|
|||
"use strict";
|
||||
|
||||
const log = require("../log");
|
||||
const colors = require("chalk");
|
||||
const program = require("commander");
|
||||
const Config = require("../config");
|
||||
const Utils = require("./utils");
|
||||
import log from "../log";
|
||||
import colors from "chalk";
|
||||
import {Command} from "commander";
|
||||
import Config from "../config";
|
||||
import Utils from "./utils";
|
||||
|
||||
const program = new Command();
|
||||
program
|
||||
.command("uninstall <package>")
|
||||
.description("Uninstall a theme or a package")
|
|
@ -2,10 +2,11 @@
|
|||
|
||||
const log = require("../log");
|
||||
const colors = require("chalk");
|
||||
const program = require("commander");
|
||||
const {Command} = require("commander");
|
||||
const Config = require("../config");
|
||||
const Utils = require("./utils");
|
||||
|
||||
const program = new Command();
|
||||
program
|
||||
.command("upgrade [packages...]")
|
||||
.description("Upgrade installed themes and packages to their latest versions")
|
|
@ -1,14 +1,15 @@
|
|||
"use strict";
|
||||
|
||||
const _ = require("lodash");
|
||||
const log = require("../log");
|
||||
const colors = require("chalk");
|
||||
const fs = require("fs");
|
||||
const Helper = require("../helper");
|
||||
const Config = require("../config");
|
||||
const path = require("path");
|
||||
import _ from "lodash";
|
||||
import log from "../log";
|
||||
import colors from "chalk";
|
||||
import fs from "fs";
|
||||
import Helper from "../helper";
|
||||
import Config from "../config";
|
||||
import path from "path";
|
||||
import {spawn} from "child_process";
|
||||
|
||||
let home;
|
||||
let home: string;
|
||||
|
||||
class Utils {
|
||||
static extraHelp() {
|
||||
|
@ -120,7 +121,7 @@ class Utils {
|
|||
|
||||
return new Promise((resolve, reject) => {
|
||||
let success = false;
|
||||
const add = require("child_process").spawn(
|
||||
const add = spawn(
|
||||
process.execPath,
|
||||
[yarn, command, ...staticParameters, ...parameters],
|
||||
{env: env}
|
||||
|
@ -168,10 +169,10 @@ class Utils {
|
|||
return reject(code);
|
||||
}
|
||||
|
||||
resolve();
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Utils;
|
||||
export default Utils;
|
|
@ -1,16 +1,19 @@
|
|||
"use strict";
|
||||
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const _ = require("lodash");
|
||||
const colors = require("chalk");
|
||||
const log = require("./log");
|
||||
const Helper = require("./helper");
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import os from "os";
|
||||
import _ from "lodash";
|
||||
import colors from "chalk";
|
||||
import log from "./log";
|
||||
import Helper from "./helper";
|
||||
import {Config as ConfigType} from "src/types/config";
|
||||
|
||||
class Config {
|
||||
values = require(path.resolve(path.join(__dirname, "..", "defaults", "config.js")));
|
||||
#homePath;
|
||||
values = require(path.resolve(
|
||||
path.join(__dirname, "..", "defaults", "config.js")
|
||||
)) as ConfigType;
|
||||
#homePath: string;
|
||||
|
||||
getHomePath() {
|
||||
return this.#homePath;
|
||||
|
@ -36,7 +39,7 @@ class Config {
|
|||
return path.join(this.#homePath, "users");
|
||||
}
|
||||
|
||||
getUserConfigPath(name) {
|
||||
getUserConfigPath(name: string) {
|
||||
return path.join(this.getUsersPath(), `${name}.json`);
|
||||
}
|
||||
|
||||
|
@ -48,7 +51,7 @@ class Config {
|
|||
return path.join(this.#homePath, "packages");
|
||||
}
|
||||
|
||||
getPackageModulePath(packageName) {
|
||||
getPackageModulePath(packageName: string) {
|
||||
return path.join(this.getPackagesPath(), "node_modules", packageName);
|
||||
}
|
||||
|
||||
|
@ -57,14 +60,16 @@ class Config {
|
|||
return "thelounge";
|
||||
}
|
||||
|
||||
return this.values.defaults.nick.replace(/%/g, () => Math.floor(Math.random() * 10));
|
||||
return this.values.defaults.nick.replace(/%/g, () =>
|
||||
Math.floor(Math.random() * 10).toString()
|
||||
);
|
||||
}
|
||||
|
||||
merge(newConfig) {
|
||||
merge(newConfig: ConfigType) {
|
||||
this._merge_config_objects(this.values, newConfig);
|
||||
}
|
||||
|
||||
_merge_config_objects(oldConfig, newConfig) {
|
||||
_merge_config_objects(oldConfig: ConfigType, newConfig: ConfigType) {
|
||||
// semi exposed function so that we can test it
|
||||
// it mutates the oldConfig, but returns it as a convenience for testing
|
||||
|
||||
|
@ -93,7 +98,7 @@ class Config {
|
|||
});
|
||||
}
|
||||
|
||||
setHome(newPath) {
|
||||
setHome(newPath: string) {
|
||||
this.#homePath = Helper.expandHome(newPath);
|
||||
|
||||
// Reload config from new home location
|
||||
|
@ -179,4 +184,4 @@ class Config {
|
|||
}
|
||||
}
|
||||
|
||||
module.exports = new Config();
|
||||
export default new Config();
|
|
@ -1,13 +1,14 @@
|
|||
"use strict";
|
||||
|
||||
const pkg = require("../package.json");
|
||||
const _ = require("lodash");
|
||||
const path = require("path");
|
||||
const os = require("os");
|
||||
const fs = require("fs");
|
||||
const net = require("net");
|
||||
const bcrypt = require("bcryptjs");
|
||||
const crypto = require("crypto");
|
||||
import pkg from "../package.json";
|
||||
import _ from "lodash";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import fs from "fs";
|
||||
import net from "net";
|
||||
import bcrypt from "bcryptjs";
|
||||
import crypto from "crypto";
|
||||
import User from "./models/user";
|
||||
|
||||
const Helper = {
|
||||
expandHome,
|
||||
|
@ -27,7 +28,7 @@ const Helper = {
|
|||
},
|
||||
};
|
||||
|
||||
module.exports = Helper;
|
||||
export default Helper;
|
||||
|
||||
function getVersion() {
|
||||
const gitCommit = getGitCommit();
|
||||
|
@ -73,7 +74,7 @@ function getVersionCacheBust() {
|
|||
return hash.substring(0, 10);
|
||||
}
|
||||
|
||||
function ip2hex(address) {
|
||||
function ip2hex(address: string) {
|
||||
// no ipv6 support
|
||||
if (!net.isIPv4(address)) {
|
||||
return "00000000";
|
||||
|
@ -95,7 +96,7 @@ function ip2hex(address) {
|
|||
|
||||
// Expand ~ into the current user home dir.
|
||||
// This does *not* support `~other_user/tmp` => `/home/other_user/tmp`.
|
||||
function expandHome(shortenedPath) {
|
||||
function expandHome(shortenedPath: string) {
|
||||
if (!shortenedPath) {
|
||||
return "";
|
||||
}
|
||||
|
@ -104,19 +105,19 @@ function expandHome(shortenedPath) {
|
|||
return path.resolve(shortenedPath.replace(/^~($|\/|\\)/, home + "$1"));
|
||||
}
|
||||
|
||||
function passwordRequiresUpdate(password) {
|
||||
function passwordRequiresUpdate(password: string) {
|
||||
return bcrypt.getRounds(password) !== 11;
|
||||
}
|
||||
|
||||
function passwordHash(password) {
|
||||
function passwordHash(password: string) {
|
||||
return bcrypt.hashSync(password, bcrypt.genSaltSync(11));
|
||||
}
|
||||
|
||||
function passwordCompare(password, expected) {
|
||||
function passwordCompare(password: string, expected: string) {
|
||||
return bcrypt.compare(password, expected);
|
||||
}
|
||||
|
||||
function parseHostmask(hostmask) {
|
||||
function parseHostmask(hostmask: string): Hostmask {
|
||||
let nick = "";
|
||||
let ident = "*";
|
||||
let hostname = "*";
|
||||
|
@ -152,7 +153,7 @@ function parseHostmask(hostmask) {
|
|||
return result;
|
||||
}
|
||||
|
||||
function compareHostmask(a, b) {
|
||||
function compareHostmask(a: Hostmask, b: Hostmask) {
|
||||
return (
|
||||
compareWithWildcard(a.nick, b.nick) &&
|
||||
compareWithWildcard(a.ident, b.ident) &&
|
||||
|
@ -160,7 +161,7 @@ function compareHostmask(a, b) {
|
|||
);
|
||||
}
|
||||
|
||||
function compareWithWildcard(a, b) {
|
||||
function compareWithWildcard(a: string, b: string) {
|
||||
// we allow '*' and '?' wildcards in our comparison.
|
||||
// this is mostly aligned with https://modern.ircdocs.horse/#wildcard-expressions
|
||||
// but we do not support the escaping. The ABNF does not seem to be clear as to
|
|
@ -1,14 +1,18 @@
|
|||
"use strict";
|
||||
|
||||
const log = require("./log");
|
||||
const fs = require("fs");
|
||||
const net = require("net");
|
||||
const colors = require("chalk");
|
||||
const Helper = require("./helper");
|
||||
const Config = require("./config");
|
||||
import log from "./log";
|
||||
import fs from "fs";
|
||||
import net from "net";
|
||||
import colors from "chalk";
|
||||
import Helper from "./helper";
|
||||
import Config from "./config";
|
||||
|
||||
class Identification {
|
||||
constructor(startedCallback) {
|
||||
private connectionId: number;
|
||||
private connections: Map<any, any>;
|
||||
private oidentdFile: string;
|
||||
|
||||
constructor(startedCallback: Function) {
|
||||
this.connectionId = 0;
|
||||
this.connections = new Map();
|
||||
|
||||
|
@ -39,11 +43,15 @@ class Identification {
|
|||
},
|
||||
() => {
|
||||
const address = server.address();
|
||||
log.info(
|
||||
`Identd server available on ${colors.green(
|
||||
address.address + ":" + address.port
|
||||
)}`
|
||||
);
|
||||
if (typeof address === "string") {
|
||||
log.info(`Identd server available on ${colors.green(address)}`);
|
||||
} else if (address.address) {
|
||||
log.info(
|
||||
`Identd server available on ${colors.green(
|
||||
address.address + ":" + address.port
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
startedCallback(this);
|
||||
}
|
||||
|
@ -120,4 +128,4 @@ class Identification {
|
|||
}
|
||||
}
|
||||
|
||||
module.exports = Identification;
|
||||
export default Identification;
|
1
src/index.d.ts
vendored
Normal file
1
src/index.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/// <reference path="types/index.d.ts" />
|
|
@ -1,304 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
const _ = require("lodash");
|
||||
const log = require("../log");
|
||||
const Config = require("../config");
|
||||
const User = require("./user");
|
||||
const Msg = require("./msg");
|
||||
const storage = require("../plugins/storage");
|
||||
|
||||
module.exports = Chan;
|
||||
|
||||
Chan.Type = {
|
||||
CHANNEL: "channel",
|
||||
LOBBY: "lobby",
|
||||
QUERY: "query",
|
||||
SPECIAL: "special",
|
||||
};
|
||||
|
||||
Chan.SpecialType = {
|
||||
BANLIST: "list_bans",
|
||||
INVITELIST: "list_invites",
|
||||
CHANNELLIST: "list_channels",
|
||||
IGNORELIST: "list_ignored",
|
||||
};
|
||||
|
||||
Chan.State = {
|
||||
PARTED: 0,
|
||||
JOINED: 1,
|
||||
};
|
||||
|
||||
function Chan(attr) {
|
||||
_.defaults(this, attr, {
|
||||
id: 0,
|
||||
messages: [],
|
||||
name: "",
|
||||
key: "",
|
||||
topic: "",
|
||||
type: Chan.Type.CHANNEL,
|
||||
state: Chan.State.PARTED,
|
||||
firstUnread: 0,
|
||||
unread: 0,
|
||||
highlight: 0,
|
||||
users: new Map(),
|
||||
muted: false,
|
||||
});
|
||||
}
|
||||
|
||||
Chan.prototype.destroy = function () {
|
||||
this.dereferencePreviews(this.messages);
|
||||
};
|
||||
|
||||
Chan.prototype.pushMessage = function (client, msg, increasesUnread) {
|
||||
const chan = this.id;
|
||||
const obj = {chan, msg};
|
||||
|
||||
msg.id = client.idMsg++;
|
||||
|
||||
// If this channel is open in any of the clients, do not increase unread counter
|
||||
const isOpen = _.find(client.attachedClients, {openChannel: chan}) !== undefined;
|
||||
|
||||
if (msg.self) {
|
||||
// reset counters/markers when receiving self-/echo-message
|
||||
this.unread = 0;
|
||||
this.firstUnread = msg.id;
|
||||
this.highlight = 0;
|
||||
} else if (!isOpen) {
|
||||
if (!this.firstUnread) {
|
||||
this.firstUnread = msg.id;
|
||||
}
|
||||
|
||||
if (increasesUnread || msg.highlight) {
|
||||
obj.unread = ++this.unread;
|
||||
}
|
||||
|
||||
if (msg.highlight) {
|
||||
obj.highlight = ++this.highlight;
|
||||
}
|
||||
}
|
||||
|
||||
client.emit("msg", obj);
|
||||
|
||||
// Never store messages in public mode as the session
|
||||
// is completely destroyed when the page gets closed
|
||||
if (Config.values.public) {
|
||||
return;
|
||||
}
|
||||
|
||||
// showInActive is only processed on "msg", don't need it on page reload
|
||||
if (msg.showInActive) {
|
||||
delete msg.showInActive;
|
||||
}
|
||||
|
||||
this.writeUserLog(client, msg);
|
||||
|
||||
if (Config.values.maxHistory >= 0 && this.messages.length > Config.values.maxHistory) {
|
||||
const deleted = this.messages.splice(0, this.messages.length - Config.values.maxHistory);
|
||||
|
||||
// If maxHistory is 0, image would be dereferenced before client had a chance to retrieve it,
|
||||
// so for now, just don't implement dereferencing for this edge case.
|
||||
if (Config.values.maxHistory > 0) {
|
||||
this.dereferencePreviews(deleted);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Chan.prototype.dereferencePreviews = function (messages) {
|
||||
if (!Config.values.prefetch || !Config.values.prefetchStorage) {
|
||||
return;
|
||||
}
|
||||
|
||||
messages.forEach((message) => {
|
||||
if (message.previews) {
|
||||
message.previews.forEach((preview) => {
|
||||
if (preview.thumb) {
|
||||
storage.dereference(preview.thumb);
|
||||
preview.thumb = "";
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Chan.prototype.getSortedUsers = function (irc) {
|
||||
const users = Array.from(this.users.values());
|
||||
|
||||
if (!irc || !irc.network || !irc.network.options || !irc.network.options.PREFIX) {
|
||||
return users;
|
||||
}
|
||||
|
||||
const userModeSortPriority = {};
|
||||
irc.network.options.PREFIX.forEach((prefix, index) => {
|
||||
userModeSortPriority[prefix.symbol] = index;
|
||||
});
|
||||
|
||||
userModeSortPriority[""] = 99; // No mode is lowest
|
||||
|
||||
return users.sort(function (a, b) {
|
||||
if (a.mode === b.mode) {
|
||||
return a.nick.toLowerCase() < b.nick.toLowerCase() ? -1 : 1;
|
||||
}
|
||||
|
||||
return userModeSortPriority[a.mode] - userModeSortPriority[b.mode];
|
||||
});
|
||||
};
|
||||
|
||||
Chan.prototype.findMessage = function (msgId) {
|
||||
return this.messages.find((message) => message.id === msgId);
|
||||
};
|
||||
|
||||
Chan.prototype.findUser = function (nick) {
|
||||
return this.users.get(nick.toLowerCase());
|
||||
};
|
||||
|
||||
Chan.prototype.getUser = function (nick) {
|
||||
return this.findUser(nick) || new User({nick});
|
||||
};
|
||||
|
||||
Chan.prototype.setUser = function (user) {
|
||||
this.users.set(user.nick.toLowerCase(), user);
|
||||
};
|
||||
|
||||
Chan.prototype.removeUser = function (user) {
|
||||
this.users.delete(user.nick.toLowerCase());
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a clean clone of this channel that will be sent to the client.
|
||||
* This function performs manual cloning of channel object for
|
||||
* better control of performance and memory usage.
|
||||
*
|
||||
* @param {(int|bool)} lastActiveChannel - Last known active user channel id (needed to control how many messages are sent)
|
||||
* If true, channel is assumed active.
|
||||
* @param {int} lastMessage - Last message id seen by active client to avoid sending duplicates.
|
||||
*/
|
||||
Chan.prototype.getFilteredClone = function (lastActiveChannel, lastMessage) {
|
||||
return Object.keys(this).reduce((newChannel, prop) => {
|
||||
if (prop === "users") {
|
||||
// Do not send users, client requests updated user list whenever needed
|
||||
newChannel[prop] = [];
|
||||
} else if (prop === "messages") {
|
||||
// If client is reconnecting, only send new messages that client has not seen yet
|
||||
if (lastMessage > -1) {
|
||||
// When reconnecting, always send up to 100 messages to prevent message gaps on the client
|
||||
// See https://github.com/thelounge/thelounge/issues/1883
|
||||
newChannel[prop] = this[prop].filter((m) => m.id > lastMessage).slice(-100);
|
||||
} else {
|
||||
// If channel is active, send up to 100 last messages, for all others send just 1
|
||||
// Client will automatically load more messages whenever needed based on last seen messages
|
||||
const messagesToSend =
|
||||
lastActiveChannel === true || this.id === lastActiveChannel ? 100 : 1;
|
||||
|
||||
newChannel[prop] = this[prop].slice(-messagesToSend);
|
||||
}
|
||||
|
||||
newChannel.totalMessages = this[prop].length;
|
||||
} else {
|
||||
newChannel[prop] = this[prop];
|
||||
}
|
||||
|
||||
return newChannel;
|
||||
}, {});
|
||||
};
|
||||
|
||||
Chan.prototype.writeUserLog = function (client, msg) {
|
||||
this.messages.push(msg);
|
||||
|
||||
// Are there any logs enabled
|
||||
if (client.messageStorage.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let targetChannel = this;
|
||||
|
||||
// Is this particular message or channel loggable
|
||||
if (!msg.isLoggable() || !this.isLoggable()) {
|
||||
// Because notices are nasty and can be shown in active channel on the client
|
||||
// if there is no open query, we want to always log notices in the sender's name
|
||||
if (msg.type === Msg.Type.NOTICE && msg.showInActive) {
|
||||
targetChannel = {
|
||||
name: msg.from.nick,
|
||||
};
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the parent network where this channel is in
|
||||
const target = client.find(this.id);
|
||||
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const messageStorage of client.messageStorage) {
|
||||
messageStorage.index(target.network, targetChannel, msg);
|
||||
}
|
||||
};
|
||||
|
||||
Chan.prototype.loadMessages = function (client, network) {
|
||||
if (!this.isLoggable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!network.irc) {
|
||||
// Network created, but misconfigured
|
||||
log.warn(
|
||||
`Failed to load messages for ${client.name}, network ${network.name} is not initialized.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!client.messageProvider) {
|
||||
if (network.irc.network.cap.isEnabled("znc.in/playback")) {
|
||||
// if we do have a message provider we might be able to only fetch partial history,
|
||||
// so delay the cap in this case.
|
||||
requestZncPlayback(this, network, 0);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
client.messageProvider
|
||||
.getMessages(network, this)
|
||||
.then((messages) => {
|
||||
if (messages.length === 0) {
|
||||
if (network.irc.network.cap.isEnabled("znc.in/playback")) {
|
||||
requestZncPlayback(this, network, 0);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.messages.unshift(...messages);
|
||||
|
||||
if (!this.firstUnread) {
|
||||
this.firstUnread = messages[messages.length - 1].id;
|
||||
}
|
||||
|
||||
client.emit("more", {
|
||||
chan: this.id,
|
||||
messages: messages.slice(-100),
|
||||
totalMessages: messages.length,
|
||||
});
|
||||
|
||||
if (network.irc.network.cap.isEnabled("znc.in/playback")) {
|
||||
const from = Math.floor(messages[messages.length - 1].time.getTime() / 1000);
|
||||
|
||||
requestZncPlayback(this, network, from);
|
||||
}
|
||||
})
|
||||
.catch((err) => log.error(`Failed to load messages for ${client.name}: ${err}`));
|
||||
};
|
||||
|
||||
Chan.prototype.isLoggable = function () {
|
||||
return this.type === Chan.Type.CHANNEL || this.type === Chan.Type.QUERY;
|
||||
};
|
||||
|
||||
Chan.prototype.setMuteStatus = function (muted) {
|
||||
this.muted = !!muted;
|
||||
};
|
||||
|
||||
function requestZncPlayback(channel, network, from) {
|
||||
network.irc.raw("ZNC", "*playback", "PLAY", channel.name, from.toString());
|
||||
}
|
293
src/models/chan.ts
Normal file
293
src/models/chan.ts
Normal file
|
@ -0,0 +1,293 @@
|
|||
"use strict";
|
||||
|
||||
import _ from "lodash";
|
||||
import log from "../log";
|
||||
import Config from "../config";
|
||||
import User from "./user";
|
||||
import Msg from "./msg";
|
||||
import storage from "../plugins/storage";
|
||||
import {ChanState, ChanType, FilteredChannel} from "src/types/models/channel";
|
||||
import Client from "src/client";
|
||||
import Network from "./network";
|
||||
import {MessageType} from "src/types/models/message";
|
||||
|
||||
class Chan {
|
||||
id: number;
|
||||
messages: Msg[];
|
||||
name: string;
|
||||
key: string;
|
||||
topic: string;
|
||||
firstUnread: number;
|
||||
unread: number;
|
||||
highlight: number;
|
||||
users: Map<string, User>;
|
||||
muted: boolean;
|
||||
type: ChanType;
|
||||
state: ChanState;
|
||||
|
||||
constructor(attr: Partial<Chan>) {
|
||||
_.defaults(this, attr, {
|
||||
id: 0,
|
||||
messages: [],
|
||||
name: "",
|
||||
key: "",
|
||||
topic: "",
|
||||
type: ChanType.CHANNEL,
|
||||
state: ChanState.PARTED,
|
||||
firstUnread: 0,
|
||||
unread: 0,
|
||||
highlight: 0,
|
||||
users: new Map(),
|
||||
muted: false,
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.dereferencePreviews(this.messages);
|
||||
}
|
||||
|
||||
pushMessage(client: Client, msg: Msg, increasesUnread: boolean) {
|
||||
const chan = this.id;
|
||||
const obj = {chan, msg} as any;
|
||||
|
||||
msg.id = client.idMsg++;
|
||||
|
||||
// If this channel is open in any of the clients, do not increase unread counter
|
||||
const isOpen = _.find(client.attachedClients, {openChannel: chan}) !== undefined;
|
||||
|
||||
if (msg.self) {
|
||||
// reset counters/markers when receiving self-/echo-message
|
||||
this.unread = 0;
|
||||
this.firstUnread = msg.id;
|
||||
this.highlight = 0;
|
||||
} else if (!isOpen) {
|
||||
if (!this.firstUnread) {
|
||||
this.firstUnread = msg.id;
|
||||
}
|
||||
|
||||
if (increasesUnread || msg.highlight) {
|
||||
obj.unread = ++this.unread;
|
||||
}
|
||||
|
||||
if (msg.highlight) {
|
||||
obj.highlight = ++this.highlight;
|
||||
}
|
||||
}
|
||||
|
||||
client.emit("msg", obj);
|
||||
|
||||
// Never store messages in public mode as the session
|
||||
// is completely destroyed when the page gets closed
|
||||
if (Config.values.public) {
|
||||
return;
|
||||
}
|
||||
|
||||
// showInActive is only processed on "msg", don't need it on page reload
|
||||
if (msg.showInActive) {
|
||||
delete msg.showInActive;
|
||||
}
|
||||
|
||||
this.writeUserLog(client, msg);
|
||||
|
||||
if (Config.values.maxHistory >= 0 && this.messages.length > Config.values.maxHistory) {
|
||||
const deleted = this.messages.splice(
|
||||
0,
|
||||
this.messages.length - Config.values.maxHistory
|
||||
);
|
||||
|
||||
// If maxHistory is 0, image would be dereferenced before client had a chance to retrieve it,
|
||||
// so for now, just don't implement dereferencing for this edge case.
|
||||
if (Config.values.maxHistory > 0) {
|
||||
this.dereferencePreviews(deleted);
|
||||
}
|
||||
}
|
||||
}
|
||||
dereferencePreviews(messages) {
|
||||
if (!Config.values.prefetch || !Config.values.prefetchStorage) {
|
||||
return;
|
||||
}
|
||||
|
||||
messages.forEach((message) => {
|
||||
if (message.previews) {
|
||||
message.previews.forEach((preview) => {
|
||||
if (preview.thumb) {
|
||||
storage.dereference(preview.thumb);
|
||||
preview.thumb = "";
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
getSortedUsers(irc) {
|
||||
const users = Array.from(this.users.values());
|
||||
|
||||
if (!irc || !irc.network || !irc.network.options || !irc.network.options.PREFIX) {
|
||||
return users;
|
||||
}
|
||||
|
||||
const userModeSortPriority = {};
|
||||
irc.network.options.PREFIX.forEach((prefix, index) => {
|
||||
userModeSortPriority[prefix.symbol] = index;
|
||||
});
|
||||
|
||||
userModeSortPriority[""] = 99; // No mode is lowest
|
||||
|
||||
return users.sort(function (a, b) {
|
||||
if (a.mode === b.mode) {
|
||||
return a.nick.toLowerCase() < b.nick.toLowerCase() ? -1 : 1;
|
||||
}
|
||||
|
||||
return userModeSortPriority[a.mode] - userModeSortPriority[b.mode];
|
||||
});
|
||||
}
|
||||
findMessage(msgId: number) {
|
||||
return this.messages.find((message) => message.id === msgId);
|
||||
}
|
||||
findUser(nick: string) {
|
||||
return this.users.get(nick.toLowerCase());
|
||||
}
|
||||
getUser(nick: string) {
|
||||
return this.findUser(nick) || new User({nick});
|
||||
}
|
||||
setUser(user: User) {
|
||||
this.users.set(user.nick.toLowerCase(), user);
|
||||
}
|
||||
removeUser(user: User) {
|
||||
this.users.delete(user.nick.toLowerCase());
|
||||
}
|
||||
/**
|
||||
* Get a clean clone of this channel that will be sent to the client.
|
||||
* This function performs manual cloning of channel object for
|
||||
* better control of performance and memory usage.
|
||||
*
|
||||
* @param {(int|bool)} lastActiveChannel - Last known active user channel id (needed to control how many messages are sent)
|
||||
* If true, channel is assumed active.
|
||||
* @param {int} lastMessage - Last message id seen by active client to avoid sending duplicates.
|
||||
*/
|
||||
getFilteredClone(lastActiveChannel: number | boolean, lastMessage: number): FilteredChannel {
|
||||
return Object.keys(this).reduce((newChannel, prop) => {
|
||||
if (prop === "users") {
|
||||
// Do not send users, client requests updated user list whenever needed
|
||||
newChannel[prop] = [];
|
||||
} else if (prop === "messages") {
|
||||
// If client is reconnecting, only send new messages that client has not seen yet
|
||||
if (lastMessage > -1) {
|
||||
// When reconnecting, always send up to 100 messages to prevent message gaps on the client
|
||||
// See https://github.com/thelounge/thelounge/issues/1883
|
||||
newChannel[prop] = this[prop].filter((m) => m.id > lastMessage).slice(-100);
|
||||
} else {
|
||||
// If channel is active, send up to 100 last messages, for all others send just 1
|
||||
// Client will automatically load more messages whenever needed based on last seen messages
|
||||
const messagesToSend =
|
||||
lastActiveChannel === true || this.id === lastActiveChannel ? 100 : 1;
|
||||
|
||||
newChannel[prop] = this[prop].slice(-messagesToSend);
|
||||
}
|
||||
|
||||
(newChannel as FilteredChannel).totalMessages = this[prop].length;
|
||||
} else {
|
||||
newChannel[prop] = this[prop];
|
||||
}
|
||||
|
||||
return newChannel;
|
||||
}, {}) as FilteredChannel;
|
||||
}
|
||||
writeUserLog(client: Client, msg: Msg) {
|
||||
this.messages.push(msg);
|
||||
|
||||
// Are there any logs enabled
|
||||
if (client.messageStorage.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let targetChannel: Chan = this;
|
||||
|
||||
// Is this particular message or channel loggable
|
||||
if (!msg.isLoggable() || !this.isLoggable()) {
|
||||
// Because notices are nasty and can be shown in active channel on the client
|
||||
// if there is no open query, we want to always log notices in the sender's name
|
||||
if (msg.type === MessageType.NOTICE && msg.showInActive) {
|
||||
targetChannel.name = msg.from.nick;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the parent network where this channel is in
|
||||
const target = client.find(this.id);
|
||||
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const messageStorage of client.messageStorage) {
|
||||
messageStorage.index(target.network, targetChannel, msg);
|
||||
}
|
||||
}
|
||||
loadMessages(client: Client, network: Network) {
|
||||
if (!this.isLoggable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!network.irc) {
|
||||
// Network created, but misconfigured
|
||||
log.warn(
|
||||
`Failed to load messages for ${client.name}, network ${network.name} is not initialized.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!client.messageProvider) {
|
||||
if (network.irc.network.cap.isEnabled("znc.in/playback")) {
|
||||
// if we do have a message provider we might be able to only fetch partial history,
|
||||
// so delay the cap in this case.
|
||||
requestZncPlayback(this, network, 0);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
client.messageProvider
|
||||
.getMessages(network, this)
|
||||
.then((messages) => {
|
||||
if (messages.length === 0) {
|
||||
if (network.irc.network.cap.isEnabled("znc.in/playback")) {
|
||||
requestZncPlayback(this, network, 0);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.messages.unshift(...messages);
|
||||
|
||||
if (!this.firstUnread) {
|
||||
this.firstUnread = messages[messages.length - 1].id;
|
||||
}
|
||||
|
||||
client.emit("more", {
|
||||
chan: this.id,
|
||||
messages: messages.slice(-100),
|
||||
totalMessages: messages.length,
|
||||
});
|
||||
|
||||
if (network.irc.network.cap.isEnabled("znc.in/playback")) {
|
||||
const from = Math.floor(messages[messages.length - 1].time.getTime() / 1000);
|
||||
|
||||
requestZncPlayback(this, network, from);
|
||||
}
|
||||
})
|
||||
.catch((err) => log.error(`Failed to load messages for ${client.name}: ${err}`));
|
||||
}
|
||||
isLoggable() {
|
||||
return this.type === ChanType.CHANNEL || this.type === ChanType.QUERY;
|
||||
}
|
||||
setMuteStatus(muted) {
|
||||
this.muted = !!muted;
|
||||
}
|
||||
}
|
||||
|
||||
function requestZncPlayback(channel, network, from) {
|
||||
network.irc.raw("ZNC", "*playback", "PLAY", channel.name, from.toString());
|
||||
}
|
||||
|
||||
export default Chan;
|
|
@ -1,92 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
const _ = require("lodash");
|
||||
|
||||
class Msg {
|
||||
constructor(attr) {
|
||||
// Some properties need to be copied in the Msg object instead of referenced
|
||||
if (attr) {
|
||||
["from", "target"].forEach((prop) => {
|
||||
if (attr[prop]) {
|
||||
this[prop] = {
|
||||
mode: attr[prop].mode,
|
||||
nick: attr[prop].nick,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_.defaults(this, attr, {
|
||||
from: {},
|
||||
id: 0,
|
||||
previews: [],
|
||||
text: "",
|
||||
type: Msg.Type.MESSAGE,
|
||||
self: false,
|
||||
});
|
||||
|
||||
if (this.time > 0) {
|
||||
this.time = new Date(this.time);
|
||||
} else {
|
||||
this.time = new Date();
|
||||
}
|
||||
}
|
||||
|
||||
findPreview(link) {
|
||||
return this.previews.find((preview) => preview.link === link);
|
||||
}
|
||||
|
||||
isLoggable() {
|
||||
if (this.type === Msg.Type.TOPIC) {
|
||||
// Do not log topic that is sent on channel join
|
||||
return !!this.from.nick;
|
||||
}
|
||||
|
||||
switch (this.type) {
|
||||
case Msg.Type.MONOSPACE_BLOCK:
|
||||
case Msg.Type.ERROR:
|
||||
case Msg.Type.TOPIC_SET_BY:
|
||||
case Msg.Type.MODE_CHANNEL:
|
||||
case Msg.Type.MODE_USER:
|
||||
case Msg.Type.RAW:
|
||||
case Msg.Type.WHOIS:
|
||||
case Msg.Type.PLUGIN:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Msg.Type = {
|
||||
UNHANDLED: "unhandled",
|
||||
ACTION: "action",
|
||||
AWAY: "away",
|
||||
BACK: "back",
|
||||
ERROR: "error",
|
||||
INVITE: "invite",
|
||||
JOIN: "join",
|
||||
KICK: "kick",
|
||||
LOGIN: "login",
|
||||
LOGOUT: "logout",
|
||||
MESSAGE: "message",
|
||||
MODE: "mode",
|
||||
MODE_CHANNEL: "mode_channel",
|
||||
MODE_USER: "mode_user", // RPL_UMODEIS
|
||||
MONOSPACE_BLOCK: "monospace_block",
|
||||
NICK: "nick",
|
||||
NOTICE: "notice",
|
||||
PART: "part",
|
||||
QUIT: "quit",
|
||||
CTCP: "ctcp",
|
||||
CTCP_REQUEST: "ctcp_request",
|
||||
CHGHOST: "chghost",
|
||||
TOPIC: "topic",
|
||||
TOPIC_SET_BY: "topic_set_by",
|
||||
WHOIS: "whois",
|
||||
RAW: "raw",
|
||||
PLUGIN: "plugin",
|
||||
WALLOPS: "wallops",
|
||||
};
|
||||
|
||||
module.exports = Msg;
|
80
src/models/msg.ts
Normal file
80
src/models/msg.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
"use strict";
|
||||
|
||||
import _ from "lodash";
|
||||
import {UserInMessage, MessagePreview, MessageType} from "src/types/models/message";
|
||||
|
||||
class Msg {
|
||||
from: UserInMessage;
|
||||
id: number;
|
||||
previews: MessagePreview[];
|
||||
text: string;
|
||||
type: MessageType;
|
||||
self: boolean;
|
||||
time: Date;
|
||||
hostmask: string;
|
||||
target: UserInMessage;
|
||||
// TODO: new_nick is only on MessageType.NICK,
|
||||
// we should probably make Msgs that extend this class and use those
|
||||
// throughout. I'll leave any similar fields below.
|
||||
new_nick: string;
|
||||
highlight: boolean;
|
||||
showInActive: boolean;
|
||||
new_ident: string;
|
||||
new_host: string;
|
||||
|
||||
constructor(attr: Partial<Msg>) {
|
||||
// Some properties need to be copied in the Msg object instead of referenced
|
||||
if (attr) {
|
||||
["from", "target"].forEach((prop) => {
|
||||
if (attr[prop]) {
|
||||
this[prop] = {
|
||||
mode: attr[prop].mode,
|
||||
nick: attr[prop].nick,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_.defaults(this, attr, {
|
||||
from: {},
|
||||
id: 0,
|
||||
previews: [],
|
||||
text: "",
|
||||
type: MessageType.MESSAGE,
|
||||
self: false,
|
||||
});
|
||||
|
||||
if (this.time.getTime() > 0) {
|
||||
this.time = new Date(this.time);
|
||||
} else {
|
||||
this.time = new Date();
|
||||
}
|
||||
}
|
||||
|
||||
findPreview(link: string) {
|
||||
return this.previews.find((preview) => preview.link === link);
|
||||
}
|
||||
|
||||
isLoggable() {
|
||||
if (this.type === MessageType.TOPIC) {
|
||||
// Do not log topic that is sent on channel join
|
||||
return !!this.from.nick;
|
||||
}
|
||||
|
||||
switch (this.type) {
|
||||
case MessageType.MONOSPACE_BLOCK:
|
||||
case MessageType.ERROR:
|
||||
case MessageType.TOPIC_SET_BY:
|
||||
case MessageType.MODE_CHANNEL:
|
||||
case MessageType.MODE_USER:
|
||||
case MessageType.RAW:
|
||||
case MessageType.WHOIS:
|
||||
case MessageType.PLUGIN:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Msg;
|
|
@ -1,558 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
const _ = require("lodash");
|
||||
const {v4: uuidv4} = require("uuid");
|
||||
const IrcFramework = require("irc-framework");
|
||||
const Chan = require("./chan");
|
||||
const Msg = require("./msg");
|
||||
const Prefix = require("./prefix");
|
||||
const Helper = require("../helper");
|
||||
const Config = require("../config");
|
||||
const STSPolicies = require("../plugins/sts");
|
||||
const ClientCertificate = require("../plugins/clientCertificate");
|
||||
|
||||
module.exports = Network;
|
||||
|
||||
/**
|
||||
* @type {Object} List of keys which should be sent to the client by default.
|
||||
*/
|
||||
const fieldsForClient = {
|
||||
uuid: true,
|
||||
name: true,
|
||||
nick: true,
|
||||
serverOptions: true,
|
||||
};
|
||||
|
||||
function Network(attr) {
|
||||
_.defaults(this, attr, {
|
||||
name: "",
|
||||
nick: "",
|
||||
host: "",
|
||||
port: 6667,
|
||||
tls: false,
|
||||
userDisconnected: false,
|
||||
rejectUnauthorized: false,
|
||||
password: "",
|
||||
awayMessage: "",
|
||||
commands: [],
|
||||
username: "",
|
||||
realname: "",
|
||||
leaveMessage: "",
|
||||
sasl: "",
|
||||
saslAccount: "",
|
||||
saslPassword: "",
|
||||
channels: [],
|
||||
irc: null,
|
||||
serverOptions: {
|
||||
CHANTYPES: ["#", "&"],
|
||||
PREFIX: new Prefix([
|
||||
{symbol: "!", mode: "Y"},
|
||||
{symbol: "@", mode: "o"},
|
||||
{symbol: "%", mode: "h"},
|
||||
{symbol: "+", mode: "v"},
|
||||
]),
|
||||
NETWORK: "",
|
||||
},
|
||||
|
||||
proxyHost: "",
|
||||
proxyPort: 1080,
|
||||
proxyUsername: "",
|
||||
proxyPassword: "",
|
||||
proxyEnabled: false,
|
||||
|
||||
chanCache: [],
|
||||
ignoreList: [],
|
||||
keepNick: null,
|
||||
});
|
||||
|
||||
if (!this.uuid) {
|
||||
this.uuid = uuidv4();
|
||||
}
|
||||
|
||||
if (!this.name) {
|
||||
this.name = this.host;
|
||||
}
|
||||
|
||||
this.channels.unshift(
|
||||
new Chan({
|
||||
name: this.name,
|
||||
type: Chan.Type.LOBBY,
|
||||
// The lobby only starts as muted if every channel (unless it's special) is muted.
|
||||
// This is A) easier to implement and B) stops some confusion on startup.
|
||||
muted:
|
||||
this.channels.length >= 1 &&
|
||||
this.channels.every((chan) => chan.muted || chan.type === Chan.Type.SPECIAL),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
Network.prototype.validate = function (client) {
|
||||
// Remove !, :, @ and whitespace characters from nicknames and usernames
|
||||
const cleanNick = (str) => str.replace(/[\x00\s:!@]/g, "_").substring(0, 100);
|
||||
|
||||
// Remove new lines and limit length
|
||||
const cleanString = (str) => str.replace(/[\x00\r\n]/g, "").substring(0, 300);
|
||||
|
||||
this.setNick(cleanNick(String(this.nick || Config.getDefaultNick())));
|
||||
|
||||
if (!this.username) {
|
||||
// If username is empty, make one from the provided nick
|
||||
this.username = this.nick.replace(/[^a-zA-Z0-9]/g, "");
|
||||
}
|
||||
|
||||
this.username = cleanString(this.username) || "thelounge";
|
||||
this.realname = cleanString(this.realname) || "The Lounge User";
|
||||
this.leaveMessage = cleanString(this.leaveMessage);
|
||||
this.password = cleanString(this.password);
|
||||
this.host = cleanString(this.host).toLowerCase();
|
||||
this.name = cleanString(this.name);
|
||||
this.saslAccount = cleanString(this.saslAccount);
|
||||
this.saslPassword = cleanString(this.saslPassword);
|
||||
|
||||
this.proxyHost = cleanString(this.proxyHost);
|
||||
this.proxyPort = this.proxyPort || 1080;
|
||||
this.proxyUsername = cleanString(this.proxyUsername);
|
||||
this.proxyPassword = cleanString(this.proxyPassword);
|
||||
this.proxyEnabled = !!this.proxyEnabled;
|
||||
|
||||
const error = function (network, text) {
|
||||
network.channels[0].pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: text,
|
||||
}),
|
||||
true
|
||||
);
|
||||
};
|
||||
|
||||
if (!this.port) {
|
||||
this.port = this.tls ? 6697 : 6667;
|
||||
}
|
||||
|
||||
if (!["", "plain", "external"].includes(this.sasl)) {
|
||||
this.sasl = "";
|
||||
}
|
||||
|
||||
if (Config.values.lockNetwork) {
|
||||
// This check is needed to prevent invalid user configurations
|
||||
if (
|
||||
!Config.values.public &&
|
||||
this.host &&
|
||||
this.host.length > 0 &&
|
||||
this.host !== Config.values.defaults.host
|
||||
) {
|
||||
error(this, `The hostname you specified (${this.host}) is not allowed.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Config.values.public) {
|
||||
this.name = Config.values.defaults.name;
|
||||
// Sync lobby channel name
|
||||
this.channels[0].name = Config.values.defaults.name;
|
||||
}
|
||||
|
||||
this.host = Config.values.defaults.host;
|
||||
this.port = Config.values.defaults.port;
|
||||
this.tls = Config.values.defaults.tls;
|
||||
this.rejectUnauthorized = Config.values.defaults.rejectUnauthorized;
|
||||
}
|
||||
|
||||
if (this.host.length === 0) {
|
||||
error(this, "You must specify a hostname to connect.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const stsPolicy = STSPolicies.get(this.host);
|
||||
|
||||
if (stsPolicy && !this.tls) {
|
||||
error(
|
||||
this,
|
||||
`${this.host} has an active strict transport security policy, will connect to port ${stsPolicy.port} over a secure connection.`
|
||||
);
|
||||
|
||||
this.port = stsPolicy.port;
|
||||
this.tls = true;
|
||||
this.rejectUnauthorized = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
Network.prototype.createIrcFramework = function (client) {
|
||||
this.irc = new IrcFramework.Client({
|
||||
version: false, // We handle it ourselves
|
||||
outgoing_addr: Config.values.bind,
|
||||
enable_chghost: true,
|
||||
enable_echomessage: true,
|
||||
enable_setname: true,
|
||||
auto_reconnect: true,
|
||||
|
||||
// Exponential backoff maxes out at 300 seconds after 9 reconnects,
|
||||
// it will keep trying for well over an hour (plus the timeouts)
|
||||
auto_reconnect_max_retries: 30,
|
||||
});
|
||||
|
||||
this.setIrcFrameworkOptions(client);
|
||||
|
||||
this.irc.requestCap([
|
||||
"znc.in/self-message", // Legacy echo-message for ZNC
|
||||
"znc.in/playback", // See http://wiki.znc.in/Playback
|
||||
]);
|
||||
};
|
||||
|
||||
Network.prototype.setIrcFrameworkOptions = function (client) {
|
||||
this.irc.options.host = this.host;
|
||||
this.irc.options.port = this.port;
|
||||
this.irc.options.password = this.password;
|
||||
this.irc.options.nick = this.nick;
|
||||
this.irc.options.username = Config.values.useHexIp
|
||||
? Helper.ip2hex(client.config.browser.ip)
|
||||
: this.username;
|
||||
this.irc.options.gecos = this.realname;
|
||||
this.irc.options.tls = this.tls;
|
||||
this.irc.options.rejectUnauthorized = this.rejectUnauthorized;
|
||||
this.irc.options.webirc = this.createWebIrc(client);
|
||||
this.irc.options.client_certificate = null;
|
||||
|
||||
if (this.proxyEnabled) {
|
||||
this.irc.options.socks = {
|
||||
host: this.proxyHost,
|
||||
port: this.proxyPort,
|
||||
user: this.proxyUsername,
|
||||
pass: this.proxyPassword,
|
||||
};
|
||||
} else {
|
||||
delete this.irc.options.socks;
|
||||
}
|
||||
|
||||
if (!this.sasl) {
|
||||
delete this.irc.options.sasl_mechanism;
|
||||
delete this.irc.options.account;
|
||||
} else if (this.sasl === "external") {
|
||||
this.irc.options.sasl_mechanism = "EXTERNAL";
|
||||
this.irc.options.account = {};
|
||||
this.irc.options.client_certificate = ClientCertificate.get(this.uuid);
|
||||
} else if (this.sasl === "plain") {
|
||||
delete this.irc.options.sasl_mechanism;
|
||||
this.irc.options.account = {
|
||||
account: this.saslAccount,
|
||||
password: this.saslPassword,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
Network.prototype.createWebIrc = function (client) {
|
||||
if (
|
||||
!Config.values.webirc ||
|
||||
!Object.prototype.hasOwnProperty.call(Config.values.webirc, this.host)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const webircObject = {
|
||||
password: Config.values.webirc[this.host],
|
||||
username: "thelounge",
|
||||
address: client.config.browser.ip,
|
||||
hostname: client.config.browser.hostname,
|
||||
};
|
||||
|
||||
// https://ircv3.net/specs/extensions/webirc#options
|
||||
if (client.config.browser.isSecure) {
|
||||
webircObject.options = {
|
||||
secure: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof Config.values.webirc[this.host] === "function") {
|
||||
webircObject.password = null;
|
||||
return Config.values.webirc[this.host](webircObject, this);
|
||||
}
|
||||
|
||||
return webircObject;
|
||||
};
|
||||
|
||||
Network.prototype.edit = function (client, args) {
|
||||
const oldNetworkName = this.name;
|
||||
const oldNick = this.nick;
|
||||
const oldRealname = this.realname;
|
||||
|
||||
this.keepNick = null;
|
||||
this.nick = args.nick;
|
||||
this.host = String(args.host || "");
|
||||
this.name = String(args.name || "") || this.host;
|
||||
this.port = parseInt(args.port, 10);
|
||||
this.tls = !!args.tls;
|
||||
this.rejectUnauthorized = !!args.rejectUnauthorized;
|
||||
this.password = String(args.password || "");
|
||||
this.username = String(args.username || "");
|
||||
this.realname = String(args.realname || "");
|
||||
this.leaveMessage = String(args.leaveMessage || "");
|
||||
this.sasl = String(args.sasl || "");
|
||||
this.saslAccount = String(args.saslAccount || "");
|
||||
this.saslPassword = String(args.saslPassword || "");
|
||||
|
||||
this.proxyHost = String(args.proxyHost || "");
|
||||
this.proxyPort = parseInt(args.proxyPort, 10);
|
||||
this.proxyUsername = String(args.proxyUsername || "");
|
||||
this.proxyPassword = String(args.proxyPassword || "");
|
||||
this.proxyEnabled = !!args.proxyEnabled;
|
||||
|
||||
// Split commands into an array
|
||||
this.commands = String(args.commands || "")
|
||||
.replace(/\r\n|\r|\n/g, "\n")
|
||||
.split("\n")
|
||||
.filter((command) => command.length > 0);
|
||||
|
||||
// Sync lobby channel name
|
||||
this.channels[0].name = this.name;
|
||||
|
||||
if (this.name !== oldNetworkName) {
|
||||
// Send updated network name to all connected clients
|
||||
client.emit("network:name", {
|
||||
uuid: this.uuid,
|
||||
name: this.name,
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.validate(client)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.irc) {
|
||||
const connected = this.irc.connection && this.irc.connection.connected;
|
||||
|
||||
if (this.nick !== oldNick) {
|
||||
if (connected) {
|
||||
// Send new nick straight away
|
||||
this.irc.changeNick(this.nick);
|
||||
} else {
|
||||
this.irc.user.nick = this.nick;
|
||||
|
||||
// Update UI nick straight away if IRC is not connected
|
||||
client.emit("nick", {
|
||||
network: this.uuid,
|
||||
nick: this.nick,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
connected &&
|
||||
this.realname !== oldRealname &&
|
||||
this.irc.network.cap.isEnabled("setname")
|
||||
) {
|
||||
this.irc.raw("SETNAME", this.realname);
|
||||
}
|
||||
|
||||
this.setIrcFrameworkOptions(client);
|
||||
|
||||
this.irc.user.username = this.irc.options.username;
|
||||
this.irc.user.gecos = this.irc.options.gecos;
|
||||
}
|
||||
|
||||
client.save();
|
||||
};
|
||||
|
||||
Network.prototype.destroy = function () {
|
||||
this.channels.forEach((channel) => channel.destroy());
|
||||
};
|
||||
|
||||
Network.prototype.setNick = function (nick) {
|
||||
this.nick = nick;
|
||||
this.highlightRegex = new RegExp(
|
||||
// Do not match characters and numbers (unless IRC color)
|
||||
"(?:^|[^a-z0-9]|\x03[0-9]{1,2})" +
|
||||
// Escape nickname, as it may contain regex stuff
|
||||
_.escapeRegExp(nick) +
|
||||
// Do not match characters and numbers
|
||||
"(?:[^a-z0-9]|$)",
|
||||
|
||||
// Case insensitive search
|
||||
"i"
|
||||
);
|
||||
|
||||
if (this.keepNick === nick) {
|
||||
this.keepNick = null;
|
||||
}
|
||||
|
||||
if (this.irc) {
|
||||
this.irc.options.nick = nick;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a clean clone of this network that will be sent to the client.
|
||||
* This function performs manual cloning of network object for
|
||||
* better control of performance and memory usage.
|
||||
*
|
||||
* Both of the parameters that are accepted by this function are passed into channels' getFilteredClone call.
|
||||
*
|
||||
* @see {@link Chan#getFilteredClone}
|
||||
*/
|
||||
Network.prototype.getFilteredClone = function (lastActiveChannel, lastMessage) {
|
||||
const filteredNetwork = Object.keys(this).reduce((newNetwork, prop) => {
|
||||
if (prop === "channels") {
|
||||
// Channels objects perform their own cloning
|
||||
newNetwork[prop] = this[prop].map((channel) =>
|
||||
channel.getFilteredClone(lastActiveChannel, lastMessage)
|
||||
);
|
||||
} else if (fieldsForClient[prop]) {
|
||||
// Some properties that are not useful for the client are skipped
|
||||
newNetwork[prop] = this[prop];
|
||||
}
|
||||
|
||||
return newNetwork;
|
||||
}, {});
|
||||
|
||||
filteredNetwork.status = this.getNetworkStatus();
|
||||
|
||||
return filteredNetwork;
|
||||
};
|
||||
|
||||
Network.prototype.getNetworkStatus = function () {
|
||||
const status = {
|
||||
connected: false,
|
||||
secure: false,
|
||||
};
|
||||
|
||||
if (this.irc && this.irc.connection && this.irc.connection.transport) {
|
||||
const transport = this.irc.connection.transport;
|
||||
|
||||
if (transport.socket) {
|
||||
const isLocalhost = transport.socket.remoteAddress === "127.0.0.1";
|
||||
const isAuthorized = transport.socket.encrypted && transport.socket.authorized;
|
||||
|
||||
status.connected = transport.isConnected();
|
||||
status.secure = isAuthorized || isLocalhost;
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
};
|
||||
|
||||
Network.prototype.addChannel = function (newChan) {
|
||||
let index = this.channels.length; // Default to putting as the last item in the array
|
||||
|
||||
// Don't sort special channels in amongst channels/users.
|
||||
if (newChan.type === Chan.Type.CHANNEL || newChan.type === Chan.Type.QUERY) {
|
||||
// We start at 1 so we don't test against the lobby
|
||||
for (let i = 1; i < this.channels.length; i++) {
|
||||
const compareChan = this.channels[i];
|
||||
|
||||
// Negative if the new chan is alphabetically before the next chan in the list, positive if after
|
||||
if (
|
||||
newChan.name.localeCompare(compareChan.name, {sensitivity: "base"}) <= 0 ||
|
||||
(compareChan.type !== Chan.Type.CHANNEL && compareChan.type !== Chan.Type.QUERY)
|
||||
) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.channels.splice(index, 0, newChan);
|
||||
return index;
|
||||
};
|
||||
|
||||
Network.prototype.quit = function (quitMessage) {
|
||||
if (!this.irc) {
|
||||
return;
|
||||
}
|
||||
|
||||
// https://ircv3.net/specs/extensions/sts#rescheduling-expiry-on-disconnect
|
||||
STSPolicies.refreshExpiration(this.host);
|
||||
|
||||
this.irc.quit(quitMessage || this.leaveMessage || Config.values.leaveMessage);
|
||||
};
|
||||
|
||||
Network.prototype.exportForEdit = function () {
|
||||
const fieldsToReturn = [
|
||||
"uuid",
|
||||
"name",
|
||||
"nick",
|
||||
"password",
|
||||
"username",
|
||||
"realname",
|
||||
"leaveMessage",
|
||||
"sasl",
|
||||
"saslAccount",
|
||||
"saslPassword",
|
||||
"commands",
|
||||
|
||||
"proxyEnabled",
|
||||
"proxyHost",
|
||||
"proxyPort",
|
||||
"proxyUsername",
|
||||
"proxyPassword",
|
||||
];
|
||||
|
||||
if (!Config.values.lockNetwork) {
|
||||
fieldsToReturn.push("host");
|
||||
fieldsToReturn.push("port");
|
||||
fieldsToReturn.push("tls");
|
||||
fieldsToReturn.push("rejectUnauthorized");
|
||||
}
|
||||
|
||||
const data = _.pick(this, fieldsToReturn);
|
||||
|
||||
data.hasSTSPolicy = !!STSPolicies.get(this.host);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
Network.prototype.export = function () {
|
||||
const network = _.pick(this, [
|
||||
"uuid",
|
||||
"awayMessage",
|
||||
"nick",
|
||||
"name",
|
||||
"host",
|
||||
"port",
|
||||
"tls",
|
||||
"userDisconnected",
|
||||
"rejectUnauthorized",
|
||||
"password",
|
||||
"username",
|
||||
"realname",
|
||||
"leaveMessage",
|
||||
"sasl",
|
||||
"saslAccount",
|
||||
"saslPassword",
|
||||
"commands",
|
||||
"ignoreList",
|
||||
|
||||
"proxyHost",
|
||||
"proxyPort",
|
||||
"proxyUsername",
|
||||
"proxyEnabled",
|
||||
"proxyPassword",
|
||||
]);
|
||||
|
||||
network.channels = this.channels
|
||||
.filter(function (channel) {
|
||||
return channel.type === Chan.Type.CHANNEL || channel.type === Chan.Type.QUERY;
|
||||
})
|
||||
.map(function (chan) {
|
||||
const keys = ["name", "muted"];
|
||||
|
||||
if (chan.type === Chan.Type.CHANNEL) {
|
||||
keys.push("key");
|
||||
} else if (chan.type === Chan.Type.QUERY) {
|
||||
keys.push("type");
|
||||
}
|
||||
|
||||
return _.pick(chan, keys);
|
||||
});
|
||||
|
||||
return network;
|
||||
};
|
||||
|
||||
Network.prototype.getChannel = function (name) {
|
||||
name = name.toLowerCase();
|
||||
|
||||
return _.find(this.channels, function (that, i) {
|
||||
// Skip network lobby (it's always unshifted into first position)
|
||||
return i > 0 && that.name.toLowerCase() === name;
|
||||
});
|
||||
};
|
627
src/models/network.ts
Normal file
627
src/models/network.ts
Normal file
|
@ -0,0 +1,627 @@
|
|||
"use strict";
|
||||
|
||||
import _ from "lodash";
|
||||
import {v4 as uuidv4} from "uuid";
|
||||
import IrcFramework from "irc-framework";
|
||||
import Chan from "./chan";
|
||||
import Msg from "./msg";
|
||||
import Prefix from "./prefix";
|
||||
import Helper from "../helper";
|
||||
import Config from "../config";
|
||||
import STSPolicies from "../plugins/sts";
|
||||
import ClientCertificate from "../plugins/clientCertificate";
|
||||
import {Channel, ChanType} from "src/types/models/channel";
|
||||
import Client from "src/client";
|
||||
import {NetworkStatus} from "src/types/models/network";
|
||||
import {MessageType} from "src/types/models/message";
|
||||
import {WebIRC} from "src/types/config";
|
||||
|
||||
/**
|
||||
* @type {Object} List of keys which should be sent to the client by default.
|
||||
*/
|
||||
const fieldsForClient = {
|
||||
uuid: true,
|
||||
name: true,
|
||||
nick: true,
|
||||
serverOptions: true,
|
||||
};
|
||||
|
||||
class Network {
|
||||
nick: string;
|
||||
name: string;
|
||||
host: string;
|
||||
port: number;
|
||||
tls: boolean;
|
||||
userDisconnected: boolean;
|
||||
rejectUnauthorized: boolean;
|
||||
password: string;
|
||||
awayMessage: string;
|
||||
commands: any[];
|
||||
username: string;
|
||||
realname: string;
|
||||
leaveMessage: string;
|
||||
sasl: string;
|
||||
saslAccount: string;
|
||||
saslPassword: string;
|
||||
channels: Chan[];
|
||||
uuid: string;
|
||||
proxyHost: string;
|
||||
proxyPort: number;
|
||||
proxyUsername: string;
|
||||
proxyPassword: string;
|
||||
proxyEnabled: boolean;
|
||||
highlightRegex?: RegExp;
|
||||
|
||||
irc?: IrcFramework.Client & {
|
||||
options?: {
|
||||
host: string;
|
||||
port: number;
|
||||
password: string;
|
||||
nick: string;
|
||||
username: string;
|
||||
gecos: string;
|
||||
tls: boolean;
|
||||
rejectUnauthorized: boolean;
|
||||
webirc: WebIRC;
|
||||
client_certificate?: ClientCertificate;
|
||||
socks: {
|
||||
host: string;
|
||||
port: number;
|
||||
user: string;
|
||||
pass: string;
|
||||
};
|
||||
sasl_mechanism: string;
|
||||
account:
|
||||
| {
|
||||
account: string;
|
||||
password: string;
|
||||
}
|
||||
| {};
|
||||
};
|
||||
};
|
||||
|
||||
chanCache: Chan[];
|
||||
ignoreList: string[];
|
||||
keepNick?: string;
|
||||
|
||||
status: NetworkStatus;
|
||||
|
||||
serverOptions: {
|
||||
CHANTYPES: string[];
|
||||
PREFIX: Prefix;
|
||||
NETWORK: string;
|
||||
};
|
||||
|
||||
// TODO: this is only available on export
|
||||
hasSTSPolicy: boolean;
|
||||
|
||||
constructor(attr: Partial<Network>) {
|
||||
_.defaults(this, attr, {
|
||||
name: "",
|
||||
nick: "",
|
||||
host: "",
|
||||
port: 6667,
|
||||
tls: false,
|
||||
userDisconnected: false,
|
||||
rejectUnauthorized: false,
|
||||
password: "",
|
||||
awayMessage: "",
|
||||
commands: [],
|
||||
username: "",
|
||||
realname: "",
|
||||
leaveMessage: "",
|
||||
sasl: "",
|
||||
saslAccount: "",
|
||||
saslPassword: "",
|
||||
channels: [],
|
||||
irc: null,
|
||||
serverOptions: {
|
||||
CHANTYPES: ["#", "&"],
|
||||
PREFIX: new Prefix([
|
||||
{symbol: "!", mode: "Y"},
|
||||
{symbol: "@", mode: "o"},
|
||||
{symbol: "%", mode: "h"},
|
||||
{symbol: "+", mode: "v"},
|
||||
]),
|
||||
NETWORK: "",
|
||||
},
|
||||
|
||||
proxyHost: "",
|
||||
proxyPort: 1080,
|
||||
proxyUsername: "",
|
||||
proxyPassword: "",
|
||||
proxyEnabled: false,
|
||||
|
||||
chanCache: [],
|
||||
ignoreList: [],
|
||||
keepNick: null,
|
||||
});
|
||||
|
||||
if (!this.uuid) {
|
||||
this.uuid = uuidv4();
|
||||
}
|
||||
|
||||
if (!this.name) {
|
||||
this.name = this.host;
|
||||
}
|
||||
|
||||
this.channels.unshift(
|
||||
new Chan({
|
||||
name: this.name,
|
||||
type: ChanType.LOBBY,
|
||||
// The lobby only starts as muted if every channel (unless it's special) is muted.
|
||||
// This is A) easier to implement and B) stops some confusion on startup.
|
||||
muted:
|
||||
this.channels.length >= 1 &&
|
||||
this.channels.every((chan) => chan.muted || chan.type === ChanType.SPECIAL),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
validate(client: Client) {
|
||||
// Remove !, :, @ and whitespace characters from nicknames and usernames
|
||||
const cleanNick = (str: string) => str.replace(/[\x00\s:!@]/g, "_").substring(0, 100);
|
||||
|
||||
// Remove new lines and limit length
|
||||
const cleanString = (str: string) => str.replace(/[\x00\r\n]/g, "").substring(0, 300);
|
||||
|
||||
this.setNick(cleanNick(String(this.nick || Config.getDefaultNick())));
|
||||
|
||||
if (!this.username) {
|
||||
// If username is empty, make one from the provided nick
|
||||
this.username = this.nick.replace(/[^a-zA-Z0-9]/g, "");
|
||||
}
|
||||
|
||||
this.username = cleanString(this.username) || "thelounge";
|
||||
this.realname = cleanString(this.realname) || "The Lounge User";
|
||||
this.leaveMessage = cleanString(this.leaveMessage);
|
||||
this.password = cleanString(this.password);
|
||||
this.host = cleanString(this.host).toLowerCase();
|
||||
this.name = cleanString(this.name);
|
||||
this.saslAccount = cleanString(this.saslAccount);
|
||||
this.saslPassword = cleanString(this.saslPassword);
|
||||
|
||||
this.proxyHost = cleanString(this.proxyHost);
|
||||
this.proxyPort = this.proxyPort || 1080;
|
||||
this.proxyUsername = cleanString(this.proxyUsername);
|
||||
this.proxyPassword = cleanString(this.proxyPassword);
|
||||
this.proxyEnabled = !!this.proxyEnabled;
|
||||
|
||||
const error = function (network: Network, text: string) {
|
||||
network.channels[0].pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: MessageType.ERROR,
|
||||
text: text,
|
||||
}),
|
||||
true
|
||||
);
|
||||
};
|
||||
|
||||
if (!this.port) {
|
||||
this.port = this.tls ? 6697 : 6667;
|
||||
}
|
||||
|
||||
if (!["", "plain", "external"].includes(this.sasl)) {
|
||||
this.sasl = "";
|
||||
}
|
||||
|
||||
if (Config.values.lockNetwork) {
|
||||
// This check is needed to prevent invalid user configurations
|
||||
if (
|
||||
!Config.values.public &&
|
||||
this.host &&
|
||||
this.host.length > 0 &&
|
||||
this.host !== Config.values.defaults.host
|
||||
) {
|
||||
error(this, `The hostname you specified (${this.host}) is not allowed.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Config.values.public) {
|
||||
this.name = Config.values.defaults.name;
|
||||
// Sync lobby channel name
|
||||
this.channels[0].name = Config.values.defaults.name;
|
||||
}
|
||||
|
||||
this.host = Config.values.defaults.host;
|
||||
this.port = Config.values.defaults.port;
|
||||
this.tls = Config.values.defaults.tls;
|
||||
this.rejectUnauthorized = Config.values.defaults.rejectUnauthorized;
|
||||
}
|
||||
|
||||
if (this.host.length === 0) {
|
||||
error(this, "You must specify a hostname to connect.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const stsPolicy = STSPolicies.get(this.host);
|
||||
|
||||
if (stsPolicy && !this.tls) {
|
||||
error(
|
||||
this,
|
||||
`${this.host} has an active strict transport security policy, will connect to port ${stsPolicy.port} over a secure connection.`
|
||||
);
|
||||
|
||||
this.port = stsPolicy.port;
|
||||
this.tls = true;
|
||||
this.rejectUnauthorized = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
createIrcFramework(client: Client) {
|
||||
this.irc = new IrcFramework.Client({
|
||||
version: false, // We handle it ourselves
|
||||
outgoing_addr: Config.values.bind,
|
||||
enable_chghost: true,
|
||||
enable_echomessage: true,
|
||||
enable_setname: true,
|
||||
auto_reconnect: true,
|
||||
|
||||
// Exponential backoff maxes out at 300 seconds after 9 reconnects,
|
||||
// it will keep trying for well over an hour (plus the timeouts)
|
||||
auto_reconnect_max_retries: 30,
|
||||
});
|
||||
|
||||
this.setIrcFrameworkOptions(client);
|
||||
|
||||
this.irc.requestCap([
|
||||
"znc.in/self-message", // Legacy echo-message for ZNC
|
||||
"znc.in/playback", // See http://wiki.znc.in/Playback
|
||||
]);
|
||||
}
|
||||
|
||||
setIrcFrameworkOptions(client: Client) {
|
||||
this.irc.options.host = this.host;
|
||||
this.irc.options.port = this.port;
|
||||
this.irc.options.password = this.password;
|
||||
this.irc.options.nick = this.nick;
|
||||
this.irc.options.username = Config.values.useHexIp
|
||||
? Helper.ip2hex(client.config.browser.ip)
|
||||
: this.username;
|
||||
this.irc.options.gecos = this.realname;
|
||||
this.irc.options.tls = this.tls;
|
||||
this.irc.options.rejectUnauthorized = this.rejectUnauthorized;
|
||||
this.irc.options.webirc = this.createWebIrc(client);
|
||||
this.irc.options.client_certificate = null;
|
||||
|
||||
if (this.proxyEnabled) {
|
||||
this.irc.options.socks = {
|
||||
host: this.proxyHost,
|
||||
port: this.proxyPort,
|
||||
user: this.proxyUsername,
|
||||
pass: this.proxyPassword,
|
||||
};
|
||||
} else {
|
||||
delete this.irc.options.socks;
|
||||
}
|
||||
|
||||
if (!this.sasl) {
|
||||
delete this.irc.options.sasl_mechanism;
|
||||
delete this.irc.options.account;
|
||||
} else if (this.sasl === "external") {
|
||||
this.irc.options.sasl_mechanism = "EXTERNAL";
|
||||
this.irc.options.account = {};
|
||||
this.irc.options.client_certificate = ClientCertificate.get(this.uuid);
|
||||
} else if (this.sasl === "plain") {
|
||||
delete this.irc.options.sasl_mechanism;
|
||||
this.irc.options.account = {
|
||||
account: this.saslAccount,
|
||||
password: this.saslPassword,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
createWebIrc(client: Client) {
|
||||
if (
|
||||
!Config.values.webirc ||
|
||||
!Object.prototype.hasOwnProperty.call(Config.values.webirc, this.host)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const webircObject = {
|
||||
password: Config.values.webirc[this.host],
|
||||
username: "thelounge",
|
||||
address: client.config.browser.ip,
|
||||
hostname: client.config.browser.hostname,
|
||||
} as any;
|
||||
|
||||
// https://ircv3.net/specs/extensions/webirc#options
|
||||
if (client.config.browser.isSecure) {
|
||||
webircObject.options = {
|
||||
secure: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof Config.values.webirc[this.host] === "function") {
|
||||
webircObject.password = null;
|
||||
return Config.values.webirc[this.host](webircObject, this);
|
||||
}
|
||||
|
||||
return webircObject;
|
||||
}
|
||||
|
||||
edit(client: Client, args: any) {
|
||||
const oldNetworkName = this.name;
|
||||
const oldNick = this.nick;
|
||||
const oldRealname = this.realname;
|
||||
|
||||
this.keepNick = null;
|
||||
this.nick = args.nick;
|
||||
this.host = String(args.host || "");
|
||||
this.name = String(args.name || "") || this.host;
|
||||
this.port = parseInt(args.port, 10);
|
||||
this.tls = !!args.tls;
|
||||
this.rejectUnauthorized = !!args.rejectUnauthorized;
|
||||
this.password = String(args.password || "");
|
||||
this.username = String(args.username || "");
|
||||
this.realname = String(args.realname || "");
|
||||
this.leaveMessage = String(args.leaveMessage || "");
|
||||
this.sasl = String(args.sasl || "");
|
||||
this.saslAccount = String(args.saslAccount || "");
|
||||
this.saslPassword = String(args.saslPassword || "");
|
||||
|
||||
this.proxyHost = String(args.proxyHost || "");
|
||||
this.proxyPort = parseInt(args.proxyPort, 10);
|
||||
this.proxyUsername = String(args.proxyUsername || "");
|
||||
this.proxyPassword = String(args.proxyPassword || "");
|
||||
this.proxyEnabled = !!args.proxyEnabled;
|
||||
|
||||
// Split commands into an array
|
||||
this.commands = String(args.commands || "")
|
||||
.replace(/\r\n|\r|\n/g, "\n")
|
||||
.split("\n")
|
||||
.filter((command) => command.length > 0);
|
||||
|
||||
// Sync lobby channel name
|
||||
this.channels[0].name = this.name;
|
||||
|
||||
if (this.name !== oldNetworkName) {
|
||||
// Send updated network name to all connected clients
|
||||
client.emit("network:name", {
|
||||
uuid: this.uuid,
|
||||
name: this.name,
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.validate(client)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.irc) {
|
||||
const connected = this.irc.connection && this.irc.connection.connected;
|
||||
|
||||
if (this.nick !== oldNick) {
|
||||
if (connected) {
|
||||
// Send new nick straight away
|
||||
this.irc.changeNick(this.nick);
|
||||
} else {
|
||||
this.irc.user.nick = this.nick;
|
||||
|
||||
// Update UI nick straight away if IRC is not connected
|
||||
client.emit("nick", {
|
||||
network: this.uuid,
|
||||
nick: this.nick,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
connected &&
|
||||
this.realname !== oldRealname &&
|
||||
this.irc.network.cap.isEnabled("setname")
|
||||
) {
|
||||
this.irc.raw("SETNAME", this.realname);
|
||||
}
|
||||
|
||||
this.setIrcFrameworkOptions(client);
|
||||
|
||||
this.irc.user.username = this.irc.options.username;
|
||||
this.irc.user.gecos = this.irc.options.gecos;
|
||||
}
|
||||
|
||||
client.save();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.channels.forEach((channel) => channel.destroy());
|
||||
}
|
||||
|
||||
setNick(nick: string) {
|
||||
this.nick = nick;
|
||||
this.highlightRegex = new RegExp(
|
||||
// Do not match characters and numbers (unless IRC color)
|
||||
"(?:^|[^a-z0-9]|\x03[0-9]{1,2})" +
|
||||
// Escape nickname, as it may contain regex stuff
|
||||
_.escapeRegExp(nick) +
|
||||
// Do not match characters and numbers
|
||||
"(?:[^a-z0-9]|$)",
|
||||
|
||||
// Case insensitive search
|
||||
"i"
|
||||
);
|
||||
|
||||
if (this.keepNick === nick) {
|
||||
this.keepNick = null;
|
||||
}
|
||||
|
||||
if (this.irc) {
|
||||
this.irc.options.nick = nick;
|
||||
}
|
||||
}
|
||||
|
||||
getFilteredClone(lastActiveChannel: number, lastMessage: number) {
|
||||
const filteredNetwork = Object.keys(this).reduce((newNetwork, prop) => {
|
||||
if (prop === "channels") {
|
||||
// Channels objects perform their own cloning
|
||||
newNetwork[prop] = this[prop].map((channel) =>
|
||||
channel.getFilteredClone(lastActiveChannel, lastMessage)
|
||||
);
|
||||
} else if (fieldsForClient[prop]) {
|
||||
// Some properties that are not useful for the client are skipped
|
||||
newNetwork[prop] = this[prop];
|
||||
}
|
||||
|
||||
return newNetwork;
|
||||
}, {}) as Network;
|
||||
|
||||
filteredNetwork.status = this.getNetworkStatus();
|
||||
|
||||
return filteredNetwork;
|
||||
}
|
||||
|
||||
getNetworkStatus() {
|
||||
const status = {
|
||||
connected: false,
|
||||
secure: false,
|
||||
};
|
||||
|
||||
if (this.irc && this.irc.connection && this.irc.connection.transport) {
|
||||
const transport = this.irc.connection.transport;
|
||||
|
||||
if (transport.socket) {
|
||||
const isLocalhost = transport.socket.remoteAddress === "127.0.0.1";
|
||||
const isAuthorized = transport.socket.encrypted && transport.socket.authorized;
|
||||
|
||||
status.connected = transport.isConnected();
|
||||
status.secure = isAuthorized || isLocalhost;
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
addChannel(newChan: Chan) {
|
||||
let index = this.channels.length; // Default to putting as the last item in the array
|
||||
|
||||
// Don't sort special channels in amongst channels/users.
|
||||
if (newChan.type === ChanType.CHANNEL || newChan.type === ChanType.QUERY) {
|
||||
// We start at 1 so we don't test against the lobby
|
||||
for (let i = 1; i < this.channels.length; i++) {
|
||||
const compareChan = this.channels[i];
|
||||
|
||||
// Negative if the new chan is alphabetically before the next chan in the list, positive if after
|
||||
if (
|
||||
newChan.name.localeCompare(compareChan.name, undefined, {
|
||||
sensitivity: "base",
|
||||
}) <= 0 ||
|
||||
(compareChan.type !== ChanType.CHANNEL && compareChan.type !== ChanType.QUERY)
|
||||
) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.channels.splice(index, 0, newChan);
|
||||
return index;
|
||||
}
|
||||
|
||||
quit(quitMessage?: string) {
|
||||
if (!this.irc) {
|
||||
return;
|
||||
}
|
||||
|
||||
// https://ircv3.net/specs/extensions/sts#rescheduling-expiry-on-disconnect
|
||||
STSPolicies.refreshExpiration(this.host);
|
||||
|
||||
this.irc.quit(quitMessage || this.leaveMessage || Config.values.leaveMessage);
|
||||
}
|
||||
|
||||
exportForEdit() {
|
||||
const fieldsToReturn = [
|
||||
"uuid",
|
||||
"name",
|
||||
"nick",
|
||||
"password",
|
||||
"username",
|
||||
"realname",
|
||||
"leaveMessage",
|
||||
"sasl",
|
||||
"saslAccount",
|
||||
"saslPassword",
|
||||
"commands",
|
||||
|
||||
"proxyEnabled",
|
||||
"proxyHost",
|
||||
"proxyPort",
|
||||
"proxyUsername",
|
||||
"proxyPassword",
|
||||
];
|
||||
|
||||
if (!Config.values.lockNetwork) {
|
||||
fieldsToReturn.push("host");
|
||||
fieldsToReturn.push("port");
|
||||
fieldsToReturn.push("tls");
|
||||
fieldsToReturn.push("rejectUnauthorized");
|
||||
}
|
||||
|
||||
const data = _.pick(this, fieldsToReturn) as Network;
|
||||
|
||||
data.hasSTSPolicy = !!STSPolicies.get(this.host);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export() {
|
||||
const network = _.pick(this, [
|
||||
"uuid",
|
||||
"awayMessage",
|
||||
"nick",
|
||||
"name",
|
||||
"host",
|
||||
"port",
|
||||
"tls",
|
||||
"userDisconnected",
|
||||
"rejectUnauthorized",
|
||||
"password",
|
||||
"username",
|
||||
"realname",
|
||||
"leaveMessage",
|
||||
"sasl",
|
||||
"saslAccount",
|
||||
"saslPassword",
|
||||
"commands",
|
||||
"ignoreList",
|
||||
|
||||
"proxyHost",
|
||||
"proxyPort",
|
||||
"proxyUsername",
|
||||
"proxyEnabled",
|
||||
"proxyPassword",
|
||||
]) as Network;
|
||||
|
||||
network.channels = this.channels
|
||||
.filter(function (channel) {
|
||||
return channel.type === ChanType.CHANNEL || channel.type === ChanType.QUERY;
|
||||
})
|
||||
.map(function (chan) {
|
||||
const keys = ["name", "muted"];
|
||||
|
||||
if (chan.type === ChanType.CHANNEL) {
|
||||
keys.push("key");
|
||||
} else if (chan.type === ChanType.QUERY) {
|
||||
keys.push("type");
|
||||
}
|
||||
|
||||
return _.pick(chan, keys);
|
||||
// Override the type because we're omitting ID
|
||||
}) as Channel[];
|
||||
|
||||
return network;
|
||||
}
|
||||
|
||||
getChannel(name: string) {
|
||||
name = name.toLowerCase();
|
||||
|
||||
return _.find(this.channels, function (that, i) {
|
||||
// Skip network lobby (it's always unshifted into first position)
|
||||
return i > 0 && that.name.toLowerCase() === name;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default Network;
|
|
@ -1,7 +1,11 @@
|
|||
"use strict";
|
||||
|
||||
class Prefix {
|
||||
constructor(prefix) {
|
||||
prefix: PrefixObject[];
|
||||
modeToSymbol: {[mode: string]: string};
|
||||
symbols: string[];
|
||||
|
||||
constructor(prefix: PrefixObject[]) {
|
||||
this.prefix = prefix || []; // [{symbol: "@", mode: "o"}, ... ]
|
||||
this.modeToSymbol = {};
|
||||
this.symbols = [];
|
||||
|
@ -20,14 +24,14 @@ class Prefix {
|
|||
});
|
||||
}
|
||||
|
||||
update(prefix) {
|
||||
update(prefix: PrefixObject[]) {
|
||||
this.prefix = prefix || [];
|
||||
this._update_internals();
|
||||
}
|
||||
|
||||
forEach(f) {
|
||||
forEach(f: (value: PrefixObject, index: number, array: PrefixObject[]) => void) {
|
||||
return this.prefix.forEach(f);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Prefix;
|
||||
export default Prefix;
|
|
@ -1,35 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
const _ = require("lodash");
|
||||
|
||||
module.exports = User;
|
||||
|
||||
function User(attr, prefix) {
|
||||
_.defaults(this, attr, {
|
||||
modes: [],
|
||||
away: "",
|
||||
nick: "",
|
||||
lastMessage: 0,
|
||||
});
|
||||
|
||||
Object.defineProperty(this, "mode", {
|
||||
get() {
|
||||
return this.modes[0] || "";
|
||||
},
|
||||
});
|
||||
|
||||
this.setModes(this.modes, prefix);
|
||||
}
|
||||
|
||||
User.prototype.setModes = function (modes, prefix) {
|
||||
// irc-framework sets character mode, but The Lounge works with symbols
|
||||
this.modes = modes.map((mode) => prefix.modeToSymbol[mode]);
|
||||
};
|
||||
|
||||
User.prototype.toJSON = function () {
|
||||
return {
|
||||
nick: this.nick,
|
||||
modes: this.modes,
|
||||
lastMessage: this.lastMessage,
|
||||
};
|
||||
};
|
45
src/models/user.ts
Normal file
45
src/models/user.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
"use strict";
|
||||
|
||||
import _ from "lodash";
|
||||
import Prefix from "./prefix";
|
||||
|
||||
class User {
|
||||
modes: string[];
|
||||
// Users in the channel have only one mode assigned
|
||||
mode: string;
|
||||
away: string;
|
||||
nick: string;
|
||||
lastMessage: number;
|
||||
|
||||
constructor(attr: Partial<User>, prefix?: Prefix) {
|
||||
_.defaults(this, attr, {
|
||||
modes: [],
|
||||
away: "",
|
||||
nick: "",
|
||||
lastMessage: 0,
|
||||
});
|
||||
|
||||
Object.defineProperty(this, "mode", {
|
||||
get() {
|
||||
return this.modes[0] || "";
|
||||
},
|
||||
});
|
||||
|
||||
this.setModes(this.modes, prefix);
|
||||
}
|
||||
|
||||
setModes(modes: string[], prefix: Prefix) {
|
||||
// irc-framework sets character mode, but The Lounge works with symbols
|
||||
this.modes = modes.map((mode) => prefix.modeToSymbol[mode]);
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
nick: this.nick,
|
||||
modes: this.modes,
|
||||
lastMessage: this.lastMessage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default User;
|
|
@ -1,7 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
const log = require("../log");
|
||||
const colors = require("chalk");
|
||||
import colors from "chalk";
|
||||
import log from "../log";
|
||||
|
||||
// The order defines priority: the first available plugin is used.
|
||||
// Always keep 'local' auth plugin at the end of the list; it should always be enabled.
|
||||
|
@ -15,8 +15,7 @@ function unimplemented(funcName) {
|
|||
);
|
||||
}
|
||||
|
||||
// Default API implementations
|
||||
module.exports = {
|
||||
const toExport = {
|
||||
moduleName: "<module with no name>",
|
||||
|
||||
// Must override: implements authentication mechanism
|
||||
|
@ -27,7 +26,11 @@ module.exports = {
|
|||
// can do so without access to the user's unhashed password.
|
||||
// Returning 'false' triggers fallback to default behaviour of loading all users
|
||||
loadUsers: () => false,
|
||||
};
|
||||
// TODO: fix typing
|
||||
} as any;
|
||||
|
||||
// Default API implementations
|
||||
export default toExport;
|
||||
|
||||
// local auth should always be enabled, but check here to verify
|
||||
let somethingEnabled = false;
|
||||
|
@ -38,7 +41,7 @@ for (const plugin of plugins) {
|
|||
somethingEnabled = true;
|
||||
|
||||
for (const name in plugin) {
|
||||
module.exports[name] = plugin[name];
|
||||
toExport[name] = plugin[name];
|
||||
}
|
||||
|
||||
break;
|
|
@ -1,11 +1,18 @@
|
|||
"use strict";
|
||||
|
||||
const log = require("../../log");
|
||||
const Config = require("../../config");
|
||||
const ldap = require("ldapjs");
|
||||
const colors = require("chalk");
|
||||
import log from "../../log";
|
||||
import Config from "../../config";
|
||||
import ldap, {SearchOptions} from "ldapjs";
|
||||
import colors from "chalk";
|
||||
import ClientManager from "src/clientManager";
|
||||
import Client from "src/client";
|
||||
|
||||
function ldapAuthCommon(user, bindDN, password, callback) {
|
||||
function ldapAuthCommon(
|
||||
user: string,
|
||||
bindDN: string,
|
||||
password: string,
|
||||
callback: (success: boolean) => void
|
||||
) {
|
||||
const config = Config.values;
|
||||
|
||||
const ldapclient = ldap.createClient({
|
||||
|
@ -30,7 +37,7 @@ function ldapAuthCommon(user, bindDN, password, callback) {
|
|||
});
|
||||
}
|
||||
|
||||
function simpleLdapAuth(user, password, callback) {
|
||||
function simpleLdapAuth(user: string, password: string, callback: (success: boolean) => void) {
|
||||
if (!user || !password) {
|
||||
return callback(false);
|
||||
}
|
||||
|
@ -48,7 +55,7 @@ function simpleLdapAuth(user, password, callback) {
|
|||
/**
|
||||
* LDAP auth using initial DN search (see config comment for ldap.searchDN)
|
||||
*/
|
||||
function advancedLdapAuth(user, password, callback) {
|
||||
function advancedLdapAuth(user: string, password: string, callback: (success: boolean) => void) {
|
||||
if (!user || !password) {
|
||||
return callback(false);
|
||||
}
|
||||
|
@ -66,7 +73,7 @@ function advancedLdapAuth(user, password, callback) {
|
|||
scope: config.ldap.searchDN.scope,
|
||||
filter: `(&(${config.ldap.primaryKey}=${userDN})${config.ldap.searchDN.filter})`,
|
||||
attributes: ["dn"],
|
||||
};
|
||||
} as SearchOptions;
|
||||
|
||||
ldapclient.on("error", function (err) {
|
||||
log.error(`Unable to connect to LDAP server: ${err}`);
|
||||
|
@ -117,12 +124,18 @@ function advancedLdapAuth(user, password, callback) {
|
|||
});
|
||||
}
|
||||
|
||||
function ldapAuth(manager, client, user, password, callback) {
|
||||
function ldapAuth(
|
||||
manager: ClientManager,
|
||||
client: Client,
|
||||
user: string,
|
||||
password: string,
|
||||
callback: (success: boolean) => void
|
||||
) {
|
||||
// TODO: Enable the use of starttls() as an alternative to ldaps
|
||||
|
||||
// TODO: move this out of here and get rid of `manager` and `client` in
|
||||
// auth plugin API
|
||||
function callbackWrapper(valid) {
|
||||
function callbackWrapper(valid: boolean) {
|
||||
if (valid && !client) {
|
||||
manager.addUser(user, null, true);
|
||||
}
|
||||
|
@ -173,7 +186,7 @@ function advancedLdapLoadUsers(users, callbackLoadUser) {
|
|||
filter: `${config.ldap.searchDN.filter}`,
|
||||
attributes: [config.ldap.primaryKey],
|
||||
paged: true,
|
||||
};
|
||||
} as SearchOptions;
|
||||
|
||||
ldapclient.search(base, searchOptions, function (err2, res) {
|
||||
if (err2) {
|
||||
|
@ -182,6 +195,8 @@ function advancedLdapLoadUsers(users, callbackLoadUser) {
|
|||
}
|
||||
|
||||
res.on("searchEntry", function (entry) {
|
||||
//@ts-ignore
|
||||
//TODO
|
||||
const user = entry.attributes[0]._vals[0].toString();
|
||||
|
||||
if (remainingUsers.has(user)) {
|
||||
|
@ -226,7 +241,7 @@ function isLdapEnabled() {
|
|||
return !Config.values.public && Config.values.ldap.enable;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
moduleName: "ldap",
|
||||
auth: ldapAuth,
|
||||
isEnabled: isLdapEnabled,
|
|
@ -1,18 +1,18 @@
|
|||
"use strict";
|
||||
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const crypto = require("crypto");
|
||||
const {md, pki} = require("node-forge");
|
||||
const log = require("../log");
|
||||
const Config = require("../config");
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import crypto from "crypto";
|
||||
import {md, pki} from "node-forge";
|
||||
import log from "../log";
|
||||
import Config from "../config";
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
get,
|
||||
remove,
|
||||
};
|
||||
|
||||
function get(uuid) {
|
||||
function get(uuid: string): ClientCertificate {
|
||||
if (Config.values.public) {
|
||||
return null;
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ function get(uuid) {
|
|||
return {
|
||||
private_key: fs.readFileSync(paths.privateKeyPath, "utf-8"),
|
||||
certificate: fs.readFileSync(paths.certificatePath, "utf-8"),
|
||||
};
|
||||
} as ClientCertificate;
|
||||
} catch (e) {
|
||||
log.error("Unable to get certificate", e);
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ function get(uuid) {
|
|||
return null;
|
||||
}
|
||||
|
||||
function remove(uuid) {
|
||||
function remove(uuid: string) {
|
||||
if (Config.values.public) {
|
||||
return null;
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ function remove(uuid) {
|
|||
}
|
||||
}
|
||||
|
||||
function generateAndWrite(folderPath, paths) {
|
||||
function generateAndWrite(folderPath: string, paths: {privateKeyPath: any; certificatePath: any}) {
|
||||
const certificate = generate();
|
||||
|
||||
try {
|
||||
|
@ -121,12 +121,12 @@ function generate() {
|
|||
const pem = {
|
||||
private_key: pki.privateKeyToPem(keys.privateKey),
|
||||
certificate: pki.certificateToPem(cert),
|
||||
};
|
||||
} as ClientCertificate;
|
||||
|
||||
return pem;
|
||||
}
|
||||
|
||||
function getPaths(folderPath, uuid) {
|
||||
function getPaths(folderPath: string, uuid: string) {
|
||||
return {
|
||||
privateKeyPath: path.join(folderPath, `${uuid}.pem`),
|
||||
certificatePath: path.join(folderPath, `${uuid}.crt`),
|
|
@ -6,7 +6,7 @@ const Msg = require("../../models/msg");
|
|||
exports.commands = ["slap", "me"];
|
||||
|
||||
exports.input = function ({irc}, chan, cmd, args) {
|
||||
if (chan.type !== Chan.Type.CHANNEL && chan.type !== Chan.Type.QUERY) {
|
||||
if (chan.type !== ChanType.CHANNEL && chan.type !== ChanType.QUERY) {
|
||||
chan.pushMessage(
|
||||
this,
|
||||
new Msg({
|
||||
|
|
|
@ -6,7 +6,7 @@ const Msg = require("../../models/msg");
|
|||
exports.commands = ["ban", "unban", "banlist", "kickban"];
|
||||
|
||||
exports.input = function ({irc}, chan, cmd, args) {
|
||||
if (chan.type !== Chan.Type.CHANNEL) {
|
||||
if (chan.type !== ChanType.CHANNEL) {
|
||||
chan.pushMessage(
|
||||
this,
|
||||
new Msg({
|
||||
|
|
|
@ -119,7 +119,7 @@ exports.input = function (network, chan, cmd, args) {
|
|||
|
||||
if (typeof newChan === "undefined") {
|
||||
newChan = client.createChannel({
|
||||
type: Chan.Type.SPECIAL,
|
||||
type: ChanType.SPECIAL,
|
||||
special: Chan.SpecialType.IGNORELIST,
|
||||
name: chanName,
|
||||
data: ignored,
|
||||
|
|
|
@ -13,7 +13,7 @@ exports.input = function ({irc}, chan, cmd, args) {
|
|||
|
||||
if (args.length === 2) {
|
||||
irc.raw("INVITE", args[0], args[1]); // Channel provided in the command
|
||||
} else if (args.length === 1 && chan.type === Chan.Type.CHANNEL) {
|
||||
} else if (args.length === 1 && chan.type === ChanType.CHANNEL) {
|
||||
irc.raw("INVITE", args[0], chan.name); // Current channel
|
||||
} else {
|
||||
chan.pushMessage(
|
||||
|
|
|
@ -6,7 +6,7 @@ const Msg = require("../../models/msg");
|
|||
exports.commands = ["kick"];
|
||||
|
||||
exports.input = function ({irc}, chan, cmd, args) {
|
||||
if (chan.type !== Chan.Type.CHANNEL) {
|
||||
if (chan.type !== ChanType.CHANNEL) {
|
||||
chan.pushMessage(
|
||||
this,
|
||||
new Msg({
|
||||
|
|
|
@ -11,7 +11,7 @@ exports.input = function ({irc, nick}, chan, cmd, args) {
|
|||
|
||||
return;
|
||||
} else if (cmd !== "mode") {
|
||||
if (chan.type !== Chan.Type.CHANNEL) {
|
||||
if (chan.type !== ChanType.CHANNEL) {
|
||||
chan.pushMessage(
|
||||
this,
|
||||
new Msg({
|
||||
|
@ -59,7 +59,7 @@ exports.input = function ({irc, nick}, chan, cmd, args) {
|
|||
|
||||
if (args.length === 0 || args[0][0] === "+" || args[0][0] === "-") {
|
||||
args.unshift(
|
||||
chan.type === Chan.Type.CHANNEL || chan.type === Chan.Type.QUERY ? chan.name : nick
|
||||
chan.type === ChanType.CHANNEL || chan.type === ChanType.QUERY ? chan.name : nick
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"use strict";
|
||||
|
||||
const {ChanType} = require("src/types/models/channel");
|
||||
const Chan = require("../../models/chan");
|
||||
const Msg = require("../../models/msg");
|
||||
|
||||
|
@ -63,7 +64,7 @@ exports.input = function (network, chan, cmd, args) {
|
|||
}
|
||||
|
||||
const newChan = this.createChannel({
|
||||
type: Chan.Type.QUERY,
|
||||
type: ChanType.QUERY,
|
||||
name: targetName,
|
||||
});
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ exports.input = function (network, chan, cmd, args) {
|
|||
}
|
||||
}
|
||||
|
||||
if (target.type === Chan.Type.LOBBY) {
|
||||
if (target.type === ChanType.LOBBY) {
|
||||
chan.pushMessage(
|
||||
this,
|
||||
new Msg({
|
||||
|
@ -34,7 +34,7 @@ exports.input = function (network, chan, cmd, args) {
|
|||
// If target is not a channel or we are not connected, instantly remove the channel
|
||||
// Otherwise send part to the server and wait for response
|
||||
if (
|
||||
target.type !== Chan.Type.CHANNEL ||
|
||||
target.type !== ChanType.CHANNEL ||
|
||||
target.state === Chan.State.PARTED ||
|
||||
!network.irc ||
|
||||
!network.irc.connection ||
|
||||
|
|
|
@ -6,7 +6,7 @@ const Chan = require("../../models/chan");
|
|||
exports.commands = ["cycle", "rejoin"];
|
||||
|
||||
exports.input = function ({irc}, chan) {
|
||||
if (chan.type !== Chan.Type.CHANNEL) {
|
||||
if (chan.type !== ChanType.CHANNEL) {
|
||||
chan.pushMessage(
|
||||
this,
|
||||
new Msg({
|
||||
|
|
|
@ -6,7 +6,7 @@ const Msg = require("../../models/msg");
|
|||
exports.commands = ["topic"];
|
||||
|
||||
exports.input = function ({irc}, chan, cmd, args) {
|
||||
if (chan.type !== Chan.Type.CHANNEL) {
|
||||
if (chan.type !== ChanType.CHANNEL) {
|
||||
chan.pushMessage(
|
||||
this,
|
||||
new Msg({
|
||||
|
|
|
@ -28,8 +28,8 @@ module.exports = function (irc, network) {
|
|||
network.channels.forEach((chan) => {
|
||||
let user;
|
||||
|
||||
switch (chan.type) {
|
||||
case Chan.Type.QUERY: {
|
||||
switch (ChanType) {
|
||||
case ChanType.QUERY: {
|
||||
if (data.nick.toLowerCase() !== chan.name.toLowerCase()) {
|
||||
return;
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ module.exports = function (irc, network) {
|
|||
break;
|
||||
}
|
||||
|
||||
case Chan.Type.CHANNEL: {
|
||||
case ChanType.CHANNEL: {
|
||||
user = chan.findUser(data.nick);
|
||||
|
||||
if (!user || user.away === away) {
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
"use strict";
|
||||
|
||||
const _ = require("lodash");
|
||||
const log = require("../../log");
|
||||
const Msg = require("../../models/msg");
|
||||
const Chan = require("../../models/chan");
|
||||
const Helper = require("../../helper");
|
||||
const Config = require("../../config");
|
||||
import _ from "lodash";
|
||||
import log from "../../log";
|
||||
import Msg from "../../models/msg";
|
||||
import Chan from "../../models/chan";
|
||||
import Helper from "../../helper";
|
||||
import Config from "../../config";
|
||||
import Network from "src/models/network";
|
||||
import {ChanState, ChanType} from "src/types/models/channel";
|
||||
import {MessageType} from "src/types/models/message";
|
||||
|
||||
module.exports = function (irc, network) {
|
||||
export default function (irc: Network["irc"], network: Network) {
|
||||
const client = this;
|
||||
|
||||
network.channels[0].pushMessage(
|
||||
|
@ -52,7 +55,7 @@ module.exports = function (irc, network) {
|
|||
}
|
||||
|
||||
network.channels.forEach((chan) => {
|
||||
if (chan.type !== Chan.Type.CHANNEL) {
|
||||
if (chan.type !== ChanType.CHANNEL) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -109,14 +112,14 @@ module.exports = function (irc, network) {
|
|||
|
||||
network.channels.forEach((chan) => {
|
||||
chan.users = new Map();
|
||||
chan.state = Chan.State.PARTED;
|
||||
chan.state = ChanState.PARTED;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
network.channels[0].pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
type: MessageType.ERROR,
|
||||
text: `Connection closed unexpectedly: ${error}`,
|
||||
}),
|
||||
true
|
||||
|
@ -154,7 +157,7 @@ module.exports = function (irc, network) {
|
|||
client,
|
||||
new Msg({
|
||||
self: !message.from_server,
|
||||
type: Msg.Type.RAW,
|
||||
type: MessageType.RAW,
|
||||
text: message.line,
|
||||
}),
|
||||
true
|
||||
|
@ -166,7 +169,7 @@ module.exports = function (irc, network) {
|
|||
network.channels[0].pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
type: MessageType.ERROR,
|
||||
text: "Socket error: " + err,
|
||||
}),
|
||||
true
|
||||
|
@ -212,8 +215,11 @@ module.exports = function (irc, network) {
|
|||
|
||||
function sendStatus() {
|
||||
const status = network.getNetworkStatus();
|
||||
status.network = network.uuid;
|
||||
const toSend = {
|
||||
...status,
|
||||
network: network.uuid,
|
||||
};
|
||||
|
||||
client.emit("network:status", status);
|
||||
client.emit("network:status", toSend);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -35,7 +35,7 @@ module.exports = function (irc, network) {
|
|||
|
||||
if (typeof chan === "undefined") {
|
||||
chan = client.createChannel({
|
||||
type: Chan.Type.SPECIAL,
|
||||
type: ChanType.SPECIAL,
|
||||
special: Chan.SpecialType.CHANNELLIST,
|
||||
name: "Channel List",
|
||||
data: msg,
|
||||
|
|
|
@ -56,7 +56,7 @@ module.exports = function (irc, network) {
|
|||
data.from_server &&
|
||||
(!data.target ||
|
||||
!network.getChannel(data.target) ||
|
||||
network.getChannel(data.target).type !== Chan.Type.CHANNEL)
|
||||
network.getChannel(data.target).type !== ChanType.CHANNEL)
|
||||
) {
|
||||
chan = network.channels[0];
|
||||
from = chan.getUser(data.nick);
|
||||
|
@ -81,7 +81,7 @@ module.exports = function (irc, network) {
|
|||
chan = network.channels[0];
|
||||
} else {
|
||||
chan = client.createChannel({
|
||||
type: Chan.Type.QUERY,
|
||||
type: ChanType.QUERY,
|
||||
name: target,
|
||||
});
|
||||
|
||||
|
@ -98,9 +98,9 @@ module.exports = function (irc, network) {
|
|||
from = chan.getUser(data.nick);
|
||||
|
||||
// Query messages (unless self or muted) always highlight
|
||||
if (chan.type === Chan.Type.QUERY) {
|
||||
if (chan.type === ChanType.QUERY) {
|
||||
highlight = !self;
|
||||
} else if (chan.type === Chan.Type.CHANNEL) {
|
||||
} else if (chan.type === ChanType.CHANNEL) {
|
||||
from.lastMessage = data.time || Date.now();
|
||||
}
|
||||
}
|
||||
|
@ -166,7 +166,7 @@ module.exports = function (irc, network) {
|
|||
if (msg.type === Msg.Type.ACTION) {
|
||||
// For actions, do not include colon in the message
|
||||
body = `${data.nick} ${body}`;
|
||||
} else if (chan.type !== Chan.Type.QUERY) {
|
||||
} else if (chan.type !== ChanType.QUERY) {
|
||||
// In channels, prepend sender nickname to the message
|
||||
body = `${data.nick}: ${body}`;
|
||||
}
|
||||
|
@ -174,7 +174,7 @@ module.exports = function (irc, network) {
|
|||
// If a channel is active on any client, highlight won't increment and notification will say (0 mention)
|
||||
if (chan.highlight > 0) {
|
||||
title += ` (${chan.highlight} ${
|
||||
chan.type === Chan.Type.QUERY ? "new message" : "mention"
|
||||
chan.type === ChanType.QUERY ? "new message" : "mention"
|
||||
}${chan.highlight > 1 ? "s" : ""})`;
|
||||
}
|
||||
|
||||
|
@ -198,7 +198,7 @@ module.exports = function (irc, network) {
|
|||
}
|
||||
|
||||
// Keep track of all mentions in channels for this client
|
||||
if (msg.highlight && chan.type === Chan.Type.CHANNEL) {
|
||||
if (msg.highlight && chan.type === ChanType.CHANNEL) {
|
||||
client.mentions.push({
|
||||
chanId: chan.id,
|
||||
msgId: msg.id,
|
||||
|
|
|
@ -51,7 +51,7 @@ module.exports = function (irc, network) {
|
|||
|
||||
if (typeof chan === "undefined") {
|
||||
chan = client.createChannel({
|
||||
type: Chan.Type.SPECIAL,
|
||||
type: ChanType.SPECIAL,
|
||||
special: type,
|
||||
name: chanName,
|
||||
data: data,
|
||||
|
|
|
@ -23,7 +23,7 @@ module.exports = function (irc, network) {
|
|||
chan = network.channels[0];
|
||||
} else {
|
||||
chan = client.createChannel({
|
||||
type: Chan.Type.QUERY,
|
||||
type: ChanType.QUERY,
|
||||
name: data.nick,
|
||||
});
|
||||
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
"use strict";
|
||||
|
||||
const log = require("../../log");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const Config = require("../../config");
|
||||
const Msg = require("../../models/msg");
|
||||
import log from "../../log";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import Config from "../../config";
|
||||
import Msg from "../../models/msg";
|
||||
import type {Database} from "sqlite3";
|
||||
import {Network} from "src/types/models/network";
|
||||
import {Channel} from "src/types/models/channel";
|
||||
import {Message} from "src/types/models/message";
|
||||
import Client from "src/client";
|
||||
import Chan from "src/models/chan";
|
||||
|
||||
let sqlite3;
|
||||
|
||||
|
@ -28,8 +34,12 @@ const schema = [
|
|||
"CREATE INDEX IF NOT EXISTS time ON messages (time)",
|
||||
];
|
||||
|
||||
class MessageStorage {
|
||||
constructor(client) {
|
||||
class SqliteMessageStorage implements SqliteMessageStorage {
|
||||
client: Client;
|
||||
isEnabled: boolean;
|
||||
database: Database;
|
||||
|
||||
constructor(client: Client) {
|
||||
this.client = client;
|
||||
this.isEnabled = false;
|
||||
}
|
||||
|
@ -98,7 +108,7 @@ class MessageStorage {
|
|||
});
|
||||
}
|
||||
|
||||
close(callback) {
|
||||
close(callback?: (error?: Error) => void) {
|
||||
if (!this.isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
@ -116,7 +126,7 @@ class MessageStorage {
|
|||
});
|
||||
}
|
||||
|
||||
index(network, channel, msg) {
|
||||
index(network: Network, channel: Chan, msg: Msg) {
|
||||
if (!this.isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
@ -144,7 +154,7 @@ class MessageStorage {
|
|||
);
|
||||
}
|
||||
|
||||
deleteChannel(network, channel) {
|
||||
deleteChannel(network: Network, channel: Channel) {
|
||||
if (!this.isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
@ -164,7 +174,7 @@ class MessageStorage {
|
|||
* @param Network network - Network object where the channel is
|
||||
* @param Chan channel - Channel object for which to load messages for
|
||||
*/
|
||||
getMessages(network, channel) {
|
||||
getMessages(network: Network, channel: Channel) {
|
||||
if (!this.isEnabled || Config.values.maxHistory === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
@ -197,7 +207,7 @@ class MessageStorage {
|
|||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
}) as Promise<Message[]>;
|
||||
}
|
||||
|
||||
search(query) {
|
||||
|
@ -225,7 +235,7 @@ class MessageStorage {
|
|||
const maxResults = 100;
|
||||
|
||||
select += " ORDER BY time DESC LIMIT ? OFFSET ? ";
|
||||
params.push(maxResults);
|
||||
params.push(maxResults.toString());
|
||||
query.offset = parseInt(query.offset, 10) || 0;
|
||||
params.push(query.offset);
|
||||
|
||||
|
@ -252,7 +262,7 @@ class MessageStorage {
|
|||
}
|
||||
}
|
||||
|
||||
module.exports = MessageStorage;
|
||||
export default SqliteMessageStorage;
|
||||
|
||||
function parseSearchRowsToMessages(id, rows) {
|
||||
const messages = [];
|
|
@ -1,14 +1,22 @@
|
|||
"use strict";
|
||||
|
||||
const log = require("../../log");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const filenamify = require("filenamify");
|
||||
const Config = require("../../config");
|
||||
const Msg = require("../../models/msg");
|
||||
import log from "../../log";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import filenamify from "filenamify";
|
||||
import Config from "../../config";
|
||||
import Msg from "../../models/msg";
|
||||
import {Network} from "src/types/models/network";
|
||||
import {Channel} from "src/types/models/channel";
|
||||
import {Message, MessageType} from "src/types/models/message";
|
||||
import {MessageStorage} from "src/types/plugins/messageStorage";
|
||||
import Client from "src/client";
|
||||
|
||||
class TextFileMessageStorage {
|
||||
constructor(client) {
|
||||
class TextFileMessageStorage implements MessageStorage {
|
||||
client: Client;
|
||||
isEnabled: boolean;
|
||||
|
||||
constructor(client: Client) {
|
||||
this.client = client;
|
||||
this.isEnabled = false;
|
||||
}
|
||||
|
@ -17,7 +25,7 @@ class TextFileMessageStorage {
|
|||
this.isEnabled = true;
|
||||
}
|
||||
|
||||
close(callback) {
|
||||
close(callback: () => void) {
|
||||
this.isEnabled = false;
|
||||
|
||||
if (callback) {
|
||||
|
@ -25,7 +33,7 @@ class TextFileMessageStorage {
|
|||
}
|
||||
}
|
||||
|
||||
index(network, channel, msg) {
|
||||
index(network: Network, channel: Channel, msg: Message) {
|
||||
if (!this.isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
@ -47,47 +55,47 @@ class TextFileMessageStorage {
|
|||
|
||||
// message types from src/models/msg.js
|
||||
switch (msg.type) {
|
||||
case Msg.Type.ACTION:
|
||||
case MessageType.ACTION:
|
||||
// [2014-01-01 00:00:00] * @Arnold is eating cookies
|
||||
line += `* ${msg.from.mode}${msg.from.nick} ${msg.text}`;
|
||||
break;
|
||||
case Msg.Type.JOIN:
|
||||
case MessageType.JOIN:
|
||||
// [2014-01-01 00:00:00] *** Arnold (~arnold@foo.bar) joined
|
||||
line += `*** ${msg.from.nick} (${msg.hostmask}) joined`;
|
||||
break;
|
||||
case Msg.Type.KICK:
|
||||
case MessageType.KICK:
|
||||
// [2014-01-01 00:00:00] *** Arnold was kicked by Bernie (Don't steal my cookies!)
|
||||
line += `*** ${msg.target.nick} was kicked by ${msg.from.nick} (${msg.text})`;
|
||||
break;
|
||||
case Msg.Type.MESSAGE:
|
||||
case MessageType.MESSAGE:
|
||||
// [2014-01-01 00:00:00] <@Arnold> Put that cookie down.. Now!!
|
||||
line += `<${msg.from.mode}${msg.from.nick}> ${msg.text}`;
|
||||
break;
|
||||
case Msg.Type.MODE:
|
||||
case MessageType.MODE:
|
||||
// [2014-01-01 00:00:00] *** Arnold set mode +o Bernie
|
||||
line += `*** ${msg.from.nick} set mode ${msg.text}`;
|
||||
break;
|
||||
case Msg.Type.NICK:
|
||||
case MessageType.NICK:
|
||||
// [2014-01-01 00:00:00] *** Arnold changed nick to Bernie
|
||||
line += `*** ${msg.from.nick} changed nick to ${msg.new_nick}`;
|
||||
break;
|
||||
case Msg.Type.NOTICE:
|
||||
case MessageType.NOTICE:
|
||||
// [2014-01-01 00:00:00] -Arnold- pssst, I have cookies!
|
||||
line += `-${msg.from.nick}- ${msg.text}`;
|
||||
break;
|
||||
case Msg.Type.PART:
|
||||
case MessageType.PART:
|
||||
// [2014-01-01 00:00:00] *** Arnold (~arnold@foo.bar) left (Bye all!)
|
||||
line += `*** ${msg.from.nick} (${msg.hostmask}) left (${msg.text})`;
|
||||
break;
|
||||
case Msg.Type.QUIT:
|
||||
case MessageType.QUIT:
|
||||
// [2014-01-01 00:00:00] *** Arnold (~arnold@foo.bar) quit (Connection reset by peer)
|
||||
line += `*** ${msg.from.nick} (${msg.hostmask}) quit (${msg.text})`;
|
||||
break;
|
||||
case Msg.Type.CHGHOST:
|
||||
case MessageType.CHGHOST:
|
||||
// [2014-01-01 00:00:00] *** Arnold changed host to: new@fancy.host
|
||||
line += `*** ${msg.from.nick} changed host to '${msg.new_ident}@${msg.new_host}'`;
|
||||
break;
|
||||
case Msg.Type.TOPIC:
|
||||
case MessageType.TOPIC:
|
||||
// [2014-01-01 00:00:00] *** Arnold changed topic to: welcome everyone!
|
||||
line += `*** ${msg.from.nick} changed topic to '${msg.text}'`;
|
||||
break;
|
||||
|
@ -141,7 +149,7 @@ class TextFileMessageStorage {
|
|||
return false;
|
||||
}
|
||||
|
||||
static getNetworkFolderName(network) {
|
||||
static getNetworkFolderName(network: Network) {
|
||||
// Limit network name in the folder name to 23 characters
|
||||
// So we can still fit 12 characters of the uuid for de-duplication
|
||||
const networkName = cleanFilename(network.name.substring(0, 23).replace(/ /g, "-"));
|
||||
|
@ -149,12 +157,12 @@ class TextFileMessageStorage {
|
|||
return `${networkName}-${network.uuid.substring(networkName.length + 1)}`;
|
||||
}
|
||||
|
||||
static getChannelFileName(channel) {
|
||||
static getChannelFileName(channel: Channel) {
|
||||
return `${cleanFilename(channel.name)}.log`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TextFileMessageStorage;
|
||||
export default TextFileMessageStorage;
|
||||
|
||||
function cleanFilename(name) {
|
||||
name = filenamify(name, {replacement: "_"});
|
|
@ -1,20 +1,21 @@
|
|||
"use strict";
|
||||
|
||||
const _ = require("lodash");
|
||||
const log = require("../../log");
|
||||
const colors = require("chalk");
|
||||
const path = require("path");
|
||||
const semver = require("semver");
|
||||
const Helper = require("../../helper");
|
||||
const Config = require("../../config");
|
||||
const themes = require("./themes");
|
||||
import _ from "lodash";
|
||||
import log from "../../log";
|
||||
import colors from "chalk";
|
||||
import path from "path";
|
||||
import semver from "semver";
|
||||
import Helper from "../../helper";
|
||||
import Config from "../../config";
|
||||
import themes from "./themes";
|
||||
const packageMap = new Map();
|
||||
const inputs = require("../inputs");
|
||||
const fs = require("fs");
|
||||
const Utils = require("../../command-line/utils");
|
||||
import inputs from "../inputs";
|
||||
import fs from "fs";
|
||||
import Utils from "../../command-line/utils";
|
||||
import Client from "src/client";
|
||||
|
||||
const stylesheets = [];
|
||||
const files = [];
|
||||
const stylesheets: string[] = [];
|
||||
const files: string[] = [];
|
||||
|
||||
const TIME_TO_LIVE = 15 * 60 * 1000; // 15 minutes, in milliseconds
|
||||
|
||||
|
@ -24,7 +25,7 @@ const cache = {
|
|||
|
||||
let experimentalWarningPrinted = false;
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
getFiles,
|
||||
getStylesheets,
|
||||
getPackage,
|
||||
|
@ -42,7 +43,7 @@ const packageApis = function (packageInfo) {
|
|||
},
|
||||
Commands: {
|
||||
add: inputs.addPluginCommand.bind(this, packageInfo),
|
||||
runAsUser: (command, targetId, client) =>
|
||||
runAsUser: (command: string, targetId: number, client: Client) =>
|
||||
client.inputLine({target: targetId, text: command}),
|
||||
},
|
||||
Config: {
|
||||
|
@ -66,7 +67,7 @@ function getStylesheets() {
|
|||
return stylesheets;
|
||||
}
|
||||
|
||||
function addFile(packageName, filename) {
|
||||
function addFile(packageName: string, filename: string) {
|
||||
files.push(packageName + "/" + filename);
|
||||
}
|
||||
|
||||
|
@ -78,7 +79,7 @@ function getPackage(name) {
|
|||
return packageMap.get(name);
|
||||
}
|
||||
|
||||
function getEnabledPackages(packageJson) {
|
||||
function getEnabledPackages(packageJson: string) {
|
||||
try {
|
||||
const json = JSON.parse(fs.readFileSync(packageJson, "utf-8"));
|
||||
return Object.keys(json.dependencies);
|
||||
|
@ -89,15 +90,16 @@ function getEnabledPackages(packageJson) {
|
|||
return [];
|
||||
}
|
||||
|
||||
function getPersistentStorageDir(packageName) {
|
||||
function getPersistentStorageDir(packageName: string) {
|
||||
const dir = path.join(Config.getPackagesPath(), packageName);
|
||||
fs.mkdirSync(dir, {recursive: true}); // we don't care if it already exists or not
|
||||
return dir;
|
||||
}
|
||||
|
||||
function loadPackage(packageName) {
|
||||
let packageInfo;
|
||||
let packageFile;
|
||||
function loadPackage(packageName: string) {
|
||||
let packageInfo: PackageInfo;
|
||||
// TODO: type
|
||||
let packageFile: any;
|
||||
|
||||
try {
|
||||
const packagePath = Config.getPackageModulePath(packageName);
|
||||
|
@ -125,9 +127,11 @@ function loadPackage(packageName) {
|
|||
}
|
||||
|
||||
const version = packageInfo.version;
|
||||
packageInfo = packageInfo.thelounge;
|
||||
packageInfo.packageName = packageName;
|
||||
packageInfo.version = version;
|
||||
packageInfo = {
|
||||
...packageInfo.thelounge,
|
||||
packageName: packageName,
|
||||
version,
|
||||
};
|
||||
|
||||
packageMap.set(packageName, packageFile);
|
||||
|
||||
|
@ -164,7 +168,7 @@ function loadPackages() {
|
|||
watchPackages(packageJson);
|
||||
}
|
||||
|
||||
function watchPackages(packageJson) {
|
||||
function watchPackages(packageJson: string) {
|
||||
fs.watch(
|
||||
packageJson,
|
||||
{
|
||||
|
@ -219,7 +223,8 @@ async function outdated(cacheTimeout = TIME_TO_LIVE) {
|
|||
}
|
||||
|
||||
// If we get an error from calling outdated and the code isn't 0, then there are no outdated packages
|
||||
await Utils.executeYarnCommand(...argsList)
|
||||
// TODO: was (...argsList), verify this works
|
||||
await Utils.executeYarnCommand(argsList.shift(), ...argsList)
|
||||
.then(() => updateOutdated(false))
|
||||
.catch((code) => updateOutdated(code !== 0));
|
||||
|
|
@ -1,12 +1,14 @@
|
|||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const Config = require("../../config");
|
||||
const path = require("path");
|
||||
const _ = require("lodash");
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import _ from "lodash";
|
||||
|
||||
import Config from "../../config";
|
||||
|
||||
const themes = new Map();
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
addTheme,
|
||||
getAll,
|
||||
getByName,
|
||||
|
@ -24,7 +26,7 @@ function loadLocalThemes() {
|
|||
.forEach((theme) => themes.set(theme.name, theme));
|
||||
}
|
||||
|
||||
function addTheme(packageName, packageObject) {
|
||||
function addTheme(packageName: string, packageObject) {
|
||||
const theme = makePackageThemeObject(packageName, packageObject);
|
||||
|
||||
if (theme) {
|
||||
|
@ -46,7 +48,7 @@ function getByName(name) {
|
|||
return themes.get(name);
|
||||
}
|
||||
|
||||
function makeLocalThemeObject(css) {
|
||||
function makeLocalThemeObject(css: string) {
|
||||
const themeName = css.slice(0, -4);
|
||||
return {
|
||||
displayName: themeName.charAt(0).toUpperCase() + themeName.slice(1),
|
||||
|
@ -55,7 +57,7 @@ function makeLocalThemeObject(css) {
|
|||
};
|
||||
}
|
||||
|
||||
function makePackageThemeObject(moduleName, module) {
|
||||
function makePackageThemeObject(moduleName: string, module: ThemeModule) {
|
||||
if (!module || module.type !== "theme") {
|
||||
return;
|
||||
}
|
|
@ -1,12 +1,17 @@
|
|||
"use strict";
|
||||
|
||||
const _ = require("lodash");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const log = require("../log");
|
||||
const Config = require("../config");
|
||||
import _ from "lodash";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import log from "../log";
|
||||
import Config from "../config";
|
||||
import type {PolicyMap, PolicyOption} from "src/types/plugins/sts";
|
||||
|
||||
class STSPolicies {
|
||||
private stsFile: string;
|
||||
private policies: PolicyMap;
|
||||
private refresh: _.DebouncedFunc<any>;
|
||||
|
||||
constructor() {
|
||||
this.stsFile = path.join(Config.getHomePath(), "sts-policies.json");
|
||||
this.policies = new Map();
|
||||
|
@ -16,7 +21,7 @@ class STSPolicies {
|
|||
return;
|
||||
}
|
||||
|
||||
const storedPolicies = JSON.parse(fs.readFileSync(this.stsFile, "utf-8"));
|
||||
const storedPolicies = JSON.parse(fs.readFileSync(this.stsFile, "utf-8")) as PolicyOption[];
|
||||
const now = Date.now();
|
||||
|
||||
storedPolicies.forEach((value) => {
|
||||
|
@ -30,7 +35,7 @@ class STSPolicies {
|
|||
});
|
||||
}
|
||||
|
||||
get(host) {
|
||||
get(host: string) {
|
||||
const policy = this.policies.get(host);
|
||||
|
||||
if (typeof policy === "undefined") {
|
||||
|
@ -46,7 +51,7 @@ class STSPolicies {
|
|||
return policy;
|
||||
}
|
||||
|
||||
update(host, port, duration) {
|
||||
update(host: string, port: number, duration: number) {
|
||||
if (duration > 0) {
|
||||
this.policies.set(host, {
|
||||
port: port,
|
||||
|
@ -60,7 +65,7 @@ class STSPolicies {
|
|||
this.refresh();
|
||||
}
|
||||
|
||||
refreshExpiration(host) {
|
||||
refreshExpiration(host: string) {
|
||||
const policy = this.policies.get(host);
|
||||
|
||||
if (typeof policy === "undefined") {
|
||||
|
@ -92,4 +97,4 @@ class STSPolicies {
|
|||
}
|
||||
}
|
||||
|
||||
module.exports = new STSPolicies();
|
||||
export default new STSPolicies();
|
|
@ -1,37 +1,50 @@
|
|||
"use strict";
|
||||
|
||||
const _ = require("lodash");
|
||||
const log = require("./log");
|
||||
const pkg = require("../package.json");
|
||||
const Client = require("./client");
|
||||
const ClientManager = require("./clientManager");
|
||||
const express = require("express");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const io = require("socket.io");
|
||||
const dns = require("dns");
|
||||
const Uploader = require("./plugins/uploader");
|
||||
const Helper = require("./helper");
|
||||
const Config = require("./config");
|
||||
const colors = require("chalk");
|
||||
const net = require("net");
|
||||
const Identification = require("./identification");
|
||||
const changelog = require("./plugins/changelog");
|
||||
const inputs = require("./plugins/inputs");
|
||||
const Auth = require("./plugins/auth");
|
||||
import _ from "lodash";
|
||||
import log from "./log";
|
||||
import pkg from "../package.json";
|
||||
import Client from "./client";
|
||||
import ClientManager from "./clientManager";
|
||||
import express from "express";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import {Server} from "socket.io";
|
||||
import dns from "dns";
|
||||
import Uploader from "./plugins/uploader";
|
||||
import Helper from "./helper";
|
||||
import Config from "./config";
|
||||
import colors from "chalk";
|
||||
import net from "net";
|
||||
import Identification from "./identification";
|
||||
import changelog from "./plugins/changelog";
|
||||
import inputs from "./plugins/inputs";
|
||||
import Auth from "./plugins/auth";
|
||||
|
||||
const themes = require("./plugins/packages/themes");
|
||||
import themes from "./plugins/packages/themes";
|
||||
themes.loadLocalThemes();
|
||||
|
||||
const packages = require("./plugins/packages/index");
|
||||
const Chan = require("./models/chan");
|
||||
import packages from "./plugins/packages/index";
|
||||
import Chan from "./models/chan";
|
||||
import {
|
||||
ClientConfiguration,
|
||||
Defaults,
|
||||
IndexTemplateConfiguration,
|
||||
ServerConfiguration,
|
||||
} from "./types/config";
|
||||
|
||||
import {Server as wsServer} from "ws";
|
||||
import {ChanType} from "./types/models/channel";
|
||||
|
||||
// A random number that will force clients to reload the page if it differs
|
||||
const serverHash = Math.floor(Date.now() * Math.random());
|
||||
|
||||
let manager = null;
|
||||
|
||||
module.exports = function (options = {}) {
|
||||
export default function (
|
||||
options: ServerOptions = {
|
||||
dev: false,
|
||||
}
|
||||
) {
|
||||
log.info(`The Lounge ${colors.green(Helper.getVersion())} \
|
||||
(Node.js ${colors.green(process.versions.node)} on ${colors.green(process.platform)} ${
|
||||
process.arch
|
||||
|
@ -165,11 +178,13 @@ module.exports = function (options = {}) {
|
|||
);
|
||||
}
|
||||
|
||||
const sockets = io(server, {
|
||||
wsEngine: require("ws").Server,
|
||||
const sockets = new Server(server, {
|
||||
wsEngine: wsServer,
|
||||
cookie: false,
|
||||
serveClient: false,
|
||||
transports: Config.values.transports,
|
||||
|
||||
// TODO: type as Server.Transport[]
|
||||
transports: Config.values.transports as any,
|
||||
pingTimeout: 60000,
|
||||
});
|
||||
|
||||
|
@ -250,7 +265,7 @@ module.exports = function (options = {}) {
|
|||
});
|
||||
|
||||
return server;
|
||||
};
|
||||
}
|
||||
|
||||
function getClientLanguage(socket) {
|
||||
const acceptLanguage = socket.handshake.headers["accept-language"];
|
||||
|
@ -342,7 +357,7 @@ function indexRequest(req, res) {
|
|||
throw err;
|
||||
}
|
||||
|
||||
const config = getServerConfiguration();
|
||||
const config = getServerConfiguration() as IndexTemplateConfiguration;
|
||||
config.cacheBust = Helper.getVersionCacheBust();
|
||||
|
||||
res.send(_.template(file)(config));
|
||||
|
@ -465,8 +480,8 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
|||
|
||||
const hash = Helper.password.hash(p1);
|
||||
|
||||
client.setPassword(hash, (success) => {
|
||||
const obj = {success: false};
|
||||
client.setPassword(hash, (success: boolean) => {
|
||||
const obj = {success: false, error: undefined};
|
||||
|
||||
if (success) {
|
||||
obj.success = true;
|
||||
|
@ -477,7 +492,7 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
|||
socket.emit("change-password", obj);
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch((error: any) => {
|
||||
log.error(`Error while checking users password. Error: ${error}`);
|
||||
});
|
||||
}
|
||||
|
@ -673,14 +688,14 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
|||
const {chan, network} = client.find(target);
|
||||
|
||||
// If the user mutes the lobby, we mute the entire network.
|
||||
if (chan.type === Chan.Type.LOBBY) {
|
||||
if (chan.type === ChanType.LOBBY) {
|
||||
for (const channel of network.channels) {
|
||||
if (channel.type !== Chan.Type.SPECIAL) {
|
||||
if (channel.type !== ChanType.SPECIAL) {
|
||||
channel.setMuteStatus(setMutedTo);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (chan.type !== Chan.Type.SPECIAL) {
|
||||
if (chan.type !== ChanType.SPECIAL) {
|
||||
chan.setMuteStatus(setMutedTo);
|
||||
}
|
||||
}
|
||||
|
@ -757,8 +772,13 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
|||
}
|
||||
}
|
||||
|
||||
function getClientConfiguration() {
|
||||
const config = _.pick(Config.values, ["public", "lockNetwork", "useHexIp", "prefetch"]);
|
||||
function getClientConfiguration(): ClientConfiguration {
|
||||
const config = _.pick(Config.values, [
|
||||
"public",
|
||||
"lockNetwork",
|
||||
"useHexIp",
|
||||
"prefetch",
|
||||
]) as ClientConfiguration;
|
||||
|
||||
config.fileUpload = Config.values.fileUpload.enable;
|
||||
config.ldapEnabled = Config.values.ldap.enable;
|
||||
|
@ -774,7 +794,7 @@ function getClientConfiguration() {
|
|||
"password",
|
||||
"realname",
|
||||
"join",
|
||||
]);
|
||||
]) as Defaults;
|
||||
}
|
||||
|
||||
config.isUpdateAvailable = changelog.isUpdateAvailable;
|
||||
|
@ -795,8 +815,8 @@ function getClientConfiguration() {
|
|||
return config;
|
||||
}
|
||||
|
||||
function getServerConfiguration() {
|
||||
const config = _.clone(Config.values);
|
||||
function getServerConfiguration(): ServerConfiguration {
|
||||
const config = _.clone(Config.values) as ServerConfiguration;
|
||||
|
||||
config.stylesheets = packages.getStylesheets();
|
||||
|
||||
|
@ -917,6 +937,9 @@ function reverseDnsLookup(ip, callback) {
|
|||
}
|
||||
|
||||
dns.resolve(hostnames[0], net.isIP(ip) === 6 ? "AAAA" : "A", (resolveErr, resolvedIps) => {
|
||||
// TODO: investigate SoaRecord class
|
||||
if (!Array.isArray(resolvedIps)) return callback(ip);
|
||||
|
||||
if (resolveErr || resolvedIps.length < 1) {
|
||||
return callback(ip);
|
||||
}
|
11
src/tsconfig.json
Normal file
11
src/tsconfig.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"files": ["index.d.ts"],
|
||||
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"types": ["node"],
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
}
|
40
src/types/client.d.ts
vendored
Normal file
40
src/types/client.d.ts
vendored
Normal file
|
@ -0,0 +1,40 @@
|
|||
import {MessageType, UserInMessage} from "./models/message";
|
||||
|
||||
type ClientConfig = {
|
||||
log: boolean;
|
||||
password: string;
|
||||
sessions: {
|
||||
[token: string]: {
|
||||
lastUse: number;
|
||||
ip: string;
|
||||
agent: string;
|
||||
pushSubscription: PushSubscription;
|
||||
};
|
||||
};
|
||||
clientSettings: {
|
||||
[key: string]: any;
|
||||
};
|
||||
browser?: {
|
||||
language?: string;
|
||||
ip?: string;
|
||||
hostname?: string;
|
||||
isSecure?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
type PushSubscription = {
|
||||
endpoint: string;
|
||||
keys: {
|
||||
p256dh: string;
|
||||
auth: string;
|
||||
};
|
||||
};
|
||||
|
||||
type Mention = {
|
||||
chanId: number;
|
||||
msgId: number;
|
||||
type: MessageType;
|
||||
time: number;
|
||||
text: string;
|
||||
from: UserInMessage;
|
||||
};
|
113
src/types/config.d.ts
vendored
Normal file
113
src/types/config.d.ts
vendored
Normal file
|
@ -0,0 +1,113 @@
|
|||
type Config = {
|
||||
public: boolean;
|
||||
host: string | undefined;
|
||||
port: number;
|
||||
bind: string | undefined;
|
||||
reverseProxy: boolean;
|
||||
maxHistory: number;
|
||||
https: Https;
|
||||
theme: string;
|
||||
prefetch: boolean;
|
||||
disableMediaPreview: boolean;
|
||||
prefetchStorage: boolean;
|
||||
prefetchMaxImageSize: number;
|
||||
prefetchMaxSearchSize: number;
|
||||
prefetchTimeout: number;
|
||||
fileUpload: FileUpload;
|
||||
transports: string[];
|
||||
leaveMessage: string;
|
||||
defaults: Defaults;
|
||||
lockNetwork: boolean;
|
||||
messageStorage: string[];
|
||||
useHexIp: boolean;
|
||||
webirc?: WebIRC;
|
||||
identd: Identd;
|
||||
oidentd?: string;
|
||||
ldap: Ldap;
|
||||
debug: Debug;
|
||||
themeColor: string;
|
||||
};
|
||||
|
||||
type ClientConfiguration = Pick<
|
||||
Config,
|
||||
"public" | "lockNetwork" | "useHexIp" | "prefetch" | "defaults"
|
||||
> & {
|
||||
fileUpload: boolean;
|
||||
ldapEnabled: boolean;
|
||||
isUpdateAvailable: boolean;
|
||||
applicationServerKey: string;
|
||||
version: string;
|
||||
gitCommit: string;
|
||||
defaultTheme: string;
|
||||
themes: string[];
|
||||
defaults: Defaults & {
|
||||
sasl?: string;
|
||||
saslAccount?: string;
|
||||
saslPassword?: string;
|
||||
};
|
||||
fileUploadMaxFileSize?: number;
|
||||
};
|
||||
|
||||
type ServerConfiguration = Config & {
|
||||
stylesheets: string[];
|
||||
};
|
||||
|
||||
// TODO: Type this
|
||||
type WebIRC = {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
type Https = {
|
||||
enable: boolean;
|
||||
key: string;
|
||||
certificate: string;
|
||||
ca: string;
|
||||
};
|
||||
|
||||
export type FileUpload = {
|
||||
enable: boolean;
|
||||
maxFileSize: number;
|
||||
baseUrl?: string;
|
||||
};
|
||||
|
||||
export type Defaults = {
|
||||
name: string;
|
||||
host: string;
|
||||
port: number;
|
||||
password: string;
|
||||
tls: boolean;
|
||||
rejectUnauthorized: boolean;
|
||||
nick: string;
|
||||
username: string;
|
||||
realname: string;
|
||||
join: string;
|
||||
leaveMessage: string;
|
||||
};
|
||||
|
||||
export type Identd = {
|
||||
enable: boolean;
|
||||
port: number;
|
||||
};
|
||||
|
||||
export type Ldap = {
|
||||
enable: boolean;
|
||||
url: string;
|
||||
tlsOptions: any;
|
||||
primaryKey: string;
|
||||
searchDN: SearchDN;
|
||||
};
|
||||
|
||||
export type TlsOptions = any;
|
||||
|
||||
export type SearchDN = {
|
||||
rootDN: string;
|
||||
rootPassword: string;
|
||||
filter: string;
|
||||
base: string;
|
||||
scope: string;
|
||||
};
|
||||
|
||||
export type Debug = {
|
||||
ircFramework: boolean;
|
||||
raw: boolean;
|
||||
};
|
5
src/types/helper.d.ts
vendored
Normal file
5
src/types/helper.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
type Hostmask = {
|
||||
nick: string;
|
||||
ident: string;
|
||||
hostname: string;
|
||||
};
|
5
src/types/index.d.ts
vendored
Normal file
5
src/types/index.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
/// <reference path="models/index.d.ts" />
|
||||
/// <reference path="plugins/index.d.ts" />
|
||||
/// <reference path="config.d.ts" />
|
||||
/// <reference path="helper.d.ts" />
|
||||
/// <reference path="server.d.ts" />
|
27
src/types/models/channel.d.ts
vendored
Normal file
27
src/types/models/channel.d.ts
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
import Chan from "src/models/chan";
|
||||
|
||||
export type Channel = Chan;
|
||||
|
||||
export type FilteredChannel = Chan & {
|
||||
users: [];
|
||||
totalMessages: number;
|
||||
};
|
||||
|
||||
export enum ChanType {
|
||||
CHANNEL = "channel",
|
||||
LOBBY = "lobby",
|
||||
QUERY = "query",
|
||||
SPECIAL = "special",
|
||||
}
|
||||
|
||||
export enum SpecialChanType {
|
||||
BANLIST = "list_bans",
|
||||
INVITELIST = "list_invites",
|
||||
CHANNELLIST = "list_channels",
|
||||
IGNORELIST = "list_ignored",
|
||||
}
|
||||
|
||||
export enum ChanState {
|
||||
PARTED = 0,
|
||||
JOINED = 1,
|
||||
}
|
3
src/types/models/index.d.ts
vendored
Normal file
3
src/types/models/index.d.ts
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/// <reference path="channel.d.ts" />
|
||||
/// <reference path="prefix.d.ts" />
|
||||
/// <reference path="message.d.ts" />
|
43
src/types/models/message.d.ts
vendored
Normal file
43
src/types/models/message.d.ts
vendored
Normal file
|
@ -0,0 +1,43 @@
|
|||
import Msg from "src/models/msg";
|
||||
import User from "src/models/user";
|
||||
|
||||
type Message = Msg;
|
||||
|
||||
type UserInMessage = Partial<User> & {
|
||||
mode: string;
|
||||
};
|
||||
|
||||
type MessagePreview = {
|
||||
link: string;
|
||||
};
|
||||
|
||||
export enum MessageType {
|
||||
UNHANDLED = "unhandled",
|
||||
ACTION = "action",
|
||||
AWAY = "away",
|
||||
BACK = "back",
|
||||
ERROR = "error",
|
||||
INVITE = "invite",
|
||||
JOIN = "join",
|
||||
KICK = "kick",
|
||||
LOGIN = "login",
|
||||
LOGOUT = "logout",
|
||||
MESSAGE = "message",
|
||||
MODE = "mode",
|
||||
MODE_CHANNEL = "mode_channel",
|
||||
MODE_USER = "mode_user", // RPL_UMODEIS
|
||||
MONOSPACE_BLOCK = "monospace_block",
|
||||
NICK = "nick",
|
||||
NOTICE = "notice",
|
||||
PART = "part",
|
||||
QUIT = "quit",
|
||||
CTCP = "ctcp",
|
||||
CTCP_REQUEST = "ctcp_request",
|
||||
CHGHOST = "chghost",
|
||||
TOPIC = "topic",
|
||||
TOPIC_SET_BY = "topic_set_by",
|
||||
WHOIS = "whois",
|
||||
RAW = "raw",
|
||||
PLUGIN = "plugin",
|
||||
WALLOPS = "wallops",
|
||||
}
|
8
src/types/models/network.d.ts
vendored
Normal file
8
src/types/models/network.d.ts
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
import NetworkClass from "src/models/network";
|
||||
|
||||
export type Network = NetworkClass;
|
||||
|
||||
export type NetworkStatus = {
|
||||
connected: boolean;
|
||||
secure: boolean;
|
||||
};
|
6
src/types/models/prefix.d.ts
vendored
Normal file
6
src/types/models/prefix.d.ts
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
type PrefixSymbol = string;
|
||||
|
||||
type PrefixObject = {
|
||||
symbol: PrefixSymbol;
|
||||
mode: string;
|
||||
};
|
3
src/types/models/user.d.ts
vendored
Normal file
3
src/types/models/user.d.ts
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
import UserClass from "src/models/user";
|
||||
|
||||
export type User = UserClass;
|
414
src/types/modules/irc-framework.d.ts
vendored
Normal file
414
src/types/modules/irc-framework.d.ts
vendored
Normal file
|
@ -0,0 +1,414 @@
|
|||
// https://raw.githubusercontent.com/eternagame/HTML-Chat/vue-rewrite/src/app/types/modules/irc-framework/irc-framework.d.ts
|
||||
|
||||
declare module "irc-framework" {
|
||||
import {EventEmitter} from "eventemitter3";
|
||||
// import { DuplexStream } from 'stream';
|
||||
import Connection from "irc-framework/src/transports/websocket";
|
||||
|
||||
type ConnectionOpts = {
|
||||
connected: boolean;
|
||||
requested_disconnect: boolean;
|
||||
|
||||
reconnect_attempts: number;
|
||||
|
||||
// When an IRC connection was successfully registered.
|
||||
registered: boolean;
|
||||
|
||||
transport: any;
|
||||
};
|
||||
|
||||
export class Client extends EventEmitter {
|
||||
constructor(options: ClientConstructorParameters);
|
||||
|
||||
// Added by Max
|
||||
connection: ConnectionOpts;
|
||||
network: {
|
||||
options: {
|
||||
CHANTYPES: string;
|
||||
PREFIX: any;
|
||||
CHANMODES: string;
|
||||
};
|
||||
cap: {
|
||||
isEnabled: (cap: string) => boolean;
|
||||
enabled: string[];
|
||||
};
|
||||
};
|
||||
// End of added by Max
|
||||
|
||||
static setDefaultTransport(transport: any): void;
|
||||
|
||||
// get Message(): ClassDecorator;//TODO
|
||||
/** Applies the default options to the options object given as impot, and returns it. */
|
||||
_applyDefaultOptions(
|
||||
user_options: ClientConstructorParameters
|
||||
): ClientConstructorParameters;
|
||||
|
||||
createStructure(): void;
|
||||
|
||||
/** Is connected to the IRC network and successfully registered. */
|
||||
connected: boolean;
|
||||
|
||||
// TODO
|
||||
/** The object for the connected message, as long as the client is connected. */ user: IrcUser;
|
||||
|
||||
// TODO
|
||||
/** Request */ requestCap(capability: string[]): void;
|
||||
|
||||
use(a: any): any;
|
||||
|
||||
connect(connect_options?: Object): void;
|
||||
|
||||
/**
|
||||
* Proxy the command handler events onto the client object, with some added sugar
|
||||
* Events are handled in order:
|
||||
* 1. Received from the command handler
|
||||
* 2. Checked if any extra properties/methods are to be added to the params + re-emitted
|
||||
* 3. Routed through middleware
|
||||
* 4. Emitted from the client instance
|
||||
*/
|
||||
proxyIrcEvents(): void;
|
||||
|
||||
addCommandHandlerListeners(): void;
|
||||
|
||||
registerToNetwork(): void;
|
||||
|
||||
startPeriodicPing(): void;
|
||||
|
||||
raw(...raw_data_line: string[]): void;
|
||||
|
||||
rawString(...parameters: Array<string>): string;
|
||||
|
||||
rawString(parameters: Array<string>): string;
|
||||
|
||||
quit(quit_message?: string): void;
|
||||
|
||||
ping(message?: string): void;
|
||||
|
||||
changeNick(nick: string): void;
|
||||
|
||||
sendMessage(commandName: string, target: string, message: string): string[];
|
||||
|
||||
say(target: string, message: string): string[];
|
||||
|
||||
notice(target: string, message: string): string[];
|
||||
|
||||
join(channel: string, key?: string): void;
|
||||
|
||||
part(channel: string, message?: string): void;
|
||||
|
||||
mode(channel: string, mode: string, extra_args?: string[]): void;
|
||||
|
||||
inviteList(channel: string, cb: (e: Event) => any): void;
|
||||
|
||||
// TODO: typeof e?
|
||||
invite(channel: string, nick: string): void;
|
||||
|
||||
addInvite(channel: String, mask: string): void;
|
||||
|
||||
removeInvite(channel: string, mask: string): void;
|
||||
|
||||
banlist(channel: string, cb: (e: Event) => any): void;
|
||||
|
||||
ban(channel: string, mask: string): void;
|
||||
|
||||
unban(channel: string, mask: string): void;
|
||||
|
||||
setTopic(channel: string, newTopic: string): void;
|
||||
|
||||
ctcpRequest(target: string, type: string /* , ...params: Array<any> */): void;
|
||||
|
||||
ctcpResponse(target: string, type: string /* , params: Array<any> */): void;
|
||||
|
||||
action(target: string, message: string): string[];
|
||||
|
||||
whowas(target: string, cb: (event: Event) => any): void;
|
||||
|
||||
whois(nick: string, cb: (event: any) => void): void;
|
||||
|
||||
/**
|
||||
* WHO requests are queued up to run serially.
|
||||
* This is mostly because networks will only reply serially and it makes
|
||||
* it easier to include the correct replies to callbacks
|
||||
*/
|
||||
who(target: string, cb: (event: any) => void): void;
|
||||
|
||||
list(/* params: Array<string> */): void;
|
||||
|
||||
channel(channel_name: string): IrcChannel;
|
||||
|
||||
match(
|
||||
match_regex: string,
|
||||
cb: (event: Event) => any,
|
||||
message_type: string
|
||||
): {stop: () => void};
|
||||
|
||||
matchNotice(match_regex: string, cb: (event: Event) => any): void;
|
||||
|
||||
matchMessage(match_regex: string, cb: (event: Event) => any): void;
|
||||
|
||||
matchAction(match_regex: string, cb: (event: Event) => any): void;
|
||||
|
||||
stringToBlocks(str: string, block_size?: number): string[];
|
||||
|
||||
on(eventType: string | symbol, cb: (event: any) => void): this;
|
||||
|
||||
on(eventType: "raw", cb: (event: RawEventArgs) => void): this;
|
||||
|
||||
on(eventType: "join", cb: (event: JoinEventArgs) => void): this;
|
||||
|
||||
on(eventType: "registered", cb: (event: RegisteredEventArgs) => void): this;
|
||||
|
||||
on(eventType: "quit", cb: (event: QuitEventArgs) => void): this;
|
||||
|
||||
on(eventType: "part", cb: (event: QuitEventArgs) => void): this;
|
||||
|
||||
on(eventType: "kick", cb: (event: QuitEventArgs) => void): this;
|
||||
|
||||
on(eventType: "message", cb: (event: MessageEventArgs) => any): this;
|
||||
|
||||
on(eventType: "notice", cb: (event: MessageEventArgs /* TODO */) => any): this;
|
||||
|
||||
on(eventType: "mode", cb: (event: ModeEventArgs) => any): this;
|
||||
|
||||
on(eventType: "socket close", cb: (event: {}) => any): this;
|
||||
|
||||
on(eventType: "socket connected", cb: (event: {}) => any): this;
|
||||
|
||||
on(eventType: "raw socket connected", cb: (event: {}) => any): this;
|
||||
|
||||
on(eventType: "server options", cb: (event: ServerOptionsEventArgs) => any): this;
|
||||
|
||||
on(eventType: "debug", cb: (message: string) => any): this;
|
||||
|
||||
on(eventType: "nick in use", cb: (event: NickInUseEventArgs) => any): this;
|
||||
|
||||
on(eventType: "nick invalid", cb: (event: NickInvalidEventArgs) => any): this;
|
||||
|
||||
on(eventType: "irc error", cb: (event: IrcErrorEventArgs) => any): this;
|
||||
}
|
||||
export class Message {
|
||||
// TODO: What is actually in it and what was in the event?
|
||||
constructor(command?: string, ...args: string[]);
|
||||
|
||||
account?: IrcUser;
|
||||
|
||||
group?: any;
|
||||
|
||||
hostname: string;
|
||||
|
||||
ident: string;
|
||||
|
||||
message: string;
|
||||
|
||||
nick: string;
|
||||
|
||||
reply(e: any): any;
|
||||
|
||||
tags: Object;
|
||||
|
||||
// any
|
||||
time?: any;
|
||||
|
||||
type: string;
|
||||
}
|
||||
export interface MessageEventArgs {
|
||||
account?: any;
|
||||
group?: any;
|
||||
hostname: string;
|
||||
ident: string;
|
||||
message: string;
|
||||
nick: string;
|
||||
reply: (message: string) => void;
|
||||
tags: {[key: string]: string};
|
||||
target: string;
|
||||
time?: any;
|
||||
type: "privmsg" | "action"; // TODO
|
||||
}
|
||||
export interface JoinEventArgs {
|
||||
// todo: is that wrong?
|
||||
account: boolean;
|
||||
channel: string;
|
||||
gecos: string;
|
||||
hostname: string;
|
||||
ident: string;
|
||||
nick: string;
|
||||
time?: any;
|
||||
}
|
||||
export interface KickEventArgs {
|
||||
kicked: string;
|
||||
nick: string;
|
||||
ident: string;
|
||||
hostname: string;
|
||||
channel: string;
|
||||
message: string;
|
||||
time: number;
|
||||
}
|
||||
export interface RawEventArgs {
|
||||
from_server: boolean;
|
||||
line: string;
|
||||
}
|
||||
export interface RegisteredEventArgs {
|
||||
nick: string;
|
||||
}
|
||||
export interface QuitEventArgs {
|
||||
hostname: string;
|
||||
ident: string;
|
||||
message: string;
|
||||
nick: string;
|
||||
time?: any;
|
||||
}
|
||||
interface Mode {
|
||||
mode: string;
|
||||
param: string;
|
||||
}
|
||||
export interface ModeEventArgs {
|
||||
modes: Mode[];
|
||||
nick: string;
|
||||
raw_modes: string;
|
||||
raw_params: string[];
|
||||
target: string;
|
||||
time?: any;
|
||||
}
|
||||
export interface ServerOptionsEventArgs {
|
||||
options: any;
|
||||
cap: any;
|
||||
}
|
||||
export interface NickInvalidEventArgs {
|
||||
nick: string;
|
||||
reason: string;
|
||||
}
|
||||
export interface NickInUseEventArgs {
|
||||
nick: string;
|
||||
reason: string;
|
||||
}
|
||||
export interface IrcErrorEventArgs {
|
||||
error: string;
|
||||
channel: string;
|
||||
reason: string;
|
||||
}
|
||||
// interface IrcUser {
|
||||
// /**The current nick you are currently using.*/
|
||||
// nick: string;
|
||||
// /**Your username (ident) that the network sees you as using.*/
|
||||
// username: string;
|
||||
// /**Your current gecos (realname).*/
|
||||
// gecos: string;
|
||||
// /**On supported servers, the hostname that the networksees you are using.*/
|
||||
// host: string;
|
||||
// /**Your current away status. Empty for not away.*/
|
||||
// away: string;
|
||||
// /**A set() instance with your current message modes.*/
|
||||
// modes: Set<string>;
|
||||
// }
|
||||
// TODO: what to call it? why is it channel.users empty after join?
|
||||
interface IrcUser {
|
||||
hostname: string;
|
||||
ident: string;
|
||||
modes: string[]; // any[]
|
||||
nick: string;
|
||||
username: string;
|
||||
gecos: string;
|
||||
}
|
||||
|
||||
class IrcChannel extends EventEmitter {
|
||||
constructor(irc_client: Client, channel_name: string, key: string);
|
||||
|
||||
irc_client: Client;
|
||||
|
||||
name: string;
|
||||
|
||||
say(message: string): string[];
|
||||
|
||||
notice(message: string): string[];
|
||||
|
||||
join(key?: string): void;
|
||||
|
||||
part(message?: string): void;
|
||||
|
||||
mode(mode: string, extra_args?: string[]): void;
|
||||
|
||||
banlist(cb: (e: Event) => any): void;
|
||||
|
||||
ban(mask: string): void;
|
||||
|
||||
unban(mask: string): void;
|
||||
|
||||
users: IrcUser[];
|
||||
|
||||
/**
|
||||
* Relay messages between this channel to another
|
||||
* @param {IrcChannel|String} target_chan Target channel
|
||||
* @param {Object} opts Extra options
|
||||
*
|
||||
* opts may contain the following properties:
|
||||
* one_way (false) Only relay messages to target_chan, not the reverse
|
||||
* replay_nicks (true) Include the sending nick as part of the relayed message
|
||||
*/
|
||||
relay(target_chan: IrcChannel | String, opts: Object): void;
|
||||
|
||||
// stream(stream_ops: Object): DuplexStream;
|
||||
|
||||
updateUsers(cb: (channel: IrcChannel) => any): void;
|
||||
|
||||
on(eventType: "channel info", cb: (event: ChannelInfoEventArgs) => any): this;
|
||||
|
||||
on(eventType: string | symbol, cb: (event: any) => any): this;
|
||||
}
|
||||
export interface ChannelInfoEventArgs {
|
||||
channel: string;
|
||||
created_at?: number;
|
||||
modes?: Mode[]; // TODO: check type
|
||||
url?: string;
|
||||
}
|
||||
export interface UserListEventArgs {
|
||||
channel: string;
|
||||
users: IrcUser[]; // TODO: check type
|
||||
}
|
||||
export interface WhoListEventArgs {
|
||||
target: string;
|
||||
users: IrcUser[]; // TODO: check type
|
||||
}
|
||||
export interface BanlistEventArgs {
|
||||
channel: string;
|
||||
bans: IrcUser[]; // TODO: check type
|
||||
}
|
||||
export interface TopicEventArgs {
|
||||
channel: string;
|
||||
topic: string;
|
||||
nick?: string;
|
||||
time?: number;
|
||||
}
|
||||
export interface TopicSetByEventArgs {
|
||||
channel: string;
|
||||
nick: string;
|
||||
ident: string;
|
||||
hostname: string;
|
||||
when?: number;
|
||||
}
|
||||
interface ClientConstructorParameters {
|
||||
host?: string;
|
||||
nick?: string;
|
||||
outgoing_addr?: string;
|
||||
username?: string;
|
||||
gecos?: string;
|
||||
encoding?: string;
|
||||
version?: string | boolean;
|
||||
enable_chghost?: boolean;
|
||||
enable_echomessage?: boolean;
|
||||
enable_setname?: boolean;
|
||||
message_max_length?: number;
|
||||
auto_reconnect?: boolean;
|
||||
auto_reconnect_wait?: number;
|
||||
auto_reconnect_max_retries?: number;
|
||||
ping_interval?: number;
|
||||
ping_timeout?: number;
|
||||
transport?: new (options: any) => Connection;
|
||||
ssl?: boolean;
|
||||
webirc?: {
|
||||
password?: string;
|
||||
username?: string;
|
||||
hostname?: string;
|
||||
ip?: string;
|
||||
};
|
||||
}
|
||||
}
|
9
src/types/packages/index.d.ts
vendored
Normal file
9
src/types/packages/index.d.ts
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
/// <reference path="themes.d.ts" />
|
||||
|
||||
type PackageInfo = {
|
||||
packageName: string;
|
||||
thelounge?: {supports: any};
|
||||
version: string;
|
||||
type?: string;
|
||||
files?: string[];
|
||||
};
|
10
src/types/packages/themes.d.ts
vendored
Normal file
10
src/types/packages/themes.d.ts
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
// TODO: move to index.d.ts when more types are added
|
||||
type Module = {
|
||||
type: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
type ThemeModule = Module & {
|
||||
themeColor: string;
|
||||
css: string;
|
||||
};
|
4
src/types/plugins/clientCertificate.d.ts
vendored
Normal file
4
src/types/plugins/clientCertificate.d.ts
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
type ClientCertificate = {
|
||||
private_key: string;
|
||||
certificate: string;
|
||||
};
|
3
src/types/plugins/index.d.ts
vendored
Normal file
3
src/types/plugins/index.d.ts
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/// <reference path="sts.d.ts" />
|
||||
/// <reference path="messageStorage/index.d.ts" />
|
||||
/// <reference path="clientCertificate.d.ts" />
|
26
src/types/plugins/messageStorage/index.d.ts
vendored
Normal file
26
src/types/plugins/messageStorage/index.d.ts
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
import {Channel} from "../../models/channel";
|
||||
import {Message} from "../../models/message";
|
||||
import {Network} from "../../models/network";
|
||||
import sqlite from "sqlite3";
|
||||
import Client from "src/client";
|
||||
|
||||
interface MessageStorage {
|
||||
client: Client;
|
||||
isEnabled: boolean;
|
||||
|
||||
enable(): void;
|
||||
|
||||
close(callback?: () => void): void;
|
||||
|
||||
index(network: Network, channel: Channel, msg: Message): void;
|
||||
|
||||
deleteChannel(network: Network, channel: Channel);
|
||||
|
||||
getMessages(network: Network, channel: Channel): Promise<Message[]>;
|
||||
|
||||
canProvideMessages(): boolean;
|
||||
}
|
||||
|
||||
interface SqliteMessageStorage extends MessageStorage {
|
||||
database: sqlite.Database;
|
||||
}
|
8
src/types/plugins/sts.d.ts
vendored
Normal file
8
src/types/plugins/sts.d.ts
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
type PolicyOption = {
|
||||
port: number;
|
||||
duration: number;
|
||||
expires: number;
|
||||
host: string;
|
||||
};
|
||||
|
||||
export type PolicyMap = Map<string, Omit<PolicyOption, "host">>;
|
3
src/types/server.d.ts
vendored
Normal file
3
src/types/server.d.ts
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
type ServerOptions = {
|
||||
dev: boolean;
|
||||
};
|
|
@ -13,7 +13,7 @@ describe("Commands", function () {
|
|||
|
||||
const lobby = new Chan({
|
||||
name: "Network Lobby",
|
||||
type: Chan.Type.LOBBY,
|
||||
type: ChanType.LOBBY,
|
||||
});
|
||||
|
||||
const testableNetwork = {
|
||||
|
|
|
@ -24,7 +24,7 @@ describe("Network", function () {
|
|||
const network = new Network({
|
||||
name: "Super Nice Network",
|
||||
channels: [
|
||||
new Chan({name: "AAAA!", type: Chan.Type.QUERY}),
|
||||
new Chan({name: "AAAA!", type: ChanType.QUERY}),
|
||||
new Chan({name: "#thelounge"}),
|
||||
new Chan({name: "&foobar"}),
|
||||
],
|
||||
|
@ -32,7 +32,7 @@ describe("Network", function () {
|
|||
network.channels.push(new Chan({name: "#swag"}));
|
||||
|
||||
expect(network.channels[0].name).to.equal("Super Nice Network");
|
||||
expect(network.channels[0].type).to.equal(Chan.Type.LOBBY);
|
||||
expect(network.channels[0].type).to.equal(ChanType.LOBBY);
|
||||
});
|
||||
|
||||
it("should maintain channel reference", function () {
|
||||
|
@ -83,8 +83,8 @@ describe("Network", function () {
|
|||
new Chan({name: "&foobar", key: "", muted: false}),
|
||||
new Chan({name: "#secret", key: "foo", muted: false}),
|
||||
new Chan({name: "&secure", key: "bar", muted: true}),
|
||||
new Chan({name: "Channel List", type: Chan.Type.SPECIAL}),
|
||||
new Chan({name: "PrivateChat", type: Chan.Type.QUERY, muted: true}),
|
||||
new Chan({name: "Channel List", type: ChanType.SPECIAL}),
|
||||
new Chan({name: "PrivateChat", type: ChanType.QUERY, muted: true}),
|
||||
],
|
||||
});
|
||||
network.setNick("chillin`");
|
||||
|
@ -420,7 +420,7 @@ describe("Network", function () {
|
|||
channels: [chan1, chan2],
|
||||
});
|
||||
|
||||
const newUser = new Chan({name: "mcinkay", type: Chan.Type.QUERY});
|
||||
const newUser = new Chan({name: "mcinkay", type: ChanType.QUERY});
|
||||
network.addChannel(newUser);
|
||||
|
||||
expect(network.channels[1]).to.equal(chan1);
|
||||
|
@ -431,14 +431,14 @@ describe("Network", function () {
|
|||
it("should sort users alphabetically", function () {
|
||||
const chan1 = new Chan({name: "#abc"});
|
||||
const chan2 = new Chan({name: "#THELOUNGE"});
|
||||
const user1 = new Chan({name: "astorije", type: Chan.Type.QUERY});
|
||||
const user2 = new Chan({name: "xpaw", type: Chan.Type.QUERY});
|
||||
const user1 = new Chan({name: "astorije", type: ChanType.QUERY});
|
||||
const user2 = new Chan({name: "xpaw", type: ChanType.QUERY});
|
||||
|
||||
const network = new Network({
|
||||
channels: [chan1, chan2, user1, user2],
|
||||
});
|
||||
|
||||
const newUser = new Chan({name: "mcinkay", type: Chan.Type.QUERY});
|
||||
const newUser = new Chan({name: "mcinkay", type: ChanType.QUERY});
|
||||
network.addChannel(newUser);
|
||||
|
||||
expect(network.channels[1]).to.equal(chan1);
|
||||
|
@ -451,14 +451,14 @@ describe("Network", function () {
|
|||
it("should not sort special channels", function () {
|
||||
const chan1 = new Chan({name: "#abc"});
|
||||
const chan2 = new Chan({name: "#THELOUNGE"});
|
||||
const user1 = new Chan({name: "astorije", type: Chan.Type.QUERY});
|
||||
const user2 = new Chan({name: "xpaw", type: Chan.Type.QUERY});
|
||||
const user1 = new Chan({name: "astorije", type: ChanType.QUERY});
|
||||
const user2 = new Chan({name: "xpaw", type: ChanType.QUERY});
|
||||
|
||||
const network = new Network({
|
||||
channels: [chan1, chan2, user1, user2],
|
||||
});
|
||||
|
||||
const newBanlist = new Chan({name: "Banlist for #THELOUNGE", type: Chan.Type.SPECIAL});
|
||||
const newBanlist = new Chan({name: "Banlist for #THELOUNGE", type: ChanType.SPECIAL});
|
||||
network.addChannel(newBanlist);
|
||||
|
||||
expect(network.channels[1]).to.equal(chan1);
|
||||
|
@ -471,15 +471,15 @@ describe("Network", function () {
|
|||
it("should not compare against special channels", function () {
|
||||
const chan1 = new Chan({name: "#abc"});
|
||||
const chan2 = new Chan({name: "#THELOUNGE"});
|
||||
const user1 = new Chan({name: "astorije", type: Chan.Type.QUERY});
|
||||
const user1 = new Chan({name: "astorije", type: ChanType.QUERY});
|
||||
|
||||
const network = new Network({
|
||||
channels: [chan1, chan2, user1],
|
||||
});
|
||||
|
||||
const newBanlist = new Chan({name: "Banlist for #THELOUNGE", type: Chan.Type.SPECIAL});
|
||||
const newBanlist = new Chan({name: "Banlist for #THELOUNGE", type: ChanType.SPECIAL});
|
||||
network.addChannel(newBanlist);
|
||||
const newUser = new Chan({name: "mcinkay", type: Chan.Type.QUERY});
|
||||
const newUser = new Chan({name: "mcinkay", type: ChanType.QUERY});
|
||||
network.addChannel(newUser);
|
||||
|
||||
expect(network.channels[1]).to.equal(chan1);
|
||||
|
@ -490,9 +490,9 @@ describe("Network", function () {
|
|||
});
|
||||
|
||||
it("should insert before first special channel", function () {
|
||||
const banlist = new Chan({name: "Banlist for #THELOUNGE", type: Chan.Type.SPECIAL});
|
||||
const banlist = new Chan({name: "Banlist for #THELOUNGE", type: ChanType.SPECIAL});
|
||||
const chan1 = new Chan({name: "#thelounge"});
|
||||
const user1 = new Chan({name: "astorije", type: Chan.Type.QUERY});
|
||||
const user1 = new Chan({name: "astorije", type: ChanType.QUERY});
|
||||
|
||||
const network = new Network({
|
||||
channels: [banlist, chan1, user1],
|
||||
|
|
14
tsconfig.json
Normal file
14
tsconfig.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "commonjs",
|
||||
"outDir": "dist",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"lib": ["es2019"],
|
||||
"baseUrl": ".",
|
||||
"allowJs": true
|
||||
},
|
||||
"exclude": ["src/node_modules", "public/*", "client/*"],
|
||||
"include": ["src/**/*"]
|
||||
}
|
3
vetur.config.js
Normal file
3
vetur.config.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
projects: ["./client/tsconfig.json"],
|
||||
};
|
|
@ -5,6 +5,7 @@ const path = require("path");
|
|||
const CopyPlugin = require("copy-webpack-plugin");
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
const VueLoaderPlugin = require("vue-loader/lib/plugin");
|
||||
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
|
||||
const Helper = require("./src/helper.js");
|
||||
const babelConfig = require("./babel.config.cjs");
|
||||
|
||||
|
@ -12,7 +13,7 @@ const isProduction = process.env.NODE_ENV === "production";
|
|||
const config = {
|
||||
mode: isProduction ? "production" : "development",
|
||||
entry: {
|
||||
"js/bundle.js": [path.resolve(__dirname, "client/js/vue.js")],
|
||||
"js/bundle.js": [path.resolve(__dirname, "client/js/vue.ts")],
|
||||
},
|
||||
devtool: "source-map",
|
||||
output: {
|
||||
|
@ -28,14 +29,22 @@ const config = {
|
|||
rules: [
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: "vue-loader",
|
||||
},
|
||||
{
|
||||
test: /\.ts$/,
|
||||
use: {
|
||||
loader: "vue-loader",
|
||||
loader: "ts-loader",
|
||||
// options: {
|
||||
// compilerOptions: {
|
||||
// preserveWhitespace: false,
|
||||
// },
|
||||
// },
|
||||
options: {
|
||||
compilerOptions: {
|
||||
preserveWhitespace: false,
|
||||
},
|
||||
appendTsSuffixTo: [/\.vue$/],
|
||||
},
|
||||
},
|
||||
exclude: path.resolve(__dirname, "node_modules"),
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
|
@ -63,11 +72,21 @@ const config = {
|
|||
],
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
test: /\.{js,ts,tsx}$/,
|
||||
include: [path.resolve(__dirname, "client")],
|
||||
use: {
|
||||
loader: "babel-loader",
|
||||
<<<<<<< HEAD
|
||||
options: babelConfig,
|
||||
||||||| parent of f6bd9354 (ts progress)
|
||||
options: {
|
||||
presets: [["@babel/env"]],
|
||||
},
|
||||
=======
|
||||
options: {
|
||||
presets: ["@babel/env", "babel-preset-typescript-vue"],
|
||||
},
|
||||
>>>>>>> f6bd9354 (ts progress)
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -83,11 +102,27 @@ const config = {
|
|||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
vue$: "vue/dist/vue.esm.js",
|
||||
},
|
||||
extensions: [".js", ".vue", ".json", ".ts"],
|
||||
// modules: ["node_modules", path.resolve(__dirname, "client")],
|
||||
plugins: [
|
||||
new TsconfigPathsPlugin({
|
||||
configFile: path.resolve(__dirname, "client/tsconfig.json"),
|
||||
extensions: [".js", ".vue", ".json", ".ts"],
|
||||
baseUrl: path.resolve(__dirname, "client"),
|
||||
}),
|
||||
],
|
||||
},
|
||||
externals: {
|
||||
json3: "JSON", // socket.io uses json3.js, but we do not target any browsers that need it
|
||||
},
|
||||
plugins: [
|
||||
new VueLoaderPlugin(),
|
||||
new VueLoaderPlugin({
|
||||
esModule: true,
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "css/style.css",
|
||||
}),
|
||||
|
|
300
yarn.lock
300
yarn.lock
|
@ -556,6 +556,13 @@
|
|||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.14.5"
|
||||
|
||||
"@babel/plugin-syntax-typescript@^7.16.7":
|
||||
version "7.17.10"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.17.10.tgz#80031e6042cad6a95ed753f672ebd23c30933195"
|
||||
integrity sha512-xJefea1DWXW09pW4Tm9bjwVlPDyYA2it3fWlmEjpYz6alPvTUjL0EOzNzI/FEOyI3r4/J7uVH5UqKgl1TQ5hqQ==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.16.7"
|
||||
|
||||
"@babel/plugin-transform-arrow-functions@^7.16.7":
|
||||
version "7.16.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.16.7.tgz#44125e653d94b98db76369de9c396dc14bef4154"
|
||||
|
@ -791,6 +798,15 @@
|
|||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.16.7"
|
||||
|
||||
"@babel/plugin-transform-typescript@^7.16.7", "@babel/plugin-transform-typescript@^7.3.2":
|
||||
version "7.16.8"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.16.8.tgz#591ce9b6b83504903fa9dd3652c357c2ba7a1ee0"
|
||||
integrity sha512-bHdQ9k7YpBDO2d0NVfkj51DpQcvwIzIusJ7mEUaMlbZq3Kt/U47j24inXZHQ5MDiYpCs+oZiwnXyKedE8+q7AQ==
|
||||
dependencies:
|
||||
"@babel/helper-create-class-features-plugin" "^7.16.7"
|
||||
"@babel/helper-plugin-utils" "^7.16.7"
|
||||
"@babel/plugin-syntax-typescript" "^7.16.7"
|
||||
|
||||
"@babel/plugin-transform-unicode-escapes@^7.16.7":
|
||||
version "7.16.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.16.7.tgz#da8717de7b3287a2c6d659750c964f302b31ece3"
|
||||
|
@ -897,6 +913,15 @@
|
|||
"@babel/types" "^7.4.4"
|
||||
esutils "^2.0.2"
|
||||
|
||||
"@babel/preset-typescript@7.16.7", "@babel/preset-typescript@^7.3.3":
|
||||
version "7.16.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.16.7.tgz#ab114d68bb2020afc069cd51b37ff98a046a70b9"
|
||||
integrity sha512-WbVEmgXdIyvzB77AQjGBEyYPZx+8tTsO50XtfozQrkW8QB2rLJpH2lgx0TRw5EJrBxOZQ+wCcyPVQvS8tjEHpQ==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.16.7"
|
||||
"@babel/helper-validator-option" "^7.16.7"
|
||||
"@babel/plugin-transform-typescript" "^7.16.7"
|
||||
|
||||
"@babel/runtime@^7.8.4":
|
||||
version "7.17.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72"
|
||||
|
@ -961,6 +986,18 @@
|
|||
"@babel/helper-validator-identifier" "^7.16.7"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@cspotcode/source-map-consumer@0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b"
|
||||
integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==
|
||||
|
||||
"@cspotcode/source-map-support@0.7.0":
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5"
|
||||
integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==
|
||||
dependencies:
|
||||
"@cspotcode/source-map-consumer" "0.8.0"
|
||||
|
||||
"@csstools/postcss-font-format-keywords@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.0.tgz#7e7df948a83a0dfb7eb150a96e2390ac642356a1"
|
||||
|
@ -1232,6 +1269,34 @@
|
|||
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
|
||||
integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
|
||||
|
||||
"@tsconfig/node10@^1.0.7":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9"
|
||||
integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==
|
||||
|
||||
"@tsconfig/node12@^1.0.7":
|
||||
version "1.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c"
|
||||
integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==
|
||||
|
||||
"@tsconfig/node14@^1.0.0":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2"
|
||||
integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==
|
||||
|
||||
"@tsconfig/node16@^1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e"
|
||||
integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==
|
||||
|
||||
"@types/body-parser@*":
|
||||
version "1.19.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0"
|
||||
integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==
|
||||
dependencies:
|
||||
"@types/connect" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/cacheable-request@^6.0.1":
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.2.tgz#c324da0197de0a98a2312156536ae262429ff6b9"
|
||||
|
@ -1254,6 +1319,13 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/component-emitter/-/component-emitter-1.2.11.tgz#50d47d42b347253817a39709fef03ce66a108506"
|
||||
integrity sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ==
|
||||
|
||||
"@types/connect@*":
|
||||
version "3.4.35"
|
||||
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1"
|
||||
integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/cookie@^0.4.1":
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d"
|
||||
|
@ -1290,6 +1362,25 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83"
|
||||
integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==
|
||||
|
||||
"@types/express-serve-static-core@^4.17.18":
|
||||
version "4.17.28"
|
||||
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz#c47def9f34ec81dc6328d0b1b5303d1ec98d86b8"
|
||||
integrity sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
"@types/qs" "*"
|
||||
"@types/range-parser" "*"
|
||||
|
||||
"@types/express@4.17.13":
|
||||
version "4.17.13"
|
||||
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034"
|
||||
integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==
|
||||
dependencies:
|
||||
"@types/body-parser" "*"
|
||||
"@types/express-serve-static-core" "^4.17.18"
|
||||
"@types/qs" "*"
|
||||
"@types/serve-static" "*"
|
||||
|
||||
"@types/http-cache-semantics@*":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812"
|
||||
|
@ -1305,6 +1396,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
|
||||
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
|
||||
|
||||
"@types/json5@^0.0.29":
|
||||
version "0.0.29"
|
||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
|
||||
|
||||
"@types/keyv@*":
|
||||
version "3.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.4.tgz#3ccdb1c6751b0c7e52300bcdacd5bcbf8faa75b6"
|
||||
|
@ -1312,6 +1408,23 @@
|
|||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/ldapjs@2.2.2":
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/ldapjs/-/ldapjs-2.2.2.tgz#cf79510d8dc34e5579442c2743f8a228427eb99c"
|
||||
integrity sha512-U5HdnwIZ5uZa+f3usxdqgyfNmOROxOxXvQdQtsu6sKo8fte5vej9br2csHxPvXreAbAO1bs8/rdEzvCLpi67nQ==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/lodash@4.14.182":
|
||||
version "4.14.182"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2"
|
||||
integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==
|
||||
|
||||
"@types/mime@^1":
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
|
||||
integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
|
||||
|
||||
"@types/minimatch@^3.0.3":
|
||||
version "3.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40"
|
||||
|
@ -1322,6 +1435,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c"
|
||||
integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==
|
||||
|
||||
"@types/mousetrap@1.6.9":
|
||||
version "1.6.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/mousetrap/-/mousetrap-1.6.9.tgz#f1ef9adbd1eac3466f21b6988b1c82c633a45340"
|
||||
integrity sha512-HUAiN65VsRXyFCTicolwb5+I7FM6f72zjMWr+ajGk+YTvzBgXqa2A5U7d+rtsouAkunJ5U4Sb5lNJjo9w+nmXg==
|
||||
|
||||
"@types/node@*", "@types/node@>=10.0.0":
|
||||
version "17.0.23"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.23.tgz#3b41a6e643589ac6442bdbd7a4a3ded62f33f7da"
|
||||
|
@ -1337,6 +1455,16 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
|
||||
|
||||
"@types/qs@*":
|
||||
version "6.9.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
|
||||
integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==
|
||||
|
||||
"@types/range-parser@*":
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
|
||||
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
||||
|
||||
"@types/responselike@*", "@types/responselike@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29"
|
||||
|
@ -1344,6 +1472,38 @@
|
|||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/serve-static@*":
|
||||
version "1.13.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9"
|
||||
integrity sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==
|
||||
dependencies:
|
||||
"@types/mime" "^1"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/sqlite3@3.1.8":
|
||||
version "3.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/sqlite3/-/sqlite3-3.1.8.tgz#e64310c5841fc01c1a8795d960d951e4cf940296"
|
||||
integrity sha512-sQMt/qnyUWnqiTcJXm5ZfNPIBeJ/DVvJDwxw+0tAxPJvadzfiP1QhryO1JOR6t1yfb8NpzQb/Rud06mob5laIA==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/ua-parser-js@0.7.36":
|
||||
version "0.7.36"
|
||||
resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.36.tgz#9bd0b47f26b5a3151be21ba4ce9f5fa457c5f190"
|
||||
integrity sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ==
|
||||
|
||||
"@types/uuid@8.3.4":
|
||||
version "8.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
|
||||
integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
|
||||
|
||||
"@types/ws@8.5.3":
|
||||
version "8.5.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d"
|
||||
integrity sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@ungap/promise-all-settled@1.1.2":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44"
|
||||
|
@ -1365,6 +1525,30 @@
|
|||
optionalDependencies:
|
||||
prettier "^1.18.2 || ^2.0.0"
|
||||
|
||||
"@vue/reactivity@3.2.33":
|
||||
version "3.2.33"
|
||||
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.33.tgz#c84eedb5225138dbfc2472864c151d3efbb4b673"
|
||||
integrity sha512-62Sq0mp9/0bLmDuxuLD5CIaMG2susFAGARLuZ/5jkU1FCf9EDbwUuF+BO8Ub3Rbodx0ziIecM/NsmyjardBxfQ==
|
||||
dependencies:
|
||||
"@vue/shared" "3.2.33"
|
||||
|
||||
"@vue/runtime-core@3.2.33":
|
||||
version "3.2.33"
|
||||
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.33.tgz#2df8907c85c37c3419fbd1bdf1a2df097fa40df2"
|
||||
integrity sha512-N2D2vfaXsBPhzCV3JsXQa2NECjxP3eXgZlFqKh4tgakp3iX6LCGv76DLlc+IfFZq+TW10Y8QUfeihXOupJ1dGw==
|
||||
dependencies:
|
||||
"@vue/reactivity" "3.2.33"
|
||||
"@vue/shared" "3.2.33"
|
||||
|
||||
"@vue/runtime-dom@3.2.33":
|
||||
version "3.2.33"
|
||||
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.33.tgz#123b8969247029ea0d9c1983676d4706a962d848"
|
||||
integrity sha512-LSrJ6W7CZTSUygX5s8aFkraDWlO6K4geOwA3quFF2O+hC3QuAMZt/0Xb7JKE3C4JD4pFwCSO7oCrZmZ0BIJUnw==
|
||||
dependencies:
|
||||
"@vue/runtime-core" "3.2.33"
|
||||
"@vue/shared" "3.2.33"
|
||||
csstype "^2.6.8"
|
||||
|
||||
"@vue/server-test-utils@1.3.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@vue/server-test-utils/-/server-test-utils-1.3.0.tgz#56c8f41cbb4ed9af38a5668cc23f861fcbbcd44b"
|
||||
|
@ -1373,6 +1557,11 @@
|
|||
"@types/cheerio" "^0.22.10"
|
||||
cheerio "^1.0.0-rc.2"
|
||||
|
||||
"@vue/shared@3.2.33":
|
||||
version "3.2.33"
|
||||
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.33.tgz#69a8c99ceb37c1b031d5cc4aec2ff1dc77e1161e"
|
||||
integrity sha512-UBc1Pg1T3yZ97vsA2ueER0F6GbJebLHYlEi4ou1H5YL4KWvMOOWwpYo9/QpWq93wxKG6Wo13IY74Hcn/f7c7Bg==
|
||||
|
||||
"@vue/test-utils@1.3.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-1.3.0.tgz#d563decdcd9c68a7bca151d4179a2bfd6d5c3e15"
|
||||
|
@ -1558,6 +1747,11 @@ acorn-jsx@^5.3.1:
|
|||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
||||
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
|
||||
|
||||
acorn-walk@^8.1.1:
|
||||
version "8.2.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
|
||||
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
|
||||
|
||||
acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.0:
|
||||
version "8.7.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf"
|
||||
|
@ -1706,6 +1900,11 @@ are-we-there-yet@^3.0.0:
|
|||
delegates "^1.0.0"
|
||||
readable-stream "^3.6.0"
|
||||
|
||||
arg@^4.1.0:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
|
||||
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
|
||||
|
||||
argparse@^1.0.7:
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
|
||||
|
@ -1849,6 +2048,16 @@ babel-plugin-polyfill-regenerator@^0.3.0:
|
|||
dependencies:
|
||||
"@babel/helper-define-polyfill-provider" "^0.3.1"
|
||||
|
||||
babel-preset-typescript-vue@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/babel-preset-typescript-vue/-/babel-preset-typescript-vue-1.1.1.tgz#6a617dcb0ee26f911735d5f2bbe530286b2c7c02"
|
||||
integrity sha512-wXeR7Y4xCsRUEdm4t4qlpv4wnxolS6jU0c7P2E6zJRWeG1sR0e6NL7DRN0tNuUwkUt0PU8bqVo4vzoA2VEuxnw==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.0.0"
|
||||
"@babel/plugin-transform-typescript" "^7.3.2"
|
||||
"@babel/preset-typescript" "^7.3.3"
|
||||
vue-template-compiler "^2.6.11"
|
||||
|
||||
backo2@~1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
|
||||
|
@ -2447,6 +2656,11 @@ cosmiconfig@^7.0.0, cosmiconfig@^7.0.1:
|
|||
path-type "^4.0.0"
|
||||
yaml "^1.10.0"
|
||||
|
||||
create-require@^1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
|
||||
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
|
||||
|
||||
cross-spawn@^6.0.5:
|
||||
version "6.0.5"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
|
||||
|
@ -2595,6 +2809,11 @@ csso@^4.2.0:
|
|||
dependencies:
|
||||
css-tree "^1.1.2"
|
||||
|
||||
csstype@^2.6.8:
|
||||
version "2.6.20"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.20.tgz#9229c65ea0b260cf4d3d997cb06288e36a8d6dda"
|
||||
integrity sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA==
|
||||
|
||||
dayjs@1.10.8:
|
||||
version "1.10.8"
|
||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.8.tgz#267df4bc6276fcb33c04a6735287e3f429abec41"
|
||||
|
@ -2712,6 +2931,11 @@ diff@5.0.0, diff@^5.0.0:
|
|||
resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b"
|
||||
integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==
|
||||
|
||||
diff@^4.0.1:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
||||
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
||||
|
||||
dir-glob@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
|
||||
|
@ -2865,7 +3089,7 @@ engine.io@~6.1.0:
|
|||
engine.io-parser "~5.0.3"
|
||||
ws "~8.2.3"
|
||||
|
||||
enhanced-resolve@^5.8.3:
|
||||
enhanced-resolve@^5.0.0, enhanced-resolve@^5.7.0, enhanced-resolve@^5.8.3:
|
||||
version "5.9.3"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz#44a342c012cbc473254af5cc6ae20ebd0aae5d88"
|
||||
integrity sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==
|
||||
|
@ -4576,6 +4800,11 @@ make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0:
|
|||
dependencies:
|
||||
semver "^6.0.0"
|
||||
|
||||
make-error@^1.1.1:
|
||||
version "1.3.6"
|
||||
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
|
||||
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
|
||||
|
||||
make-fetch-happen@^9.1.0:
|
||||
version "9.1.0"
|
||||
resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz#53085a09e7971433e6765f7971bf63f4e05cb968"
|
||||
|
@ -4680,7 +4909,7 @@ methods@~1.1.2:
|
|||
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
|
||||
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
|
||||
|
||||
micromatch@^4.0.4:
|
||||
micromatch@^4.0.0, micromatch@^4.0.4:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
|
||||
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
|
||||
|
@ -4777,7 +5006,7 @@ minimist-options@4.1.0:
|
|||
is-plain-obj "^1.1.0"
|
||||
kind-of "^6.0.3"
|
||||
|
||||
minimist@^1.2.0, minimist@^1.2.5:
|
||||
minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6:
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||
|
@ -7123,6 +7352,54 @@ trim-repeated@^1.0.0:
|
|||
dependencies:
|
||||
escape-string-regexp "^1.0.2"
|
||||
|
||||
ts-loader@9.3.0:
|
||||
version "9.3.0"
|
||||
resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.3.0.tgz#980f4dbfb60e517179e15e10ed98e454b132159f"
|
||||
integrity sha512-2kLLAdAD+FCKijvGKi9sS0OzoqxLCF3CxHpok7rVgCZ5UldRzH0TkbwG9XECKjBzHsAewntC5oDaI/FwKzEUog==
|
||||
dependencies:
|
||||
chalk "^4.1.0"
|
||||
enhanced-resolve "^5.0.0"
|
||||
micromatch "^4.0.0"
|
||||
semver "^7.3.4"
|
||||
|
||||
ts-node@10.7.0:
|
||||
version "10.7.0"
|
||||
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.7.0.tgz#35d503d0fab3e2baa672a0e94f4b40653c2463f5"
|
||||
integrity sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==
|
||||
dependencies:
|
||||
"@cspotcode/source-map-support" "0.7.0"
|
||||
"@tsconfig/node10" "^1.0.7"
|
||||
"@tsconfig/node12" "^1.0.7"
|
||||
"@tsconfig/node14" "^1.0.0"
|
||||
"@tsconfig/node16" "^1.0.2"
|
||||
acorn "^8.4.1"
|
||||
acorn-walk "^8.1.1"
|
||||
arg "^4.1.0"
|
||||
create-require "^1.1.0"
|
||||
diff "^4.0.1"
|
||||
make-error "^1.1.1"
|
||||
v8-compile-cache-lib "^3.0.0"
|
||||
yn "3.1.1"
|
||||
|
||||
tsconfig-paths-webpack-plugin@3.5.2:
|
||||
version "3.5.2"
|
||||
resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-3.5.2.tgz#01aafff59130c04a8c4ebc96a3045c43c376449a"
|
||||
integrity sha512-EhnfjHbzm5IYI9YPNVIxx1moxMI4bpHD2e0zTXeDNQcwjjRaGepP7IhTHJkyDBG0CAOoxRfe7jCG630Ou+C6Pw==
|
||||
dependencies:
|
||||
chalk "^4.1.0"
|
||||
enhanced-resolve "^5.7.0"
|
||||
tsconfig-paths "^3.9.0"
|
||||
|
||||
tsconfig-paths@^3.9.0:
|
||||
version "3.14.1"
|
||||
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a"
|
||||
integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==
|
||||
dependencies:
|
||||
"@types/json5" "^0.0.29"
|
||||
json5 "^1.0.1"
|
||||
minimist "^1.2.6"
|
||||
strip-bom "^3.0.0"
|
||||
|
||||
tslib@^2.2.0:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
|
||||
|
@ -7175,6 +7452,11 @@ typedarray-to-buffer@^3.1.5:
|
|||
dependencies:
|
||||
is-typedarray "^1.0.0"
|
||||
|
||||
typescript@4.6.4:
|
||||
version "4.6.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9"
|
||||
integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==
|
||||
|
||||
ua-parser-js@1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.2.tgz#e2976c34dbfb30b15d2c300b2a53eac87c57a775"
|
||||
|
@ -7286,6 +7568,11 @@ uuid@^3.3.3:
|
|||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
||||
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
||||
|
||||
v8-compile-cache-lib@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
|
||||
integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
|
||||
|
||||
v8-compile-cache@^2.0.3, v8-compile-cache@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
|
||||
|
@ -7385,7 +7672,7 @@ vue-style-loader@^4.1.0:
|
|||
hash-sum "^1.0.2"
|
||||
loader-utils "^1.0.2"
|
||||
|
||||
vue-template-compiler@2.6.14:
|
||||
vue-template-compiler@2.6.14, vue-template-compiler@^2.6.11:
|
||||
version "2.6.14"
|
||||
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.14.tgz#a2f0e7d985670d42c9c9ee0d044fed7690f4f763"
|
||||
integrity sha512-ODQS1SyMbjKoO1JBJZojSw6FE4qnh9rIpUZn2EUT86FKizx9uH5z6uXiIrm4/Nb/gwxTi/o17ZDEGWAXHvtC7g==
|
||||
|
@ -7752,6 +8039,11 @@ yeast@0.1.2:
|
|||
resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
|
||||
integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk=
|
||||
|
||||
yn@3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
|
||||
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
|
||||
|
||||
yocto-queue@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||
|
|
Loading…
Reference in a new issue