mirror of
https://github.com/thelounge/thelounge
synced 2024-11-24 21:13:06 +00:00
untangle client and server
Our project was quite confused as to the boundaries between client and server code. This false sharing meant that it was quite hard to tell what was actually sent to the client and what was uniquely scoped to either side. Further, this meant that our compilation and build pipelines were very confused and pulled in files they should not have. This commit series tries to untangle the two. This also entails fixing quite some typing issues. It's hard to make this in sane, small, commits that still build at each step (it's impossible, as fixing one type error / any type immediately lead to further errors in a game of whack a mole). So you'll get my actual progress in small commits that can each be reviewed, however the earlier ones are in fact sometimes wrong and get cleaned up later once the picture is a bit clearer.
This commit is contained in:
commit
f7926267d9
101 changed files with 1370 additions and 1128 deletions
|
@ -59,7 +59,7 @@
|
|||
<script lang="ts">
|
||||
import {filter as fuzzyFilter} from "fuzzy";
|
||||
import {computed, defineComponent, nextTick, PropType, ref} from "vue";
|
||||
import type {UserInMessage} from "../../server/models/msg";
|
||||
import type {UserInMessage} from "../../shared/types/msg";
|
||||
import type {ClientChan, ClientUser} from "../js/types";
|
||||
import Username from "./Username.vue";
|
||||
|
||||
|
@ -104,7 +104,7 @@ export default defineComponent({
|
|||
const result = filteredUsers.value;
|
||||
|
||||
for (const user of result) {
|
||||
const mode = user.original.modes[0] || "";
|
||||
const mode: string = user.original.modes[0] || "";
|
||||
|
||||
if (!groups[mode]) {
|
||||
groups[mode] = [];
|
||||
|
|
|
@ -41,9 +41,9 @@
|
|||
<script lang="ts">
|
||||
import Mousetrap from "mousetrap";
|
||||
import {computed, defineComponent, ref, watch} from "vue";
|
||||
import {onBeforeRouteLeave, onBeforeRouteUpdate} from "vue-router";
|
||||
import eventbus from "../js/eventbus";
|
||||
import {ClientChan, ClientMessage, ClientLinkPreview} from "../js/types";
|
||||
import {ClientChan, ClientLinkPreview} from "../js/types";
|
||||
import {SharedMsg} from "../../shared/types/msg";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ImageViewer",
|
||||
|
@ -104,9 +104,9 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
const links = channel.value.messages
|
||||
.map((msg) => msg.previews)
|
||||
.map((msg: SharedMsg) => msg.previews)
|
||||
.flat()
|
||||
.filter((preview) => preview.thumb);
|
||||
.filter((preview) => preview && preview.thumb);
|
||||
|
||||
const currentIndex = links.indexOf(link.value);
|
||||
|
||||
|
|
|
@ -150,10 +150,14 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
const messageComponent = computed(() => {
|
||||
return "message-" + props.message.type;
|
||||
return "message-" + (props.message.type || "invalid"); // TODO: force existence of type in sharedmsg
|
||||
});
|
||||
|
||||
const isAction = () => {
|
||||
if (!props.message.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return typeof MessageTypes["message-" + props.message.type] !== "undefined";
|
||||
};
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@ import {
|
|||
} from "vue";
|
||||
import {useStore} from "../js/store";
|
||||
import {ClientChan, ClientMessage, ClientNetwork, ClientLinkPreview} from "../js/types";
|
||||
import Msg from "../../server/models/msg";
|
||||
import {SharedMsg} from "../../shared/types/msg";
|
||||
|
||||
type CondensedMessageContainer = {
|
||||
type: "condensed";
|
||||
|
@ -242,7 +242,7 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
const shouldDisplayDateMarker = (
|
||||
message: Msg | ClientMessage | CondensedMessageContainer,
|
||||
message: SharedMsg | ClientMessage | CondensedMessageContainer,
|
||||
id: number
|
||||
) => {
|
||||
const previousMessage = condensedMessages.value[id - 1];
|
||||
|
@ -270,7 +270,7 @@ export default defineComponent({
|
|||
return false;
|
||||
};
|
||||
|
||||
const isPreviousSource = (currentMessage: ClientMessage | Msg, id: number) => {
|
||||
const isPreviousSource = (currentMessage: ClientMessage | SharedMsg, id: number) => {
|
||||
const previousMessage = condensedMessages.value[id - 1];
|
||||
return !!(
|
||||
previousMessage &&
|
||||
|
|
|
@ -26,36 +26,43 @@ export default defineComponent({
|
|||
},
|
||||
setup(props) {
|
||||
const errorMessage = computed(() => {
|
||||
// TODO: enforce chan and nick fields so that we can get rid of that
|
||||
const chan = props.message.channel || "!UNKNOWN_CHAN";
|
||||
const nick = props.message.nick || "!UNKNOWN_NICK";
|
||||
|
||||
switch (props.message.error) {
|
||||
case "bad_channel_key":
|
||||
return `Cannot join ${props.message.channel} - Bad channel key.`;
|
||||
return `Cannot join ${chan} - Bad channel key.`;
|
||||
case "banned_from_channel":
|
||||
return `Cannot join ${props.message.channel} - You have been banned from the channel.`;
|
||||
return `Cannot join ${chan} - You have been banned from the channel.`;
|
||||
case "cannot_send_to_channel":
|
||||
return `Cannot send to channel ${props.message.channel}`;
|
||||
return `Cannot send to channel ${chan}`;
|
||||
case "channel_is_full":
|
||||
return `Cannot join ${props.message.channel} - Channel is full.`;
|
||||
return `Cannot join ${chan} - Channel is full.`;
|
||||
case "chanop_privs_needed":
|
||||
return "Cannot perform action: You're not a channel operator.";
|
||||
case "invite_only_channel":
|
||||
return `Cannot join ${props.message.channel} - Channel is invite only.`;
|
||||
return `Cannot join ${chan} - Channel is invite only.`;
|
||||
case "no_such_nick":
|
||||
return `User ${props.message.nick} hasn't logged in or does not exist.`;
|
||||
return `User ${nick} hasn't logged in or does not exist.`;
|
||||
case "not_on_channel":
|
||||
return "Cannot perform action: You're not on the channel.";
|
||||
case "password_mismatch":
|
||||
return "Password mismatch.";
|
||||
case "too_many_channels":
|
||||
return `Cannot join ${props.message.channel} - You've already reached the maximum number of channels allowed.`;
|
||||
return `Cannot join ${chan} - You've already reached the maximum number of channels allowed.`;
|
||||
case "unknown_command":
|
||||
return `Unknown command: ${props.message.command}`;
|
||||
// TODO: not having message.command should never happen, so force existence
|
||||
return `Unknown command: ${props.message.command || "!UNDEFINED_COMMAND_BUG"}`;
|
||||
case "user_not_in_channel":
|
||||
return `User ${props.message.nick} is not on the channel.`;
|
||||
return `User ${nick} is not on the channel.`;
|
||||
case "user_on_channel":
|
||||
return `User ${props.message.nick} is already on the channel.`;
|
||||
return `User ${nick} is already on the channel.`;
|
||||
default:
|
||||
if (props.message.reason) {
|
||||
return `${props.message.reason} (${props.message.error})`;
|
||||
return `${props.message.reason} (${
|
||||
props.message.error || "!UNDEFINED_ERR"
|
||||
})`;
|
||||
}
|
||||
|
||||
return props.message.error;
|
||||
|
|
|
@ -498,6 +498,7 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
watch(
|
||||
// eslint-disable-next-line
|
||||
() => props.defaults?.commands,
|
||||
() => {
|
||||
void nextTick(() => {
|
||||
|
@ -507,6 +508,7 @@ export default defineComponent({
|
|||
);
|
||||
|
||||
watch(
|
||||
// eslint-disable-next-line
|
||||
() => props.defaults?.tls,
|
||||
(isSecureChecked) => {
|
||||
const ports = [6667, 6697];
|
||||
|
|
|
@ -309,8 +309,7 @@ export default defineComponent({
|
|||
|
||||
moveItemInArray(store.state.networks, oldIndex, newIndex);
|
||||
|
||||
socket.emit("sort", {
|
||||
type: "networks",
|
||||
socket.emit("sort:networks", {
|
||||
order: store.state.networks.map((n) => n.uuid),
|
||||
});
|
||||
};
|
||||
|
@ -341,9 +340,8 @@ export default defineComponent({
|
|||
|
||||
moveItemInArray(netChan.network.channels, oldIndex, newIndex);
|
||||
|
||||
socket.emit("sort", {
|
||||
type: "channels",
|
||||
target: netChan.network.uuid,
|
||||
socket.emit("sort:channel", {
|
||||
network: netChan.network.uuid,
|
||||
order: netChan.network.channels.map((c) => c.id),
|
||||
});
|
||||
};
|
||||
|
|
|
@ -12,10 +12,10 @@
|
|||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, PropType} from "vue";
|
||||
import {UserInMessage} from "../../server/models/msg";
|
||||
import {UserInMessage} from "../../shared/types/msg";
|
||||
import eventbus from "../js/eventbus";
|
||||
import colorClass from "../js/helpers/colorClass";
|
||||
import type {ClientChan, ClientNetwork, ClientUser} from "../js/types";
|
||||
import type {ClientChan, ClientNetwork} from "../js/types";
|
||||
import {useStore} from "../js/store";
|
||||
|
||||
type UsernameUser = Partial<UserInMessage> & {
|
||||
|
|
|
@ -106,7 +106,7 @@ import type {ClientMessage} from "../../js/types";
|
|||
import {useStore} from "../../js/store";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import {switchToChannel} from "../../js/router";
|
||||
import {SearchQuery} from "../../../server/plugins/messageStorage/types";
|
||||
import {SearchQuery} from "../../../shared/types/storage";
|
||||
|
||||
export default defineComponent({
|
||||
name: "SearchResults",
|
||||
|
|
35
client/js/chan.ts
Normal file
35
client/js/chan.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import {ClientChan, ClientMessage} from "./types";
|
||||
import {SharedNetworkChan} from "../../shared/types/network";
|
||||
import {SharedMsg} from "../../shared/types/msg";
|
||||
|
||||
export function toClientChan(shared: SharedNetworkChan): ClientChan {
|
||||
const history: string[] = [""].concat(
|
||||
shared.messages
|
||||
.filter((m) => m.self && m.text && m.type === "message")
|
||||
// TS is too stupid to see the nil guard on filter... so we monkey patch it
|
||||
.map((m): string => (m.text ? m.text : ""))
|
||||
.reverse()
|
||||
.slice(0, 99)
|
||||
);
|
||||
// filter the unused vars
|
||||
const {messages, totalMessages: _, ...props} = shared;
|
||||
const channel: ClientChan = {
|
||||
...props,
|
||||
editTopic: false,
|
||||
pendingMessage: "",
|
||||
inputHistoryPosition: 0,
|
||||
historyLoading: false,
|
||||
scrolledToBottom: true,
|
||||
users: [],
|
||||
usersOutdated: shared.type === "channel" ? true : false,
|
||||
moreHistoryAvailable: shared.totalMessages > shared.messages.length,
|
||||
inputHistory: history,
|
||||
messages: sharedMsgToClientMsg(messages),
|
||||
};
|
||||
return channel;
|
||||
}
|
||||
|
||||
function sharedMsgToClientMsg(shared: SharedMsg[]): ClientMessage[] {
|
||||
// TODO: this is a stub for now, we will want to populate client specific stuff here
|
||||
return shared;
|
||||
}
|
|
@ -11,7 +11,7 @@ function input() {
|
|||
for (const message of store.state.activeChannel.channel.messages) {
|
||||
let toggled = false;
|
||||
|
||||
for (const preview of message.previews) {
|
||||
for (const preview of message.previews || []) {
|
||||
if (preview.shown) {
|
||||
preview.shown = false;
|
||||
toggled = true;
|
||||
|
|
|
@ -11,7 +11,7 @@ function input() {
|
|||
for (const message of store.state.activeChannel.channel.messages) {
|
||||
let toggled = false;
|
||||
|
||||
for (const preview of message.previews) {
|
||||
for (const preview of message.previews || []) {
|
||||
if (!preview.shown) {
|
||||
preview.shown = true;
|
||||
toggled = true;
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import {nextTick} from "vue";
|
||||
import socket from "../socket";
|
||||
import storage from "../localStorage";
|
||||
import {toClientChan} from "../chan";
|
||||
import {router, switchToChannel, navigate} from "../router";
|
||||
import {store} from "../store";
|
||||
import parseIrcUri from "../helpers/parseIrcUri";
|
||||
import {ClientNetwork, InitClientChan} from "../types";
|
||||
import {ClientNetwork, ClientChan} from "../types";
|
||||
import {SharedNetwork, SharedNetworkChan} from "../../../shared/types/network";
|
||||
|
||||
socket.on("init", async function (data) {
|
||||
store.commit("networks", mergeNetworkData(data.networks));
|
||||
|
@ -31,54 +32,54 @@ socket.on("init", async function (data) {
|
|||
window.g_TheLoungeRemoveLoading();
|
||||
}
|
||||
|
||||
const handledQuery = await handleQueryParams();
|
||||
if (await handleQueryParams()) {
|
||||
// If we handled query parameters like irc:// links or just general
|
||||
// connect parameters in public mode, then nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
// If we handled query parameters like irc:// links or just general
|
||||
// connect parameters in public mode, then nothing to do here
|
||||
if (!handledQuery) {
|
||||
// If we are on an unknown route or still on SignIn component
|
||||
// then we can open last known channel on server, or Connect window if none
|
||||
if (
|
||||
!router.currentRoute?.value?.name ||
|
||||
router.currentRoute?.value?.name === "SignIn"
|
||||
) {
|
||||
const channel = store.getters.findChannel(data.active);
|
||||
// If we are on an unknown route or still on SignIn component
|
||||
// then we can open last known channel on server, or Connect window if none
|
||||
if (!router.currentRoute?.value?.name || router.currentRoute?.value?.name === "SignIn") {
|
||||
const channel = store.getters.findChannel(data.active);
|
||||
|
||||
if (channel) {
|
||||
switchToChannel(channel.channel);
|
||||
} else if (store.state.networks.length > 0) {
|
||||
// Server is telling us to open a channel that does not exist
|
||||
// For example, it can be unset if you first open the page after server start
|
||||
switchToChannel(store.state.networks[0].channels[0]);
|
||||
} else {
|
||||
await navigate("Connect");
|
||||
}
|
||||
if (channel) {
|
||||
switchToChannel(channel.channel);
|
||||
} else if (store.state.networks.length > 0) {
|
||||
// Server is telling us to open a channel that does not exist
|
||||
// For example, it can be unset if you first open the page after server start
|
||||
switchToChannel(store.state.networks[0].channels[0]);
|
||||
} else {
|
||||
await navigate("Connect");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function mergeNetworkData(newNetworks: ClientNetwork[]) {
|
||||
function mergeNetworkData(newNetworks: SharedNetwork[]): ClientNetwork[] {
|
||||
const stored = storage.get("thelounge.networks.collapsed");
|
||||
const collapsedNetworks = stored ? new Set(JSON.parse(stored)) : new Set();
|
||||
const result: ReturnType<typeof mergeNetworkData> = [];
|
||||
|
||||
for (let n = 0; n < newNetworks.length; n++) {
|
||||
const network = newNetworks[n];
|
||||
const currentNetwork = store.getters.findNetwork(network.uuid);
|
||||
for (const sharedNet of newNetworks) {
|
||||
const currentNetwork = store.getters.findNetwork(sharedNet.uuid);
|
||||
|
||||
// If this network is new, set some default variables and initalize channel variables
|
||||
if (!currentNetwork) {
|
||||
network.isJoinChannelShown = false;
|
||||
network.isCollapsed = collapsedNetworks.has(network.uuid);
|
||||
network.channels.forEach(store.getters.initChannel);
|
||||
|
||||
const newNet: ClientNetwork = {
|
||||
...sharedNet,
|
||||
channels: sharedNet.channels.map(toClientChan),
|
||||
isJoinChannelShown: false,
|
||||
isCollapsed: collapsedNetworks.has(sharedNet.uuid),
|
||||
};
|
||||
result.push(newNet);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Merge received network object into existing network object on the client
|
||||
// so the object reference stays the same (e.g. for currentChannel state)
|
||||
for (const key in network) {
|
||||
if (!Object.prototype.hasOwnProperty.call(network, key)) {
|
||||
for (const key in sharedNet) {
|
||||
if (!Object.prototype.hasOwnProperty.call(sharedNet, key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -86,81 +87,82 @@ function mergeNetworkData(newNetworks: ClientNetwork[]) {
|
|||
if (key === "channels") {
|
||||
currentNetwork.channels = mergeChannelData(
|
||||
currentNetwork.channels,
|
||||
network.channels as InitClientChan[]
|
||||
sharedNet.channels
|
||||
);
|
||||
} else {
|
||||
currentNetwork[key] = network[key];
|
||||
currentNetwork[key] = sharedNet[key];
|
||||
}
|
||||
}
|
||||
|
||||
newNetworks[n] = currentNetwork;
|
||||
result.push(currentNetwork);
|
||||
}
|
||||
|
||||
return newNetworks;
|
||||
return result;
|
||||
}
|
||||
|
||||
function mergeChannelData(oldChannels: InitClientChan[], newChannels: InitClientChan[]) {
|
||||
for (let c = 0; c < newChannels.length; c++) {
|
||||
const channel = newChannels[c];
|
||||
const currentChannel = oldChannels.find((chan) => chan.id === channel.id);
|
||||
function mergeChannelData(
|
||||
oldChannels: ClientChan[],
|
||||
newChannels: SharedNetworkChan[]
|
||||
): ClientChan[] {
|
||||
const result: ReturnType<typeof mergeChannelData> = [];
|
||||
|
||||
for (const newChannel of newChannels) {
|
||||
const currentChannel = oldChannels.find((chan) => chan.id === newChannel.id);
|
||||
|
||||
// This is a new channel that was joined while client was disconnected, initialize it
|
||||
if (!currentChannel) {
|
||||
store.getters.initChannel(channel);
|
||||
|
||||
// This is a new channel that was joined while client was disconnected, initialize it
|
||||
const current = toClientChan(newChannel);
|
||||
result.push(current);
|
||||
emitNamesOrMarkUsersOudated(current); // TODO: this should not carry logic like that
|
||||
continue;
|
||||
}
|
||||
|
||||
// Merge received channel object into existing currentChannel
|
||||
// so the object references are exactly the same (e.g. in store.state.activeChannel)
|
||||
for (const key in channel) {
|
||||
if (!Object.prototype.hasOwnProperty.call(channel, key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Server sends an empty users array, client requests it whenever needed
|
||||
if (key === "users") {
|
||||
if (channel.type === "channel") {
|
||||
if (
|
||||
store.state.activeChannel &&
|
||||
store.state.activeChannel.channel === currentChannel
|
||||
) {
|
||||
// For currently open channel, request the user list straight away
|
||||
socket.emit("names", {
|
||||
target: channel.id,
|
||||
});
|
||||
} else {
|
||||
// For all other channels, mark the user list as outdated
|
||||
// so an update will be requested whenever user switches to these channels
|
||||
currentChannel.usersOutdated = true;
|
||||
}
|
||||
}
|
||||
emitNamesOrMarkUsersOudated(currentChannel); // TODO: this should not carry logic like that
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Server sends total count of messages in memory, we compare it to amount of messages
|
||||
// on the client, and decide whether theres more messages to load from server
|
||||
if (key === "totalMessages") {
|
||||
currentChannel.moreHistoryAvailable =
|
||||
channel.totalMessages! > currentChannel.messages.length;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reconnection only sends new messages, so merge it on the client
|
||||
// Only concat if server sent us less than 100 messages so we don't introduce gaps
|
||||
if (key === "messages" && currentChannel.messages && channel.messages.length < 100) {
|
||||
currentChannel.messages = currentChannel.messages.concat(channel.messages);
|
||||
} else {
|
||||
currentChannel[key] = channel[key];
|
||||
}
|
||||
// Reconnection only sends new messages, so merge it on the client
|
||||
// Only concat if server sent us less than 100 messages so we don't introduce gaps
|
||||
if (currentChannel.messages && newChannel.messages.length < 100) {
|
||||
currentChannel.messages = currentChannel.messages.concat(newChannel.messages);
|
||||
} else {
|
||||
currentChannel.messages = newChannel.messages;
|
||||
}
|
||||
|
||||
newChannels[c] = currentChannel;
|
||||
// TODO: this is copies more than what the compiler knows about
|
||||
for (const key in newChannel) {
|
||||
if (!Object.hasOwn(currentChannel, key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key === "messages") {
|
||||
// already handled
|
||||
continue;
|
||||
}
|
||||
|
||||
currentChannel[key] = newChannel[key];
|
||||
}
|
||||
|
||||
result.push(currentChannel);
|
||||
}
|
||||
|
||||
return newChannels;
|
||||
return result;
|
||||
}
|
||||
|
||||
function emitNamesOrMarkUsersOudated(chan: ClientChan) {
|
||||
if (store.state.activeChannel && store.state.activeChannel.channel === chan) {
|
||||
// For currently open channel, request the user list straight away
|
||||
socket.emit("names", {
|
||||
target: chan.id,
|
||||
});
|
||||
chan.usersOutdated = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// For all other channels, mark the user list as outdated
|
||||
// so an update will be requested whenever user switches to these channels
|
||||
chan.usersOutdated = true;
|
||||
}
|
||||
|
||||
async function handleQueryParams() {
|
||||
|
@ -170,30 +172,28 @@ async function handleQueryParams() {
|
|||
|
||||
const params = new URLSearchParams(document.location.search);
|
||||
|
||||
const cleanParams = () => {
|
||||
// Remove query parameters from url without reloading the page
|
||||
const cleanUri = window.location.origin + window.location.pathname + window.location.hash;
|
||||
window.history.replaceState({}, document.title, cleanUri);
|
||||
};
|
||||
|
||||
if (params.has("uri")) {
|
||||
// Set default connection settings from IRC protocol links
|
||||
const uri = params.get("uri");
|
||||
const queryParams = parseIrcUri(String(uri));
|
||||
|
||||
cleanParams();
|
||||
removeQueryParams();
|
||||
await router.push({name: "Connect", query: queryParams});
|
||||
|
||||
return true;
|
||||
} else if (document.body.classList.contains("public") && document.location.search) {
|
||||
}
|
||||
|
||||
if (document.body.classList.contains("public") && document.location.search) {
|
||||
// Set default connection settings from url params
|
||||
const queryParams = Object.fromEntries(params.entries());
|
||||
|
||||
cleanParams();
|
||||
removeQueryParams();
|
||||
await router.push({name: "Connect", query: queryParams});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove query parameters from url without reloading the page
|
||||
function removeQueryParams() {
|
||||
const cleanUri = window.location.origin + window.location.pathname + window.location.hash;
|
||||
window.history.replaceState(null, "", cleanUri);
|
||||
}
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
import {switchToChannel} from "../router";
|
||||
import {ClientChan} from "../types";
|
||||
import {toClientChan} from "../chan";
|
||||
|
||||
socket.on("join", function (data) {
|
||||
store.getters.initChannel(data.chan);
|
||||
|
||||
const network = store.getters.findNetwork(data.network);
|
||||
|
||||
if (!network) {
|
||||
return;
|
||||
}
|
||||
|
||||
network.channels.splice(data.index || -1, 0, data.chan);
|
||||
const clientChan: ClientChan = toClientChan(data.chan);
|
||||
network.channels.splice(data.index || -1, 0, clientChan);
|
||||
|
||||
// Queries do not automatically focus, unless the user did a whois
|
||||
if (data.chan.type === "query" && !data.shouldOpen) {
|
||||
|
|
|
@ -1,7 +1,17 @@
|
|||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
import {ClientMention} from "../types";
|
||||
import {SharedMention} from "../../../shared/types/mention";
|
||||
|
||||
socket.on("mentions:list", function (data) {
|
||||
store.commit("mentions", data as ClientMention[]);
|
||||
store.commit("mentions", data.map(sharedToClientMention));
|
||||
});
|
||||
|
||||
function sharedToClientMention(shared: SharedMention): ClientMention {
|
||||
const mention: ClientMention = {
|
||||
...shared,
|
||||
localetime: "", // TODO: can't be right
|
||||
channel: null,
|
||||
};
|
||||
return mention;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import {nextTick} from "vue";
|
|||
|
||||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
import {ClientMessage} from "../types";
|
||||
import type {ClientChan, ClientMessage} from "../types";
|
||||
|
||||
socket.on("more", async (data) => {
|
||||
const channel = store.getters.findChannel(data.chan)?.channel;
|
||||
|
@ -14,13 +14,15 @@ socket.on("more", async (data) => {
|
|||
channel.inputHistory = channel.inputHistory.concat(
|
||||
data.messages
|
||||
.filter((m) => m.self && m.text && m.type === "message")
|
||||
.map((m) => m.text)
|
||||
// TS is too stupid to see the guard in .filter(), so we monkey patch it
|
||||
// to please the compiler
|
||||
.map((m) => (m.text ? m.text : ""))
|
||||
.reverse()
|
||||
.slice(0, 100 - channel.inputHistory.length)
|
||||
);
|
||||
channel.moreHistoryAvailable =
|
||||
data.totalMessages > channel.messages.length + data.messages.length;
|
||||
channel.messages.unshift(...(data.messages as ClientMessage[]));
|
||||
channel.messages.unshift(...data.messages);
|
||||
|
||||
await nextTick();
|
||||
channel.historyLoading = false;
|
||||
|
|
|
@ -3,7 +3,8 @@ import socket from "../socket";
|
|||
import {cleanIrcMessage} from "../../../shared/irc";
|
||||
import {store} from "../store";
|
||||
import {switchToChannel} from "../router";
|
||||
import {ClientChan, ClientMention, ClientMessage, NetChan} from "../types";
|
||||
import {ClientChan, NetChan, ClientMessage} from "../types";
|
||||
import {SharedMsg} from "../../../shared/types/msg";
|
||||
|
||||
let pop;
|
||||
|
||||
|
@ -95,6 +96,14 @@ socket.on("msg", function (data) {
|
|||
}
|
||||
});
|
||||
|
||||
declare global {
|
||||
// this extends the interface from lib.dom with additional stuff which is not
|
||||
// exactly standard but implemented in some browsers
|
||||
interface NotificationOptions {
|
||||
timestamp?: number; // chrome has it, other browsers ignore it
|
||||
}
|
||||
}
|
||||
|
||||
function notifyMessage(
|
||||
targetId: number,
|
||||
channel: ClientChan,
|
||||
|
@ -122,12 +131,14 @@ function notifyMessage(
|
|||
) {
|
||||
let title: string;
|
||||
let body: string;
|
||||
// TODO: fix msg type and get rid of that conditional
|
||||
const nick = msg.from && msg.from.nick ? msg.from.nick : "unkonown";
|
||||
|
||||
if (msg.type === "invite") {
|
||||
title = "New channel invite:";
|
||||
body = msg.from.nick + " invited you to " + msg.channel;
|
||||
body = nick + " invited you to " + msg.channel;
|
||||
} else {
|
||||
title = String(msg.from.nick);
|
||||
title = nick;
|
||||
|
||||
if (channel.type !== "query") {
|
||||
title += ` (${channel.name})`;
|
||||
|
@ -137,7 +148,8 @@ function notifyMessage(
|
|||
title += " says:";
|
||||
}
|
||||
|
||||
body = cleanIrcMessage(msg.text);
|
||||
// TODO: fix msg type and get rid of that conditional
|
||||
body = cleanIrcMessage(msg.text ? msg.text : "");
|
||||
}
|
||||
|
||||
const timestamp = Date.parse(String(msg.time));
|
||||
|
@ -184,24 +196,40 @@ function notifyMessage(
|
|||
}
|
||||
}
|
||||
|
||||
function updateUserList(channel, msg) {
|
||||
if (msg.type === "message" || msg.type === "action") {
|
||||
const user = channel.users.find((u) => u.nick === msg.from.nick);
|
||||
function updateUserList(channel: ClientChan, msg: SharedMsg) {
|
||||
switch (msg.type) {
|
||||
case "message": // fallthrough
|
||||
|
||||
if (user) {
|
||||
user.lastMessage = new Date(msg.time).getTime() || Date.now();
|
||||
case "action": {
|
||||
const user = channel.users.find((u) => u.nick === msg.from?.nick);
|
||||
|
||||
if (user) {
|
||||
user.lastMessage = new Date(msg.time).getTime() || Date.now();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
} else if (msg.type === "quit" || msg.type === "part") {
|
||||
const idx = channel.users.findIndex((u) => u.nick === msg.from.nick);
|
||||
|
||||
if (idx > -1) {
|
||||
channel.users.splice(idx, 1);
|
||||
case "quit": // fallthrough
|
||||
|
||||
case "part": {
|
||||
const idx = channel.users.findIndex((u) => u.nick === msg.from?.nick);
|
||||
|
||||
if (idx > -1) {
|
||||
channel.users.splice(idx, 1);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
} else if (msg.type === "kick") {
|
||||
const idx = channel.users.findIndex((u) => u.nick === msg.target.nick);
|
||||
|
||||
if (idx > -1) {
|
||||
channel.users.splice(idx, 1);
|
||||
case "kick": {
|
||||
const idx = channel.users.findIndex((u) => u.nick === msg.target?.nick);
|
||||
|
||||
if (idx > -1) {
|
||||
channel.users.splice(idx, 1);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ socket.on("msg:preview", function (data) {
|
|||
const netChan = store.getters.findChannel(data.chan);
|
||||
const message = netChan?.channel.messages.find((m) => m.id === data.id);
|
||||
|
||||
if (!message) {
|
||||
if (!message || !message.previews) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
import {switchToChannel} from "../router";
|
||||
import {toClientChan} from "../chan";
|
||||
import {ClientNetwork} from "../types";
|
||||
import {ChanState} from "../../../shared/types/chan";
|
||||
|
||||
socket.on("network", function (data) {
|
||||
const network = data.networks[0];
|
||||
|
||||
network.isJoinChannelShown = false;
|
||||
network.isCollapsed = false;
|
||||
network.channels.forEach(store.getters.initChannel);
|
||||
const network: ClientNetwork = {
|
||||
...data.network,
|
||||
channels: data.network.channels.map(toClientChan),
|
||||
isJoinChannelShown: false,
|
||||
isCollapsed: false,
|
||||
};
|
||||
|
||||
store.commit("networks", [...store.state.networks, network]);
|
||||
|
||||
|
@ -19,7 +23,7 @@ socket.on("network:options", function (data) {
|
|||
const network = store.getters.findNetwork(data.network);
|
||||
|
||||
if (network) {
|
||||
network.serverOptions = data.serverOptions as typeof network.serverOptions;
|
||||
network.serverOptions = data.serverOptions;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -35,8 +39,8 @@ socket.on("network:status", function (data) {
|
|||
|
||||
if (!data.connected) {
|
||||
network.channels.forEach((channel) => {
|
||||
channel.users = [];
|
||||
channel.state = 0;
|
||||
channel.users = []; // TODO: untangle this
|
||||
channel.state = ChanState.PARTED;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,30 +1,16 @@
|
|||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
|
||||
socket.on("sync_sort", function (data) {
|
||||
const order = data.order;
|
||||
|
||||
switch (data.type) {
|
||||
case "networks":
|
||||
store.commit(
|
||||
"sortNetworks",
|
||||
(a, b) => (order as string[]).indexOf(a.uuid) - (order as string[]).indexOf(b.uuid)
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case "channels": {
|
||||
const network = store.getters.findNetwork(data.target);
|
||||
|
||||
if (!network) {
|
||||
return;
|
||||
}
|
||||
|
||||
network.channels.sort(
|
||||
(a, b) => (order as number[]).indexOf(a.id) - (order as number[]).indexOf(b.id)
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
socket.on("sync_sort:networks", function (data) {
|
||||
store.commit("sortNetworks", (a, b) => data.order.indexOf(a.uuid) - data.order.indexOf(b.uuid));
|
||||
});
|
||||
|
||||
socket.on("sync_sort:channels", function (data) {
|
||||
const network = store.getters.findNetwork(data.network);
|
||||
|
||||
if (!network) {
|
||||
return;
|
||||
}
|
||||
|
||||
network.channels.sort((a, b) => data.order.indexOf(a.id) - data.order.indexOf(b.id));
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import io, {Socket} from "socket.io-client";
|
||||
import type {ServerToClientEvents, ClientToServerEvents} from "../../server/types/socket-events";
|
||||
import type {ServerToClientEvents, ClientToServerEvents} from "../../shared/types/socket-events";
|
||||
|
||||
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io({
|
||||
transports: JSON.parse(document.body.dataset.transports || "['polling', 'websocket']"),
|
||||
|
|
|
@ -3,19 +3,12 @@
|
|||
import {ActionContext, createStore, Store, useStore as baseUseStore} from "vuex";
|
||||
import {createSettingsStore} from "./store-settings";
|
||||
import storage from "./localStorage";
|
||||
import type {
|
||||
ClientChan,
|
||||
ClientConfiguration,
|
||||
ClientNetwork,
|
||||
InitClientChan,
|
||||
NetChan,
|
||||
ClientMessage,
|
||||
ClientMention,
|
||||
} from "./types";
|
||||
import type {ClientChan, ClientNetwork, NetChan, ClientMention, ClientMessage} from "./types";
|
||||
import type {InjectionKey} from "vue";
|
||||
|
||||
import {SettingsState} from "./settings";
|
||||
import {SearchQuery} from "../../server/plugins/messageStorage/types";
|
||||
import {SearchQuery} from "../../shared/types/storage";
|
||||
import {SharedConfiguration, LockedSharedConfiguration} from "../../shared/types/config";
|
||||
|
||||
const appName = document.title;
|
||||
|
||||
|
@ -59,7 +52,7 @@ export type State = {
|
|||
mentions: ClientMention[];
|
||||
hasServiceWorker: boolean;
|
||||
pushNotificationState: string;
|
||||
serverConfiguration: ClientConfiguration | null;
|
||||
serverConfiguration: SharedConfiguration | LockedSharedConfiguration | null;
|
||||
sessions: ClientSession[];
|
||||
sidebarOpen: boolean;
|
||||
sidebarDragging: boolean;
|
||||
|
@ -131,7 +124,6 @@ type Getters = {
|
|||
findNetwork: (state: State) => (uuid: string) => ClientNetwork | null;
|
||||
highlightCount(state: State): number;
|
||||
title(state: State, getters: Omit<Getters, "title">): string;
|
||||
initChannel: () => (channel: InitClientChan) => ClientChan;
|
||||
};
|
||||
|
||||
// getters without the state argument
|
||||
|
@ -202,31 +194,6 @@ const getters: Getters = {
|
|||
|
||||
return alertEventCount + channelname + appName;
|
||||
},
|
||||
initChannel: () => (channel: InitClientChan) => {
|
||||
// TODO: This should be a mutation
|
||||
channel.pendingMessage = "";
|
||||
channel.inputHistoryPosition = 0;
|
||||
|
||||
channel.inputHistory = [""].concat(
|
||||
channel.messages
|
||||
.filter((m) => m.self && m.text && m.type === "message")
|
||||
.map((m) => m.text)
|
||||
.reverse()
|
||||
.slice(0, 99)
|
||||
);
|
||||
channel.historyLoading = false;
|
||||
channel.scrolledToBottom = true;
|
||||
channel.editTopic = false;
|
||||
|
||||
channel.moreHistoryAvailable = channel.totalMessages! > channel.messages.length;
|
||||
delete channel.totalMessages;
|
||||
|
||||
if (channel.type === "channel") {
|
||||
channel.usersOutdated = true;
|
||||
}
|
||||
|
||||
return channel as ClientChan;
|
||||
},
|
||||
};
|
||||
|
||||
type Mutations = {
|
||||
|
|
36
client/js/types.d.ts
vendored
36
client/js/types.d.ts
vendored
|
@ -1,12 +1,11 @@
|
|||
import {defineComponent} from "vue";
|
||||
|
||||
import Chan from "../../server/models/chan";
|
||||
import Network from "../../server/models/network";
|
||||
import User from "../../server/models/user";
|
||||
import Message from "../../server/models/msg";
|
||||
import {Mention} from "../../server/client";
|
||||
import {ClientConfiguration} from "../../server/server";
|
||||
import {LinkPreview} from "../../server/plugins/irc-events/link";
|
||||
import {SharedChan} from "../../shared/types/chan";
|
||||
import {SharedNetwork} from "../../shared/types/network";
|
||||
import {SharedUser} from "../../shared/types/user";
|
||||
import {SharedMention} from "../../shared/types/mention";
|
||||
import {SharedConfiguration, LockedSharedConfiguration} from "../../shared/types/config";
|
||||
import {LinkPreview, SharedMsg} from "../../shared/types/msg";
|
||||
|
||||
interface LoungeWindow extends Window {
|
||||
g_TheLoungeRemoveLoading?: () => void;
|
||||
|
@ -16,19 +15,15 @@ interface LoungeWindow extends Window {
|
|||
};
|
||||
}
|
||||
|
||||
type ClientUser = User & {
|
||||
//
|
||||
};
|
||||
type ClientUser = SharedUser;
|
||||
|
||||
type ClientMessage = Omit<Message, "users"> & {
|
||||
time: number;
|
||||
users: string[];
|
||||
};
|
||||
// we will eventually need to put client specific fields here
|
||||
// which are not shared with the server
|
||||
export type ClientMessage = SharedMsg;
|
||||
|
||||
type ClientChan = Omit<Chan, "users" | "messages"> & {
|
||||
type ClientChan = Omit<SharedChan, "messages"> & {
|
||||
moreHistoryAvailable: boolean;
|
||||
editTopic: boolean;
|
||||
users: ClientUser[];
|
||||
messages: ClientMessage[];
|
||||
|
||||
// these are added in store/initChannel
|
||||
|
@ -38,6 +33,8 @@ type ClientChan = Omit<Chan, "users" | "messages"> & {
|
|||
historyLoading: boolean;
|
||||
scrolledToBottom: boolean;
|
||||
usersOutdated: boolean;
|
||||
|
||||
users: ClientUser[];
|
||||
};
|
||||
|
||||
type InitClientChan = ClientChan & {
|
||||
|
@ -46,7 +43,7 @@ type InitClientChan = ClientChan & {
|
|||
};
|
||||
|
||||
// We omit channels so we can use ClientChan[] instead of Chan[]
|
||||
type ClientNetwork = Omit<Network, "channels"> & {
|
||||
type ClientNetwork = Omit<SharedNetwork, "channels"> & {
|
||||
isJoinChannelShown: boolean;
|
||||
isCollapsed: boolean;
|
||||
channels: ClientChan[];
|
||||
|
@ -57,9 +54,8 @@ type NetChan = {
|
|||
network: ClientNetwork;
|
||||
};
|
||||
|
||||
type ClientConfiguration = ClientConfiguration;
|
||||
type ClientMention = Mention & {
|
||||
localetime: string;
|
||||
type ClientMention = SharedMention & {
|
||||
localetime: string; // TODO: this needs to go the way of the dodo, nothing but a single component uses it
|
||||
channel: NetChan | null;
|
||||
};
|
||||
|
||||
|
|
|
@ -7,9 +7,9 @@ import App from "../components/App.vue";
|
|||
import storage from "./localStorage";
|
||||
import {router} from "./router";
|
||||
import socket from "./socket";
|
||||
import "./socket-events"; // this sets up all socket event listeners, do not remove
|
||||
import eventbus from "./eventbus";
|
||||
|
||||
import "./socket-events";
|
||||
import "./webpush";
|
||||
import "./keybinds";
|
||||
import {LoungeWindow} from "./types";
|
||||
|
|
|
@ -6,40 +6,8 @@
|
|||
] /* Specifies a list of glob patterns that match files to be included in compilation. If no 'files' or 'include' property is present in a tsconfig.json, the compiler defaults to including all files in the containing directory and subdirectories except those specified by 'exclude'. Requires TypeScript version 2.0 or later. */,
|
||||
"files": [
|
||||
"../package.json",
|
||||
"../server/types/socket-events.d.ts",
|
||||
"../server/helper.ts",
|
||||
"../server/log.ts",
|
||||
"../server/config.ts",
|
||||
"../server/client.ts",
|
||||
"../server/storageCleaner.ts",
|
||||
"../server/clientManager.ts",
|
||||
"../server/identification.ts",
|
||||
"../server/plugins/changelog.ts",
|
||||
"../server/plugins/uploader.ts",
|
||||
"../server/plugins/storage.ts",
|
||||
"../server/plugins/inputs/index.ts",
|
||||
"../server/plugins/messageStorage/sqlite.ts",
|
||||
"../server/plugins/messageStorage/text.ts",
|
||||
"../server/plugins/packages/index.ts",
|
||||
"../server/plugins/packages/publicClient.ts",
|
||||
"../server/plugins/packages/themes.ts",
|
||||
"../server/plugins/dev-server.ts",
|
||||
"../server/plugins/webpush.ts",
|
||||
"../server/plugins/sts.ts",
|
||||
"../server/plugins/clientCertificate.ts",
|
||||
"../server/plugins/auth.ts",
|
||||
"../server/plugins/auth/local.ts",
|
||||
"../server/plugins/auth/ldap.ts",
|
||||
"../server/plugins/irc-events/link.ts",
|
||||
"../server/command-line/utils.ts",
|
||||
"../server/models/network.ts",
|
||||
"../server/models/user.ts",
|
||||
"../server/models/msg.ts",
|
||||
"../server/models/prefix.ts",
|
||||
"./js/helpers/fullnamemap.json",
|
||||
"./js/helpers/simplemap.json",
|
||||
"../webpack.config.ts",
|
||||
"../babel.config.cjs"
|
||||
"./js/helpers/simplemap.json"
|
||||
] /* If no 'files' or 'include' property is present in a tsconfig.json, the compiler defaults to including all files in the containing directory and subdirectories except those specified by 'exclude'. When a 'files' property is specified, only those files and those specified by 'include' are included. */,
|
||||
// "exclude": [],
|
||||
"compilerOptions": {
|
||||
|
|
115
server/client.ts
115
server/client.ts
|
@ -6,10 +6,12 @@ import crypto from "crypto";
|
|||
import colors from "chalk";
|
||||
|
||||
import log from "./log";
|
||||
import Chan, {ChanConfig, Channel, ChanType} from "./models/chan";
|
||||
import Msg, {MessageType, UserInMessage} from "./models/msg";
|
||||
import Chan, {ChanConfig} from "./models/chan";
|
||||
import Msg from "./models/msg";
|
||||
import Config from "./config";
|
||||
import {condensedTypes} from "../shared/irc";
|
||||
import {MessageType} from "../shared/types/msg";
|
||||
import {SharedMention} from "../shared/types/mention";
|
||||
|
||||
import inputs from "./plugins/inputs";
|
||||
import PublicClient from "./plugins/packages/publicClient";
|
||||
|
@ -17,11 +19,12 @@ import SqliteMessageStorage from "./plugins/messageStorage/sqlite";
|
|||
import TextFileMessageStorage from "./plugins/messageStorage/text";
|
||||
import Network, {IgnoreListItem, NetworkConfig, NetworkWithIrcFramework} from "./models/network";
|
||||
import ClientManager from "./clientManager";
|
||||
import {MessageStorage, SearchQuery, SearchResponse} from "./plugins/messageStorage/types";
|
||||
import {MessageStorage} from "./plugins/messageStorage/types";
|
||||
import {StorageCleaner} from "./storageCleaner";
|
||||
|
||||
type OrderItem = Chan["id"] | Network["uuid"];
|
||||
type Order = OrderItem[];
|
||||
import {SearchQuery, SearchResponse} from "../shared/types/storage";
|
||||
import {SharedChan, ChanType} from "../shared/types/chan";
|
||||
import {SharedNetwork} from "../shared/types/network";
|
||||
import {ServerToClientEvents} from "../shared/types/socket-events";
|
||||
|
||||
const events = [
|
||||
"away",
|
||||
|
@ -82,15 +85,6 @@ export type UserConfig = {
|
|||
networks?: NetworkConfig[];
|
||||
};
|
||||
|
||||
export type Mention = {
|
||||
chanId: number;
|
||||
msgId: number;
|
||||
type: MessageType;
|
||||
time: Date;
|
||||
text: string;
|
||||
from: UserInMessage;
|
||||
};
|
||||
|
||||
class Client {
|
||||
awayMessage!: string;
|
||||
lastActiveChannel!: number;
|
||||
|
@ -98,12 +92,12 @@ class Client {
|
|||
[socketId: string]: {token: string; openChannel: number};
|
||||
};
|
||||
config!: UserConfig;
|
||||
id!: number;
|
||||
id: string;
|
||||
idMsg!: number;
|
||||
idChan!: number;
|
||||
name!: string;
|
||||
networks!: Network[];
|
||||
mentions!: Mention[];
|
||||
mentions!: SharedMention[];
|
||||
manager!: ClientManager;
|
||||
messageStorage!: MessageStorage[];
|
||||
highlightRegex!: RegExp | null;
|
||||
|
@ -113,12 +107,12 @@ class Client {
|
|||
fileHash!: string;
|
||||
|
||||
constructor(manager: ClientManager, name?: string, config = {} as UserConfig) {
|
||||
this.id = uuidv4();
|
||||
_.merge(this, {
|
||||
awayMessage: "",
|
||||
lastActiveChannel: -1,
|
||||
attachedClients: {},
|
||||
config: config,
|
||||
id: uuidv4(),
|
||||
idChan: 1,
|
||||
idMsg: 1,
|
||||
name: name,
|
||||
|
@ -229,9 +223,12 @@ class Client {
|
|||
return chan;
|
||||
}
|
||||
|
||||
emit(event: string, data?: any) {
|
||||
emit<Ev extends keyof ServerToClientEvents>(
|
||||
event: Ev,
|
||||
...args: Parameters<ServerToClientEvents[Ev]>
|
||||
) {
|
||||
if (this.manager !== null) {
|
||||
this.manager.sockets.in(this.id.toString()).emit(event, data);
|
||||
this.manager.sockets.in(this.id).emit(event, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -351,7 +348,7 @@ class Client {
|
|||
|
||||
client.networks.push(network);
|
||||
client.emit("network", {
|
||||
networks: [network.getFilteredClone(this.lastActiveChannel, -1)],
|
||||
network: network.getFilteredClone(this.lastActiveChannel, -1),
|
||||
});
|
||||
|
||||
if (!network.validate(client)) {
|
||||
|
@ -697,56 +694,39 @@ class Client {
|
|||
this.emit("open", targetNetChan.chan.id);
|
||||
}
|
||||
|
||||
sort(data: {order: Order; type: "networks" | "channels"; target: string}) {
|
||||
const order = data.order;
|
||||
sortChannels(netid: SharedNetwork["uuid"], order: SharedChan["id"][]) {
|
||||
const network = _.find(this.networks, {uuid: netid});
|
||||
|
||||
if (!_.isArray(order)) {
|
||||
if (!network) {
|
||||
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;
|
||||
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);
|
||||
});
|
||||
this.save();
|
||||
// Sync order to connected clients
|
||||
this.emit("sync_sort:channels", {
|
||||
network: network.uuid,
|
||||
order: network.channels.map((obj) => obj.id),
|
||||
});
|
||||
}
|
||||
|
||||
sortNetworks(order: SharedNetwork["uuid"][]) {
|
||||
this.networks.sort((a, b) => order.indexOf(a.uuid) - order.indexOf(b.uuid));
|
||||
this.save();
|
||||
// Sync order to connected clients
|
||||
this.emit("sync_sort:networks", {
|
||||
order: this.networks.map((obj) => obj.uuid),
|
||||
});
|
||||
}
|
||||
|
||||
names(data: {target: number}) {
|
||||
|
@ -776,7 +756,7 @@ class Client {
|
|||
|
||||
quit(signOut?: boolean) {
|
||||
const sockets = this.manager.sockets.sockets;
|
||||
const room = sockets.adapter.rooms.get(this.id.toString());
|
||||
const room = sockets.adapter.rooms.get(this.id);
|
||||
|
||||
if (room) {
|
||||
for (const user of room) {
|
||||
|
@ -836,12 +816,13 @@ class Client {
|
|||
}
|
||||
|
||||
// TODO: type session to this.attachedClients
|
||||
registerPushSubscription(session: any, subscription: ClientPushSubscription, noSave = false) {
|
||||
registerPushSubscription(session: any, subscription: PushSubscriptionJSON, noSave = false) {
|
||||
if (
|
||||
!_.isPlainObject(subscription) ||
|
||||
!_.isPlainObject(subscription.keys) ||
|
||||
typeof subscription.endpoint !== "string" ||
|
||||
!/^https?:\/\//.test(subscription.endpoint) ||
|
||||
!_.isPlainObject(subscription.keys) ||
|
||||
!subscription.keys || // TS compiler doesn't understand isPlainObject
|
||||
typeof subscription.keys.p256dh !== "string" ||
|
||||
typeof subscription.keys.auth !== "string"
|
||||
) {
|
||||
|
|
|
@ -10,7 +10,7 @@ import Config from "./config";
|
|||
import {NetworkConfig} from "./models/network";
|
||||
import WebPush from "./plugins/webpush";
|
||||
import log from "./log";
|
||||
import {Server} from "socket.io";
|
||||
import {Server} from "./server";
|
||||
|
||||
class ClientManager {
|
||||
clients: Client[];
|
||||
|
|
|
@ -4,6 +4,7 @@ import fs, {Stats} from "fs";
|
|||
import os from "os";
|
||||
import _ from "lodash";
|
||||
import colors from "chalk";
|
||||
import {SearchOptions} from "ldapjs";
|
||||
|
||||
import log from "./log";
|
||||
import Helper from "./helper";
|
||||
|
@ -44,7 +45,7 @@ export type Defaults = Pick<
|
|||
| "saslAccount"
|
||||
| "saslPassword"
|
||||
> & {
|
||||
join?: string;
|
||||
join: string;
|
||||
};
|
||||
|
||||
type Identd = {
|
||||
|
@ -57,7 +58,7 @@ type SearchDN = {
|
|||
rootPassword: string;
|
||||
filter: string;
|
||||
base: string;
|
||||
scope: string;
|
||||
scope: SearchOptions["scope"];
|
||||
};
|
||||
|
||||
type Ldap = {
|
||||
|
|
|
@ -2,36 +2,14 @@ import _ from "lodash";
|
|||
import log from "../log";
|
||||
import Config from "../config";
|
||||
import User from "./user";
|
||||
import Msg, {MessageType} from "./msg";
|
||||
import Msg from "./msg";
|
||||
import storage from "../plugins/storage";
|
||||
import Client from "../client";
|
||||
import Network from "./network";
|
||||
import Prefix from "./prefix";
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export type FilteredChannel = Chan & {
|
||||
users: [];
|
||||
totalMessages: number;
|
||||
};
|
||||
import {MessageType, SharedMsg} from "../../shared/types/msg";
|
||||
import {ChanType, SpecialChanType, ChanState} from "../../shared/types/chan";
|
||||
import {SharedNetworkChan} from "../../shared/types/network";
|
||||
|
||||
export type ChanConfig = {
|
||||
name: string;
|
||||
|
@ -60,7 +38,6 @@ class Chan {
|
|||
data?: any;
|
||||
closed?: boolean;
|
||||
num_users?: number;
|
||||
static optionalProperties = ["userAway", "special", "data", "closed", "num_users"];
|
||||
|
||||
constructor(attr?: Partial<Chan>) {
|
||||
_.defaults(this, attr, {
|
||||
|
@ -84,18 +61,11 @@ class Chan {
|
|||
}
|
||||
|
||||
pushMessage(client: Client, msg: Msg, increasesUnread = false) {
|
||||
const chan = this.id;
|
||||
const obj = {chan, msg} as {
|
||||
chan: number;
|
||||
msg: Msg;
|
||||
unread?: number;
|
||||
highlight?: number;
|
||||
};
|
||||
|
||||
const chanId = this.id;
|
||||
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;
|
||||
const isOpen = _.find(client.attachedClients, {openChannel: chanId}) !== undefined;
|
||||
|
||||
if (msg.self) {
|
||||
// reset counters/markers when receiving self-/echo-message
|
||||
|
@ -108,15 +78,15 @@ class Chan {
|
|||
}
|
||||
|
||||
if (increasesUnread || msg.highlight) {
|
||||
obj.unread = ++this.unread;
|
||||
this.unread++;
|
||||
}
|
||||
|
||||
if (msg.highlight) {
|
||||
obj.highlight = ++this.highlight;
|
||||
this.highlight++;
|
||||
}
|
||||
}
|
||||
|
||||
client.emit("msg", obj);
|
||||
client.emit("msg", {chan: chanId, msg, unread: this.unread, highlight: this.highlight});
|
||||
|
||||
// Never store messages in public mode as the session
|
||||
// is completely destroyed when the page gets closed
|
||||
|
@ -144,7 +114,8 @@ class Chan {
|
|||
}
|
||||
}
|
||||
}
|
||||
dereferencePreviews(messages) {
|
||||
|
||||
dereferencePreviews(messages: Msg[]) {
|
||||
if (!Config.values.prefetch || !Config.values.prefetchStorage) {
|
||||
return;
|
||||
}
|
||||
|
@ -160,6 +131,7 @@ class Chan {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
getSortedUsers(irc?: Network["irc"]) {
|
||||
const users = Array.from(this.users.values());
|
||||
|
||||
|
@ -182,21 +154,27 @@ class Chan {
|
|||
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}, new Prefix([]));
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -206,38 +184,54 @@ class Chan {
|
|||
* 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 (Chan.optionalProperties.includes(prop)) {
|
||||
if (this[prop] !== undefined || (Array.isArray(this[prop]) && this[prop].length)) {
|
||||
newChannel[prop] = this[prop];
|
||||
}
|
||||
} else 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 && 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;
|
||||
getFilteredClone(
|
||||
lastActiveChannel?: number | boolean,
|
||||
lastMessage?: number
|
||||
): SharedNetworkChan {
|
||||
let msgs: SharedMsg[];
|
||||
|
||||
newChannel[prop] = this[prop].slice(-messagesToSend);
|
||||
}
|
||||
// If client is reconnecting, only send new messages that client has not seen yet
|
||||
if (lastMessage && 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
|
||||
msgs = this.messages.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;
|
||||
msgs = this.messages.slice(-messagesToSend);
|
||||
}
|
||||
|
||||
(newChannel as FilteredChannel).totalMessages = this[prop].length;
|
||||
} else {
|
||||
newChannel[prop] = this[prop];
|
||||
}
|
||||
return {
|
||||
id: this.id,
|
||||
messages: msgs,
|
||||
totalMessages: this.messages.length,
|
||||
name: this.name,
|
||||
key: this.key,
|
||||
topic: this.topic,
|
||||
firstUnread: this.firstUnread,
|
||||
unread: this.unread,
|
||||
highlight: this.highlight,
|
||||
muted: this.muted,
|
||||
type: this.type,
|
||||
state: this.state,
|
||||
|
||||
return newChannel;
|
||||
}, {}) as FilteredChannel;
|
||||
special: this.special,
|
||||
data: this.data,
|
||||
closed: this.closed,
|
||||
num_users: this.num_users,
|
||||
};
|
||||
// TODO: funny array mutation below might need to be reproduced
|
||||
// static optionalProperties = ["userAway", "special", "data", "closed", "num_users"];
|
||||
// return Object.keys(this).reduce((newChannel, prop) => {
|
||||
// if (Chan.optionalProperties.includes(prop)) {
|
||||
// if (this[prop] !== undefined || (Array.isArray(this[prop]) && this[prop].length)) {
|
||||
// newChannel[prop] = this[prop];
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
writeUserLog(client: Client, msg: Msg) {
|
||||
this.messages.push(msg);
|
||||
|
||||
|
@ -270,6 +264,7 @@ class Chan {
|
|||
messageStorage.index(target.network, targetChannel, msg).catch((e) => log.error(e));
|
||||
}
|
||||
}
|
||||
|
||||
loadMessages(client: Client, network: Network) {
|
||||
if (!this.isLoggable()) {
|
||||
return;
|
||||
|
@ -326,15 +321,23 @@ class Chan {
|
|||
log.error(`Failed to load messages for ${client.name}: ${err.toString()}`)
|
||||
);
|
||||
}
|
||||
|
||||
isLoggable() {
|
||||
return this.type === ChanType.CHANNEL || this.type === ChanType.QUERY;
|
||||
}
|
||||
|
||||
setMuteStatus(muted: boolean) {
|
||||
this.muted = !!muted;
|
||||
}
|
||||
}
|
||||
|
||||
function requestZncPlayback(channel, network, from) {
|
||||
function requestZncPlayback(channel: Chan, network: Network, from: number) {
|
||||
if (!network.irc) {
|
||||
throw new Error(
|
||||
`requestZncPlayback: no irc field on network "${network.name}", this is a bug`
|
||||
);
|
||||
}
|
||||
|
||||
network.irc.raw("ZNC", "*playback", "PLAY", channel.name, from.toString());
|
||||
}
|
||||
|
||||
|
|
|
@ -1,41 +1,5 @@
|
|||
import _ from "lodash";
|
||||
import {LinkPreview} from "../plugins/irc-events/link";
|
||||
import User from "./user";
|
||||
|
||||
export type UserInMessage = Partial<User> & {
|
||||
mode: 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",
|
||||
}
|
||||
import {MessageType, LinkPreview, UserInMessage} from "../../shared/types/msg";
|
||||
|
||||
class Msg {
|
||||
from!: UserInMessage;
|
||||
|
@ -70,7 +34,7 @@ class Msg {
|
|||
raw_modes!: any;
|
||||
when!: Date;
|
||||
whois!: any;
|
||||
users!: UserInMessage[] | string[];
|
||||
users!: string[];
|
||||
statusmsgGroup!: string;
|
||||
params!: string[];
|
||||
|
||||
|
|
|
@ -1,24 +1,17 @@
|
|||
import _ from "lodash";
|
||||
import {v4 as uuidv4} from "uuid";
|
||||
import IrcFramework, {Client as IRCClient} from "irc-framework";
|
||||
import Chan, {ChanConfig, Channel, ChanType} from "./chan";
|
||||
import Msg, {MessageType} from "./msg";
|
||||
import Chan, {ChanConfig, Channel} from "./chan";
|
||||
import Msg from "./msg";
|
||||
import Prefix from "./prefix";
|
||||
import Helper, {Hostmask} from "../helper";
|
||||
import Config, {WebIRC} from "../config";
|
||||
import STSPolicies from "../plugins/sts";
|
||||
import ClientCertificate, {ClientCertificateType} from "../plugins/clientCertificate";
|
||||
import Client from "../client";
|
||||
|
||||
/**
|
||||
* List of keys which should be sent to the client by default.
|
||||
*/
|
||||
const fieldsForClient = {
|
||||
uuid: true,
|
||||
name: true,
|
||||
nick: true,
|
||||
serverOptions: true,
|
||||
};
|
||||
import {MessageType} from "../../shared/types/msg";
|
||||
import {ChanType} from "../../shared/types/chan";
|
||||
import {SharedNetwork} from "../../shared/types/network";
|
||||
|
||||
type NetworkIrcOptions = {
|
||||
host: string;
|
||||
|
@ -52,7 +45,7 @@ type NetworkStatus = {
|
|||
};
|
||||
|
||||
export type IgnoreListItem = Hostmask & {
|
||||
when?: number;
|
||||
when: number;
|
||||
};
|
||||
|
||||
type IgnoreList = IgnoreListItem[];
|
||||
|
@ -505,24 +498,17 @@ class Network {
|
|||
}
|
||||
}
|
||||
|
||||
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;
|
||||
getFilteredClone(lastActiveChannel?: number, lastMessage?: number): SharedNetwork {
|
||||
return {
|
||||
uuid: this.uuid,
|
||||
name: this.name,
|
||||
nick: this.nick,
|
||||
serverOptions: this.serverOptions,
|
||||
status: this.getNetworkStatus(),
|
||||
channels: this.channels.map((channel) =>
|
||||
channel.getFilteredClone(lastActiveChannel, lastMessage)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
getNetworkStatus() {
|
||||
|
|
|
@ -67,11 +67,11 @@ function advancedLdapAuth(user: string, password: string, callback: (success: bo
|
|||
});
|
||||
|
||||
const base = config.ldap.searchDN.base;
|
||||
const searchOptions = {
|
||||
const searchOptions: SearchOptions = {
|
||||
scope: config.ldap.searchDN.scope,
|
||||
filter: `(&(${config.ldap.primaryKey}=${userDN})${config.ldap.searchDN.filter})`,
|
||||
attributes: ["dn"],
|
||||
} as SearchOptions;
|
||||
};
|
||||
|
||||
ldapclient.on("error", function (err: Error) {
|
||||
log.error(`Unable to connect to LDAP server: ${err.toString()}`);
|
||||
|
@ -178,12 +178,12 @@ function advancedLdapLoadUsers(users: string[], callbackLoadUser) {
|
|||
|
||||
const remainingUsers = new Set(users);
|
||||
|
||||
const searchOptions = {
|
||||
const searchOptions: SearchOptions = {
|
||||
scope: config.ldap.searchDN.scope,
|
||||
filter: `${config.ldap.searchDN.filter}`,
|
||||
attributes: [config.ldap.primaryKey],
|
||||
paged: true,
|
||||
} as SearchOptions;
|
||||
};
|
||||
|
||||
ldapclient.search(base, searchOptions, function (err2, res) {
|
||||
if (err2) {
|
||||
|
|
|
@ -4,6 +4,7 @@ import log from "../log";
|
|||
import pkg from "../../package.json";
|
||||
import ClientManager from "../clientManager";
|
||||
import Config from "../config";
|
||||
import {SharedChangelogData} from "../../shared/types/changelog";
|
||||
|
||||
const TIME_TO_LIVE = 15 * 60 * 1000; // 15 minutes, in milliseconds
|
||||
|
||||
|
@ -12,31 +13,17 @@ export default {
|
|||
fetch,
|
||||
checkForUpdates,
|
||||
};
|
||||
export type ChangelogData = {
|
||||
current: {
|
||||
prerelease: boolean;
|
||||
version: string;
|
||||
changelog?: string;
|
||||
url: string;
|
||||
};
|
||||
expiresAt: number;
|
||||
latest?: {
|
||||
prerelease: boolean;
|
||||
version: string;
|
||||
url: string;
|
||||
};
|
||||
packages?: boolean;
|
||||
};
|
||||
|
||||
const versions = {
|
||||
const versions: SharedChangelogData = {
|
||||
current: {
|
||||
prerelease: false,
|
||||
version: `v${pkg.version}`,
|
||||
changelog: undefined,
|
||||
url: "", // TODO: properly init
|
||||
},
|
||||
expiresAt: -1,
|
||||
latest: undefined,
|
||||
packages: undefined,
|
||||
} as ChangelogData;
|
||||
};
|
||||
|
||||
async function fetch() {
|
||||
const time = Date.now();
|
||||
|
|
|
@ -31,7 +31,7 @@ function get(uuid: string): ClientCertificateType | null {
|
|||
return {
|
||||
private_key: fs.readFileSync(paths.privateKeyPath, "utf-8"),
|
||||
certificate: fs.readFileSync(paths.certificatePath, "utf-8"),
|
||||
} as ClientCertificateType;
|
||||
};
|
||||
} catch (e: any) {
|
||||
log.error("Unable to get certificate", e);
|
||||
}
|
||||
|
@ -122,10 +122,10 @@ function generate() {
|
|||
// Sign this certificate with a SHA256 signature
|
||||
cert.sign(keys.privateKey, md.sha256.create());
|
||||
|
||||
const pem = {
|
||||
const pem: ClientCertificateType = {
|
||||
private_key: pki.privateKeyToPem(keys.privateKey),
|
||||
certificate: pki.certificateToPem(cert),
|
||||
} as ClientCertificateType;
|
||||
};
|
||||
|
||||
return pem;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import {ChanType} from "../../models/chan";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["slap", "me"];
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {ChanType} from "../../models/chan";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {PluginInputHandler} from "./index";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["ban", "unban", "banlist", "kickban"];
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {PluginInputHandler} from "./index";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
const commands = ["connect", "server"];
|
||||
const allowDisconnected = true;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {PluginInputHandler} from "./index";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
const commands = ["ctcp"];
|
||||
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import Helper from "../../helper";
|
||||
import {PluginInputHandler} from "./index";
|
||||
import {IgnoreListItem} from "../../models/network";
|
||||
import {ChanType, SpecialChanType} from "../../models/chan";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
const commands = ["ignore", "unignore", "ignorelist"];
|
||||
const commands = ["ignore", "unignore"];
|
||||
|
||||
const input: PluginInputHandler = function (network, chan, cmd, args) {
|
||||
const client = this;
|
||||
let target: string;
|
||||
// let hostmask: cmd === "ignoreList" ? string : undefined;
|
||||
let hostmask: IgnoreListItem | undefined;
|
||||
|
||||
if (cmd !== "ignorelist" && (args.length === 0 || args[0].trim().length === 0)) {
|
||||
if (args.length === 0 || args[0].trim().length === 0) {
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
|
@ -24,16 +20,13 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (cmd !== "ignorelist") {
|
||||
// Trim to remove any spaces from the hostmask
|
||||
target = args[0].trim();
|
||||
hostmask = Helper.parseHostmask(target) as IgnoreListItem;
|
||||
}
|
||||
const target = args[0].trim();
|
||||
const hostmask = Helper.parseHostmask(target);
|
||||
|
||||
switch (cmd) {
|
||||
case "ignore": {
|
||||
// IRC nicks are case insensitive
|
||||
if (hostmask!.nick.toLowerCase() === network.nick.toLowerCase()) {
|
||||
if (hostmask.nick.toLowerCase() === network.nick.toLowerCase()) {
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
|
@ -41,25 +34,14 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
|||
text: "You can't ignore yourself",
|
||||
})
|
||||
);
|
||||
} else if (
|
||||
!network.ignoreList.some(function (entry) {
|
||||
return Helper.compareHostmask(entry, hostmask!);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
network.ignoreList.some(function (entry) {
|
||||
return Helper.compareHostmask(entry, hostmask);
|
||||
})
|
||||
) {
|
||||
hostmask!.when = Date.now();
|
||||
network.ignoreList.push(hostmask!);
|
||||
|
||||
client.save();
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: MessageType.ERROR,
|
||||
text: `\u0002${hostmask!.nick}!${hostmask!.ident}@${
|
||||
hostmask!.hostname
|
||||
}\u000f added to ignorelist`,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
|
@ -67,32 +49,31 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
|||
text: "The specified user/hostmask is already ignored",
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
network.ignoreList.push({
|
||||
...hostmask,
|
||||
when: Date.now(),
|
||||
});
|
||||
|
||||
client.save();
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: MessageType.ERROR, // TODO: Successfully added via type.Error 🤔 ?
|
||||
text: `\u0002${hostmask.nick}!${hostmask.ident}@${hostmask.hostname}\u000f added to ignorelist`,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
case "unignore": {
|
||||
const idx = network.ignoreList.findIndex(function (entry) {
|
||||
return Helper.compareHostmask(entry, hostmask!);
|
||||
return Helper.compareHostmask(entry, hostmask);
|
||||
});
|
||||
|
||||
// Check if the entry exists before removing it, otherwise
|
||||
// let the user know.
|
||||
if (idx !== -1) {
|
||||
network.ignoreList.splice(idx, 1);
|
||||
client.save();
|
||||
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: MessageType.ERROR,
|
||||
text: `Successfully removed \u0002${hostmask!.nick}!${hostmask!.ident}@${
|
||||
hostmask!.hostname
|
||||
}\u000f from ignorelist`,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
if (idx === -1) {
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
|
@ -100,52 +81,20 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
|||
text: "The specified user/hostmask is not ignored",
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
network.ignoreList.splice(idx, 1);
|
||||
client.save();
|
||||
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: MessageType.ERROR, // TODO: Successfully removed via type.Error 🤔 ?
|
||||
text: `Successfully removed \u0002${hostmask.nick}!${hostmask.ident}@${hostmask.hostname}\u000f from ignorelist`,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
case "ignorelist":
|
||||
if (network.ignoreList.length === 0) {
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: MessageType.ERROR,
|
||||
text: "Ignorelist is empty",
|
||||
})
|
||||
);
|
||||
} else {
|
||||
const chanName = "Ignored users";
|
||||
const ignored = network.ignoreList.map((data) => ({
|
||||
hostmask: `${data.nick}!${data.ident}@${data.hostname}`,
|
||||
when: data.when,
|
||||
}));
|
||||
let newChan = network.getChannel(chanName);
|
||||
|
||||
if (typeof newChan === "undefined") {
|
||||
newChan = client.createChannel({
|
||||
type: ChanType.SPECIAL,
|
||||
special: SpecialChanType.IGNORELIST,
|
||||
name: chanName,
|
||||
data: ignored,
|
||||
});
|
||||
client.emit("join", {
|
||||
network: network.uuid,
|
||||
chan: newChan.getFilteredClone(true),
|
||||
index: network.addChannel(newChan),
|
||||
});
|
||||
} else {
|
||||
// TODO: add type for this chan/event
|
||||
newChan.data = ignored;
|
||||
|
||||
client.emit("msg:special", {
|
||||
chan: newChan.id,
|
||||
data: ignored,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
57
server/plugins/inputs/ignorelist.ts
Normal file
57
server/plugins/inputs/ignorelist.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
import Msg from "../../models/msg";
|
||||
import {ChanType, SpecialChanType} from "../../../shared/types/chan";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
const commands = ["ignorelist"];
|
||||
|
||||
const input: PluginInputHandler = function (network, chan, _cmd, _args) {
|
||||
const client = this;
|
||||
|
||||
if (network.ignoreList.length === 0) {
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: MessageType.ERROR,
|
||||
text: "Ignorelist is empty",
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const chanName = "Ignored users";
|
||||
const ignored = network.ignoreList.map((data) => ({
|
||||
hostmask: `${data.nick}!${data.ident}@${data.hostname}`,
|
||||
when: data.when,
|
||||
}));
|
||||
let newChan = network.getChannel(chanName);
|
||||
|
||||
if (typeof newChan === "undefined") {
|
||||
newChan = client.createChannel({
|
||||
type: ChanType.SPECIAL,
|
||||
special: SpecialChanType.IGNORELIST,
|
||||
name: chanName,
|
||||
data: ignored,
|
||||
});
|
||||
client.emit("join", {
|
||||
network: network.uuid,
|
||||
chan: newChan.getFilteredClone(true),
|
||||
shouldOpen: false,
|
||||
index: network.addChannel(newChan),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: add type for this chan/event
|
||||
newChan.data = ignored;
|
||||
|
||||
client.emit("msg:special", {
|
||||
chan: newChan.id,
|
||||
data: ignored,
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
commands,
|
||||
input,
|
||||
};
|
|
@ -54,6 +54,7 @@ const builtInInputs = [
|
|||
"ctcp",
|
||||
"disconnect",
|
||||
"ignore",
|
||||
"ignorelist",
|
||||
"invite",
|
||||
"kick",
|
||||
"kill",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import {ChanType} from "../../models/chan";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["invite", "invitelist"];
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import {ChanType} from "../../models/chan";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["kick"];
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import {ChanType} from "../../models/chan";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["mode", "umode", "op", "deop", "hop", "dehop", "voice", "devoice"];
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Chan, {ChanType} from "../../models/chan";
|
||||
import Msg from "../../models/msg";
|
||||
import Chan from "../../models/chan";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["query", "msg", "say"];
|
||||
|
||||
|
@ -97,10 +99,10 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
|||
// being sent back to us.
|
||||
if (!network.irc.network.cap.isEnabled("echo-message")) {
|
||||
const parsedTarget = network.irc.network.extractTargetGroup(targetName);
|
||||
let targetGroup;
|
||||
let targetGroup: string | undefined = undefined;
|
||||
|
||||
if (parsedTarget) {
|
||||
targetName = parsedTarget.target as string;
|
||||
targetName = parsedTarget.target;
|
||||
targetGroup = parsedTarget.target_group;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,9 +2,10 @@ import Chan from "../../models/chan";
|
|||
import Network from "../../models/network";
|
||||
import {PluginInputHandler} from "./index";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
|
||||
import Client from "../../client";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
const commands = ["mute", "unmute"];
|
||||
const allowDisconnected = true;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
const commands = ["nick"];
|
||||
const allowDisconnected = true;
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import Config from "../../config";
|
||||
import {ChanType, ChanState} from "../../models/chan";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType, ChanState} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["close", "leave", "part"];
|
||||
const allowDisconnected = true;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import {ChanType} from "../../models/chan";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["cycle", "rejoin"];
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import {ChanType} from "../../models/chan";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["topic"];
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
import {ChanType} from "../../models/chan";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -14,7 +14,7 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
handleSTS(data, false);
|
||||
});
|
||||
|
||||
function handleSTS(data, shouldReconnect) {
|
||||
function handleSTS(data, shouldReconnect: boolean) {
|
||||
if (!Object.prototype.hasOwnProperty.call(data.capabilities, "sts")) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -3,10 +3,11 @@ import _ from "lodash";
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import log from "../../log";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import Helper from "../../helper";
|
||||
import Config from "../../config";
|
||||
import {ChanType, ChanState} from "../../models/chan";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType, ChanState} from "../../../shared/types/chan";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import _ from "lodash";
|
||||
import {IrcEventHandler} from "../../client";
|
||||
import Helper from "../../helper";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import User from "../../models/user";
|
||||
import pkg from "../../../package.json";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
const ctcpResponses = {
|
||||
CLIENTINFO: () =>
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import Config from "../../config";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {IrcEventHandler} from "../../client";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {IrcEventHandler} from "../../client";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import User from "../../models/user";
|
||||
import type {IrcEventHandler} from "../../client";
|
||||
import {ChanState} from "../../models/chan";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanState} from "../../../shared/types/chan";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
@ -18,6 +19,7 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
client.emit("join", {
|
||||
network: network.uuid,
|
||||
chan: chan.getFilteredClone(true),
|
||||
shouldOpen: false,
|
||||
index: network.addChannel(chan),
|
||||
});
|
||||
client.save();
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
import {ChanState} from "../../models/chan";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import User from "../../models/user";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanState} from "../../../shared/types/chan";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
@ -14,11 +14,12 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
return;
|
||||
}
|
||||
|
||||
const user = chan.getUser(data.kicked!);
|
||||
const msg = new Msg({
|
||||
type: MessageType.KICK,
|
||||
time: data.time,
|
||||
from: chan.getUser(data.nick),
|
||||
target: chan.getUser(data.kicked!),
|
||||
target: user,
|
||||
text: data.message || "",
|
||||
highlight: data.kicked === irc.user.nick,
|
||||
self: data.nick === irc.user.nick,
|
||||
|
@ -34,7 +35,7 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
state: chan.state,
|
||||
});
|
||||
} else {
|
||||
chan.removeUser(msg.target as User);
|
||||
chan.removeUser(user);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@ import mime from "mime-types";
|
|||
import log from "../../log";
|
||||
import Config from "../../config";
|
||||
import {findLinksWithSchema} from "../../../shared/linkify";
|
||||
import {LinkPreview} from "../../../shared/types/msg";
|
||||
import storage from "../storage";
|
||||
import Client from "../../client";
|
||||
import Chan from "../../models/chan";
|
||||
|
@ -20,23 +21,6 @@ const currentFetchPromises = new Map<string, Promise<FetchRequest>>();
|
|||
const imageTypeRegex = /^image\/.+/;
|
||||
const mediaTypeRegex = /^(audio|video)\/.+/;
|
||||
|
||||
export type LinkPreview = {
|
||||
type: string;
|
||||
head: string;
|
||||
body: string;
|
||||
thumb: string;
|
||||
size: number;
|
||||
link: string; // Send original matched link to the client
|
||||
shown?: boolean | null;
|
||||
error?: string;
|
||||
message?: string;
|
||||
|
||||
media?: string;
|
||||
mediaType?: string;
|
||||
maxSize?: number;
|
||||
thumbActualUrl?: string;
|
||||
};
|
||||
|
||||
export default function (client: Client, chan: Chan, msg: Msg, cleanText: string) {
|
||||
if (!Config.values.prefetch) {
|
||||
return;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Chan, {ChanType, SpecialChanType} from "../../models/chan";
|
||||
import Chan from "../../models/chan";
|
||||
import {ChanType, SpecialChanType} from "../../../shared/types/chan";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
@ -50,6 +51,7 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
client.emit("join", {
|
||||
network: network.uuid,
|
||||
chan: chan.getFilteredClone(true),
|
||||
shouldOpen: false,
|
||||
index: network.addChannel(chan),
|
||||
});
|
||||
} else {
|
||||
|
|
|
@ -1,24 +1,38 @@
|
|||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import LinkPrefetch from "./link";
|
||||
import {cleanIrcMessage} from "../../../shared/irc";
|
||||
import Helper from "../../helper";
|
||||
import {IrcEventHandler} from "../../client";
|
||||
import Chan, {ChanType} from "../../models/chan";
|
||||
import Chan from "../../models/chan";
|
||||
import User from "../../models/user";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
import {MessageEventArgs} from "irc-framework";
|
||||
|
||||
const nickRegExp = /(?:\x03[0-9]{1,2}(?:,[0-9]{1,2})?)?([\w[\]\\`^{|}-]+)/g;
|
||||
|
||||
type HandleInput = {
|
||||
nick: string;
|
||||
hostname: string;
|
||||
ident: string;
|
||||
target: string;
|
||||
type: MessageType;
|
||||
time: number;
|
||||
text?: string;
|
||||
from_server?: boolean;
|
||||
message: string;
|
||||
group?: string;
|
||||
};
|
||||
|
||||
function convertForHandle(type: MessageType, data: MessageEventArgs): HandleInput {
|
||||
return {...data, time: data.time ? data.time : new Date().getTime(), type: type};
|
||||
}
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
||||
irc.on("notice", function (data) {
|
||||
data.type = MessageType.NOTICE;
|
||||
|
||||
type ModifiedData = typeof data & {
|
||||
type: MessageType.NOTICE;
|
||||
};
|
||||
|
||||
handleMessage(data as ModifiedData);
|
||||
handleMessage(convertForHandle(MessageType.NOTICE, data));
|
||||
});
|
||||
|
||||
irc.on("action", function (data) {
|
||||
|
@ -37,18 +51,7 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
handleMessage(data);
|
||||
});
|
||||
|
||||
function handleMessage(data: {
|
||||
nick: string;
|
||||
hostname: string;
|
||||
ident: string;
|
||||
target: string;
|
||||
type: MessageType;
|
||||
time: number;
|
||||
text?: string;
|
||||
from_server?: boolean;
|
||||
message: string;
|
||||
group?: string;
|
||||
}) {
|
||||
function handleMessage(data: HandleInput) {
|
||||
let chan: Chan | undefined;
|
||||
let from: User;
|
||||
let highlight = false;
|
||||
|
@ -105,6 +108,7 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
client.emit("join", {
|
||||
network: network.uuid,
|
||||
chan: chan.getFilteredClone(true),
|
||||
shouldOpen: false,
|
||||
index: network.addChannel(chan),
|
||||
});
|
||||
client.save();
|
||||
|
@ -125,7 +129,7 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
// msg is constructed down here because `from` is being copied in the constructor
|
||||
const msg = new Msg({
|
||||
type: data.type,
|
||||
time: data.time as any,
|
||||
time: new Date(data.time),
|
||||
text: data.message,
|
||||
self: self,
|
||||
from: from,
|
||||
|
@ -164,7 +168,6 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
|
||||
while ((match = nickRegExp.exec(data.message))) {
|
||||
if (chan.findUser(match[1])) {
|
||||
// @ts-expect-error Type 'string' is not assignable to type '{ mode: string; }'.ts(2345)
|
||||
msg.users.push(match[1]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import _ from "lodash";
|
||||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
import {SpecialChanType, ChanType} from "../../models/chan";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {SpecialChanType, ChanType} from "../../../shared/types/chan";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
@ -68,6 +69,7 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
client.emit("join", {
|
||||
network: network.uuid,
|
||||
chan: chan.getFilteredClone(true),
|
||||
shouldOpen: false,
|
||||
index: network.addChannel(chan),
|
||||
});
|
||||
} else {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
import {ChanType} from "../../models/chan";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
@ -28,9 +29,9 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
});
|
||||
|
||||
client.emit("join", {
|
||||
shouldOpen: true,
|
||||
network: network.uuid,
|
||||
chan: chan.getFilteredClone(true),
|
||||
shouldOpen: true,
|
||||
index: network.addChannel(chan),
|
||||
});
|
||||
chan.loadMessages(client, network);
|
||||
|
|
|
@ -7,8 +7,9 @@ import Config from "../../config";
|
|||
import Msg, {Message} from "../../models/msg";
|
||||
import Chan, {Channel} from "../../models/chan";
|
||||
import Helper from "../../helper";
|
||||
import type {SearchResponse, SearchQuery, SearchableMessageStorage, DeletionRequest} from "./types";
|
||||
import type {SearchableMessageStorage, DeletionRequest} from "./types";
|
||||
import Network from "../../models/network";
|
||||
import {SearchQuery, SearchResponse} from "../../../shared/types/storage";
|
||||
|
||||
// TODO; type
|
||||
let sqlite3: any;
|
||||
|
|
|
@ -6,8 +6,9 @@ import filenamify from "filenamify";
|
|||
import Config from "../../config";
|
||||
import {MessageStorage} from "./types";
|
||||
import Channel from "../../models/chan";
|
||||
import {Message, MessageType} from "../../models/msg";
|
||||
import {Message} from "../../models/msg";
|
||||
import Network from "../../models/network";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
class TextFileMessageStorage implements MessageStorage {
|
||||
isEnabled: boolean;
|
||||
|
|
14
server/plugins/messageStorage/types.d.ts
vendored
14
server/plugins/messageStorage/types.d.ts
vendored
|
@ -4,7 +4,8 @@ import {Channel} from "../../models/channel";
|
|||
import {Message} from "../../models/message";
|
||||
import {Network} from "../../models/network";
|
||||
import Client from "../../client";
|
||||
import type {MessageType} from "../../models/msg";
|
||||
import {SearchQuery, SearchResponse} from "../../../shared/types/storage";
|
||||
import type {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export type DeletionRequest = {
|
||||
olderThanDays: number;
|
||||
|
@ -28,17 +29,6 @@ interface MessageStorage {
|
|||
canProvideMessages(): boolean;
|
||||
}
|
||||
|
||||
export type SearchQuery = {
|
||||
searchTerm: string;
|
||||
networkUuid: string;
|
||||
channelName: string;
|
||||
offset: number;
|
||||
};
|
||||
|
||||
export type SearchResponse = SearchQuery & {
|
||||
results: Message[];
|
||||
};
|
||||
|
||||
type SearchFunction = (query: SearchQuery) => Promise<SearchResponse>;
|
||||
|
||||
export interface SearchableMessageStorage extends MessageStorage {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import {PackageInfo} from "./index";
|
||||
import Client from "../../client";
|
||||
import Chan from "../../models/chan";
|
||||
import Msg, {MessageType, UserInMessage} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default class PublicClient {
|
||||
private client: Client;
|
||||
|
@ -35,8 +36,11 @@ export default class PublicClient {
|
|||
* @param {String} event - Name of the event, must be something the browser will recognise
|
||||
* @param {Object} data - Body of the event, can be anything, but will need to be properly interpreted by the client
|
||||
*/
|
||||
sendToBrowser(event: string, data) {
|
||||
this.client.emit(event, data);
|
||||
// FIXME: this is utterly bonkers
|
||||
// This needs to get wrapped into its own, typed plugin event
|
||||
// Plus it is completely insane to let a plugin inject arbitrary events like that
|
||||
sendToBrowser(event: string, data: any) {
|
||||
this.client.emit(event as any, data);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -61,7 +65,8 @@ export default class PublicClient {
|
|||
text: text,
|
||||
from: {
|
||||
nick: this.packageInfo.name || this.packageInfo.packageName,
|
||||
} as UserInMessage,
|
||||
mode: "",
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
286
server/server.ts
286
server/server.ts
|
@ -3,7 +3,7 @@ import {Server as wsServer} from "ws";
|
|||
import express, {NextFunction, Request, Response} from "express";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import {Server, Socket} from "socket.io";
|
||||
import {Server as ioServer, Socket as ioSocket} from "socket.io";
|
||||
import dns from "dns";
|
||||
import colors from "chalk";
|
||||
import net from "net";
|
||||
|
@ -13,25 +13,32 @@ import Client from "./client";
|
|||
import ClientManager from "./clientManager";
|
||||
import Uploader from "./plugins/uploader";
|
||||
import Helper from "./helper";
|
||||
import Config, {ConfigType, Defaults} from "./config";
|
||||
import Config, {ConfigType} from "./config";
|
||||
import Identification from "./identification";
|
||||
import changelog from "./plugins/changelog";
|
||||
import inputs from "./plugins/inputs";
|
||||
import Auth from "./plugins/auth";
|
||||
|
||||
import themes, {ThemeForClient} from "./plugins/packages/themes";
|
||||
import themes from "./plugins/packages/themes";
|
||||
themes.loadLocalThemes();
|
||||
|
||||
import packages from "./plugins/packages/index";
|
||||
import {NetworkWithIrcFramework} from "./models/network";
|
||||
import {ChanType} from "./models/chan";
|
||||
import Utils from "./command-line/utils";
|
||||
import type {
|
||||
ClientToServerEvents,
|
||||
ServerToClientEvents,
|
||||
InterServerEvents,
|
||||
SocketData,
|
||||
} from "./types/socket-events";
|
||||
AuthPerformData,
|
||||
} from "../shared/types/socket-events";
|
||||
import {ChanType} from "../shared/types/chan";
|
||||
import {
|
||||
LockedSharedConfiguration,
|
||||
SharedConfiguration,
|
||||
ConfigNetDefaults,
|
||||
LockedConfigNetDefaults,
|
||||
} from "../shared/types/config";
|
||||
|
||||
type ServerOptions = {
|
||||
dev: boolean;
|
||||
|
@ -45,21 +52,13 @@ type IndexTemplateConfiguration = ServerConfiguration & {
|
|||
cacheBust: string;
|
||||
};
|
||||
|
||||
export type ClientConfiguration = Pick<
|
||||
ConfigType,
|
||||
"public" | "lockNetwork" | "useHexIp" | "prefetch" | "defaults"
|
||||
> & {
|
||||
fileUpload: boolean;
|
||||
ldapEnabled: boolean;
|
||||
isUpdateAvailable: boolean;
|
||||
applicationServerKey: string;
|
||||
version: string;
|
||||
gitCommit: string | null;
|
||||
defaultTheme: string;
|
||||
themes: ThemeForClient[];
|
||||
defaults: Defaults;
|
||||
fileUploadMaxFileSize?: number;
|
||||
};
|
||||
type Socket = ioSocket<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>;
|
||||
export type Server = ioServer<
|
||||
ClientToServerEvents,
|
||||
ServerToClientEvents,
|
||||
InterServerEvents,
|
||||
SocketData
|
||||
>;
|
||||
|
||||
// A random number that will force clients to reload the page if it differs
|
||||
const serverHash = Math.floor(Date.now() * Math.random());
|
||||
|
@ -219,12 +218,7 @@ export default async function (
|
|||
return;
|
||||
}
|
||||
|
||||
const sockets = new Server<
|
||||
ClientToServerEvents,
|
||||
ServerToClientEvents,
|
||||
InterServerEvents,
|
||||
SocketData
|
||||
>(server, {
|
||||
const sockets: Server = new ioServer(server, {
|
||||
wsEngine: wsServer,
|
||||
cookie: false,
|
||||
serveClient: false,
|
||||
|
@ -330,7 +324,7 @@ export default async function (
|
|||
return server;
|
||||
}
|
||||
|
||||
function getClientLanguage(socket: Socket): string | null {
|
||||
function getClientLanguage(socket: Socket): string | undefined {
|
||||
const acceptLanguage = socket.handshake.headers["accept-language"];
|
||||
|
||||
if (typeof acceptLanguage === "string" && /^[\x00-\x7F]{1,50}$/.test(acceptLanguage)) {
|
||||
|
@ -338,10 +332,10 @@ function getClientLanguage(socket: Socket): string | null {
|
|||
return acceptLanguage;
|
||||
}
|
||||
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getClientIp(socket: Socket) {
|
||||
function getClientIp(socket: Socket): string {
|
||||
let ip = socket.handshake.address || "127.0.0.1";
|
||||
|
||||
if (Config.values.reverseProxy) {
|
||||
|
@ -367,12 +361,12 @@ function getClientSecure(socket: Socket) {
|
|||
return secure;
|
||||
}
|
||||
|
||||
function allRequests(req: Request, res: Response, next: NextFunction) {
|
||||
function allRequests(_req: Request, res: Response, next: NextFunction) {
|
||||
res.setHeader("X-Content-Type-Options", "nosniff");
|
||||
return next();
|
||||
}
|
||||
|
||||
function addSecurityHeaders(req: Request, res: Response, next: NextFunction) {
|
||||
function addSecurityHeaders(_req: Request, res: Response, next: NextFunction) {
|
||||
const policies = [
|
||||
"default-src 'none'", // default to nothing
|
||||
"base-uri 'none'", // disallow <base>, has no fallback to default-src
|
||||
|
@ -402,32 +396,30 @@ function addSecurityHeaders(req: Request, res: Response, next: NextFunction) {
|
|||
return next();
|
||||
}
|
||||
|
||||
function forceNoCacheRequest(req: Request, res: Response, next: NextFunction) {
|
||||
function forceNoCacheRequest(_req: Request, res: Response, next: NextFunction) {
|
||||
// Intermittent proxies must not cache the following requests,
|
||||
// browsers must fetch the latest version of these files (service worker, source maps)
|
||||
res.setHeader("Cache-Control", "no-cache, no-transform");
|
||||
return next();
|
||||
}
|
||||
|
||||
function indexRequest(req: Request, res: Response) {
|
||||
function indexRequest(_req: Request, res: Response) {
|
||||
res.setHeader("Content-Type", "text/html");
|
||||
|
||||
return fs.readFile(
|
||||
Utils.getFileFromRelativeToRoot("client/index.html.tpl"),
|
||||
"utf-8",
|
||||
(err, file) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
const config: IndexTemplateConfiguration = {
|
||||
...getServerConfiguration(),
|
||||
...{cacheBust: Helper.getVersionCacheBust()},
|
||||
};
|
||||
|
||||
res.send(_.template(file)(config));
|
||||
fs.readFile(Utils.getFileFromRelativeToRoot("client/index.html.tpl"), "utf-8", (err, file) => {
|
||||
if (err) {
|
||||
log.error(`failed to server index request: ${err.name}, ${err.message}`);
|
||||
res.sendStatus(500);
|
||||
return;
|
||||
}
|
||||
);
|
||||
|
||||
const config: IndexTemplateConfiguration = {
|
||||
...getServerConfiguration(),
|
||||
...{cacheBust: Helper.getVersionCacheBust()},
|
||||
};
|
||||
|
||||
res.send(_.template(file)(config));
|
||||
});
|
||||
}
|
||||
|
||||
function initializeClient(
|
||||
|
@ -552,18 +544,10 @@ function initializeClient(
|
|||
const hash = Helper.password.hash(p1);
|
||||
|
||||
client.setPassword(hash, (success: boolean) => {
|
||||
const obj = {success: false, error: undefined} as {
|
||||
success: boolean;
|
||||
error: string | undefined;
|
||||
};
|
||||
|
||||
if (success) {
|
||||
obj.success = true;
|
||||
} else {
|
||||
obj.error = "update_failed";
|
||||
}
|
||||
|
||||
socket.emit("change-password", obj);
|
||||
socket.emit("change-password", {
|
||||
success: success,
|
||||
error: success ? undefined : "update_failed",
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
|
@ -577,10 +561,28 @@ function initializeClient(
|
|||
client.open(socket.id, data);
|
||||
});
|
||||
|
||||
socket.on("sort", (data) => {
|
||||
if (_.isPlainObject(data)) {
|
||||
client.sort(data);
|
||||
socket.on("sort:networks", (data) => {
|
||||
if (!_.isPlainObject(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(data.order)) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.sortNetworks(data.order);
|
||||
});
|
||||
|
||||
socket.on("sort:channels", (data) => {
|
||||
if (!_.isPlainObject(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(data.order) || typeof data.network !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
client.sortChannels(data.network, data.order);
|
||||
});
|
||||
|
||||
socket.on("names", (data) => {
|
||||
|
@ -630,13 +632,13 @@ function initializeClient(
|
|||
return;
|
||||
}
|
||||
|
||||
const message = networkAndChan.chan.findMessage(data.msgId);
|
||||
const message = data.msgId ? networkAndChan.chan.findMessage(data.msgId) : null;
|
||||
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
const preview = message.findPreview(data.link);
|
||||
const preview = data.link ? message.findPreview(data.link) : null;
|
||||
|
||||
if (preview) {
|
||||
preview.shown = newState;
|
||||
|
@ -828,9 +830,9 @@ function initializeClient(
|
|||
});
|
||||
|
||||
// socket.join is a promise depending on the adapter.
|
||||
void socket.join(client.id?.toString());
|
||||
void socket.join(client.id);
|
||||
|
||||
const sendInitEvent = (tokenToSend: string | null) => {
|
||||
const sendInitEvent = (tokenToSend?: string) => {
|
||||
socket.emit("init", {
|
||||
active: openChannel,
|
||||
networks: client.networks.map((network) =>
|
||||
|
@ -842,7 +844,7 @@ function initializeClient(
|
|||
};
|
||||
|
||||
if (Config.values.public) {
|
||||
sendInitEvent(null);
|
||||
sendInitEvent();
|
||||
} else if (!token) {
|
||||
client.generateToken((newToken) => {
|
||||
token = client.calculateTokenHash(newToken);
|
||||
|
@ -853,73 +855,108 @@ function initializeClient(
|
|||
});
|
||||
} else {
|
||||
client.updateSession(token, getClientIp(socket), socket.request);
|
||||
sendInitEvent(null);
|
||||
sendInitEvent();
|
||||
}
|
||||
}
|
||||
|
||||
function getClientConfiguration(): ClientConfiguration {
|
||||
const config = _.pick(Config.values, [
|
||||
"public",
|
||||
"lockNetwork",
|
||||
"useHexIp",
|
||||
"prefetch",
|
||||
]) as ClientConfiguration;
|
||||
function getClientConfiguration(): SharedConfiguration | LockedSharedConfiguration {
|
||||
const common = {
|
||||
fileUpload: Config.values.fileUpload.enable,
|
||||
ldapEnabled: Config.values.ldap.enable,
|
||||
isUpdateAvailable: changelog.isUpdateAvailable,
|
||||
applicationServerKey: manager!.webPush.vapidKeys!.publicKey,
|
||||
version: Helper.getVersionNumber(),
|
||||
gitCommit: Helper.getGitCommit(),
|
||||
themes: themes.getAll(),
|
||||
defaultTheme: Config.values.theme,
|
||||
public: Config.values.public,
|
||||
useHexIp: Config.values.useHexIp,
|
||||
prefetch: Config.values.prefetch,
|
||||
fileUploadMaxFileSize: Uploader ? Uploader.getMaxFileSize() : undefined, // TODO can't be undefined?
|
||||
};
|
||||
|
||||
config.fileUpload = Config.values.fileUpload.enable;
|
||||
config.ldapEnabled = Config.values.ldap.enable;
|
||||
const defaultsOverride = {
|
||||
nick: Config.getDefaultNick(), // expand the number part
|
||||
|
||||
if (!config.lockNetwork) {
|
||||
config.defaults = _.clone(Config.values.defaults);
|
||||
} else {
|
||||
// Only send defaults that are visible on the client
|
||||
config.defaults = _.pick(Config.values.defaults, [
|
||||
"name",
|
||||
"nick",
|
||||
"username",
|
||||
"password",
|
||||
"realname",
|
||||
"join",
|
||||
]) as Defaults;
|
||||
// TODO: this doesn't seem right, if the client needs this as a buffer
|
||||
// the client ought to add it on its own
|
||||
sasl: "",
|
||||
saslAccount: "",
|
||||
saslPassword: "",
|
||||
};
|
||||
|
||||
if (!Config.values.lockNetwork) {
|
||||
const defaults: ConfigNetDefaults = {
|
||||
..._.clone(Config.values.defaults),
|
||||
...defaultsOverride,
|
||||
};
|
||||
const result: SharedConfiguration = {
|
||||
...common,
|
||||
defaults: defaults,
|
||||
lockNetwork: Config.values.lockNetwork,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
config.isUpdateAvailable = changelog.isUpdateAvailable;
|
||||
config.applicationServerKey = manager!.webPush.vapidKeys!.publicKey;
|
||||
config.version = Helper.getVersionNumber();
|
||||
config.gitCommit = Helper.getGitCommit();
|
||||
config.themes = themes.getAll();
|
||||
config.defaultTheme = Config.values.theme;
|
||||
config.defaults.nick = Config.getDefaultNick();
|
||||
config.defaults.sasl = "";
|
||||
config.defaults.saslAccount = "";
|
||||
config.defaults.saslPassword = "";
|
||||
// Only send defaults that are visible on the client
|
||||
const defaults: LockedConfigNetDefaults = {
|
||||
..._.pick(Config.values.defaults, ["name", "username", "password", "realname", "join"]),
|
||||
...defaultsOverride,
|
||||
};
|
||||
|
||||
if (Uploader) {
|
||||
config.fileUploadMaxFileSize = Uploader.getMaxFileSize();
|
||||
}
|
||||
const result: LockedSharedConfiguration = {
|
||||
...common,
|
||||
lockNetwork: Config.values.lockNetwork,
|
||||
defaults: defaults,
|
||||
};
|
||||
|
||||
return config;
|
||||
return result;
|
||||
}
|
||||
|
||||
function getServerConfiguration(): ServerConfiguration {
|
||||
return {...Config.values, ...{stylesheets: packages.getStylesheets()}};
|
||||
}
|
||||
|
||||
function performAuthentication(this: Socket, data) {
|
||||
function performAuthentication(this: Socket, data: AuthPerformData) {
|
||||
if (!_.isPlainObject(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const socket = this;
|
||||
let client;
|
||||
let client: Client | undefined;
|
||||
let token: string;
|
||||
|
||||
const finalInit = () =>
|
||||
initializeClient(socket, client, token, data.lastMessage || -1, data.openChannel);
|
||||
const finalInit = () => {
|
||||
let lastMessage = -1;
|
||||
|
||||
if (data && "lastMessage" in data && data.lastMessage) {
|
||||
lastMessage = data.lastMessage;
|
||||
}
|
||||
|
||||
// TODO: bonkers, but for now good enough until we rewrite the logic properly
|
||||
// initializeClient will check for if(openChannel) and as 0 is falsey it does the fallback...
|
||||
let openChannel = 0;
|
||||
|
||||
if (data && "openChannel" in data && data.openChannel) {
|
||||
openChannel = data.openChannel;
|
||||
}
|
||||
|
||||
// TODO: remove this once the logic is cleaned up
|
||||
if (!client) {
|
||||
throw new Error("finalInit called with undefined client, this is a bug");
|
||||
}
|
||||
|
||||
initializeClient(socket, client, token, lastMessage, openChannel);
|
||||
};
|
||||
|
||||
const initClient = () => {
|
||||
if (!client) {
|
||||
throw new Error("initClient called with undefined client");
|
||||
}
|
||||
|
||||
// Configuration does not change during runtime of TL,
|
||||
// and the client listens to this event only once
|
||||
if (!data.hasConfig) {
|
||||
if (data && (!("hasConfig" in data) || !data.hasConfig)) {
|
||||
socket.emit("configuration", getClientConfiguration());
|
||||
|
||||
socket.emit(
|
||||
|
@ -928,8 +965,10 @@ function performAuthentication(this: Socket, data) {
|
|||
);
|
||||
}
|
||||
|
||||
const clientIP = getClientIp(socket);
|
||||
|
||||
client.config.browser = {
|
||||
ip: getClientIp(socket),
|
||||
ip: clientIP,
|
||||
isSecure: getClientSecure(socket),
|
||||
language: getClientLanguage(socket),
|
||||
};
|
||||
|
@ -939,8 +978,9 @@ function performAuthentication(this: Socket, data) {
|
|||
return finalInit();
|
||||
}
|
||||
|
||||
reverseDnsLookup(client.config.browser?.ip, (hostname) => {
|
||||
client.config.browser!.hostname = hostname;
|
||||
const cb_client = client; // ensure that TS figures out that client can't be nil
|
||||
reverseDnsLookup(clientIP, (hostname) => {
|
||||
cb_client.config.browser!.hostname = hostname;
|
||||
|
||||
finalInit();
|
||||
});
|
||||
|
@ -951,9 +991,10 @@ function performAuthentication(this: Socket, data) {
|
|||
client.connect();
|
||||
manager!.clients.push(client);
|
||||
|
||||
const cb_client = client; // ensure TS can see we never have a nil client
|
||||
socket.on("disconnect", function () {
|
||||
manager!.clients = _.without(manager!.clients, client);
|
||||
client.quit();
|
||||
manager!.clients = _.without(manager!.clients, cb_client);
|
||||
cb_client.quit();
|
||||
});
|
||||
|
||||
initClient();
|
||||
|
@ -965,7 +1006,7 @@ function performAuthentication(this: Socket, data) {
|
|||
return;
|
||||
}
|
||||
|
||||
const authCallback = (success) => {
|
||||
const authCallback = (success: boolean) => {
|
||||
// Authorization failed
|
||||
if (!success) {
|
||||
if (!client) {
|
||||
|
@ -990,6 +1031,10 @@ function performAuthentication(this: Socket, data) {
|
|||
// load it and find the user again (this happens with LDAP)
|
||||
if (!client) {
|
||||
client = manager!.loadUser(data.user);
|
||||
|
||||
if (!client) {
|
||||
throw new Error(`authCallback: ${data.user} not found after second lookup`);
|
||||
}
|
||||
}
|
||||
|
||||
initClient();
|
||||
|
@ -998,16 +1043,23 @@ function performAuthentication(this: Socket, data) {
|
|||
client = manager!.findClient(data.user);
|
||||
|
||||
// We have found an existing user and client has provided a token
|
||||
if (client && data.token) {
|
||||
if (client && "token" in data && data.token) {
|
||||
const providedToken = client.calculateTokenHash(data.token);
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(client.config.sessions, providedToken)) {
|
||||
token = providedToken;
|
||||
|
||||
return authCallback(true);
|
||||
authCallback(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!("user" in data && "password" in data)) {
|
||||
log.warn("performAuthentication: callback data has no user or no password");
|
||||
authCallback(false);
|
||||
return;
|
||||
}
|
||||
|
||||
Auth.initialize().then(() => {
|
||||
// Perform password checking
|
||||
Auth.auth(manager, client, data.user, data.password, authCallback);
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import SqliteMessageStorage from "./plugins/messageStorage/sqlite";
|
||||
import {MessageType} from "./models/msg";
|
||||
import Config from "./config";
|
||||
import {DeletionRequest} from "./plugins/messageStorage/types";
|
||||
import log from "./log";
|
||||
import {MessageType} from "../shared/types/msg";
|
||||
|
||||
const status_types = [
|
||||
MessageType.AWAY,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"extends": "../tsconfig.base.json" /* Path to base configuration file to inherit from. Requires TypeScript version 2.1 or later. */,
|
||||
"include": [
|
||||
"**/*",
|
||||
".",
|
||||
"../shared/"
|
||||
] /* Specifies a list of glob patterns that match files to be included in compilation. If no 'files' or 'include' property is present in a tsconfig.json, the compiler defaults to including all files in the containing directory and subdirectories except those specified by 'exclude'. Requires TypeScript version 2.0 or later. */,
|
||||
"files": [
|
||||
|
|
1
server/types/index.d.ts
vendored
1
server/types/index.d.ts
vendored
|
@ -1,2 +1 @@
|
|||
import "./modules";
|
||||
import "./socket-events";
|
||||
|
|
9
server/types/modules/irc-framework.d.ts
vendored
9
server/types/modules/irc-framework.d.ts
vendored
|
@ -33,8 +33,7 @@ declare module "irc-framework" {
|
|||
reply: (message: string) => void;
|
||||
tags: {[key: string]: string};
|
||||
target: string;
|
||||
time?: any;
|
||||
type: "privmsg" | "action" | "notice" | "wallops";
|
||||
time?: number;
|
||||
}
|
||||
export interface JoinEventArgs {
|
||||
account: boolean;
|
||||
|
@ -117,7 +116,11 @@ declare module "irc-framework" {
|
|||
isEnabled: (cap: string) => boolean;
|
||||
enabled: string[];
|
||||
};
|
||||
extractTargetGroup: (target: string) => any;
|
||||
extractTargetGroup: (target: string) => {
|
||||
target: string;
|
||||
target_group: string;
|
||||
};
|
||||
|
||||
supports(feature: "MODES"): string;
|
||||
supports(feature: string): boolean;
|
||||
};
|
||||
|
|
224
server/types/socket-events.d.ts
vendored
224
server/types/socket-events.d.ts
vendored
|
@ -1,224 +0,0 @@
|
|||
import {ClientMessage, ClientNetwork, InitClientChan} from "../../client/js/types";
|
||||
import {Mention} from "../client";
|
||||
import {ChanState} from "../models/chan";
|
||||
import Msg from "../models/msg";
|
||||
import Network from "../models/network";
|
||||
import User from "../models/user";
|
||||
import {ChangelogData} from "../plugins/changelog";
|
||||
import {LinkPreview} from "../plugins/irc-events/link";
|
||||
import {ClientConfiguration} from "../server";
|
||||
|
||||
type Session = {
|
||||
current: boolean;
|
||||
active: number;
|
||||
lastUse: number;
|
||||
ip: string;
|
||||
agent: string;
|
||||
token: string;
|
||||
};
|
||||
|
||||
interface ServerToClientEvents {
|
||||
"auth:failed": () => void;
|
||||
"auth:start": (serverHash: number) => void;
|
||||
"auth:success": () => void;
|
||||
|
||||
"upload:auth": (token: string) => void;
|
||||
|
||||
changelog: (data: ChangelogData) => void;
|
||||
"changelog:newversion": () => void;
|
||||
|
||||
"channel:state": (data: {chan: number; state: ChanState}) => void;
|
||||
|
||||
"change-password": ({success, error}: {success: boolean; error?: any}) => void;
|
||||
|
||||
commands: (data: string[]) => void;
|
||||
|
||||
configuration: (config: ClientConfiguration) => void;
|
||||
|
||||
"push:issubscribed": (isSubscribed: boolean) => void;
|
||||
"push:unregister": () => void;
|
||||
|
||||
"sessions:list": (data: Session[]) => void;
|
||||
|
||||
"mentions:list": (data: Mention[]) => void;
|
||||
|
||||
"setting:new": ({name: string, value: any}) => void;
|
||||
"setting:all": (settings: {[key: string]: any}) => void;
|
||||
|
||||
"history:clear": ({target}: {target: number}) => void;
|
||||
|
||||
"mute:changed": (response: {target: number; status: boolean}) => void;
|
||||
|
||||
names: (data: {id: number; users: User[]}) => void;
|
||||
|
||||
network: (data: {networks: ClientNetwork[]}) => void;
|
||||
"network:options": (data: {network: string; serverOptions: {[key: string]: any}}) => void;
|
||||
"network:status": (data: {network: string; connected: boolean; secure: boolean}) => void;
|
||||
"network:info": (data: {uuid: string}) => void;
|
||||
"network:name": (data: {uuid: string; name: string}) => void;
|
||||
|
||||
nick: (data: {network: string; nick: string}) => void;
|
||||
|
||||
open: (id: number) => void;
|
||||
|
||||
part: (data: {chan: number}) => void;
|
||||
|
||||
"sign-out": () => void;
|
||||
|
||||
sync_sort: (
|
||||
data:
|
||||
| {
|
||||
type: "networks";
|
||||
order: string[];
|
||||
target: string;
|
||||
}
|
||||
| {
|
||||
type: "channels";
|
||||
order: number[];
|
||||
target: string;
|
||||
}
|
||||
) => void;
|
||||
|
||||
topic: (data: {chan: number; topic: string}) => void;
|
||||
|
||||
users: (data: {chan: number}) => void;
|
||||
|
||||
more: ({
|
||||
chan,
|
||||
messages,
|
||||
totalMessages,
|
||||
}: {
|
||||
chan: number;
|
||||
messages: Msg[];
|
||||
totalMessages: number;
|
||||
}) => void;
|
||||
|
||||
"msg:preview": ({id, chan, preview}: {id: number; chan: number; preview: LinkPreview}) => void;
|
||||
"msg:special": (data: {chan: number; data?: Record<string, any>}) => void;
|
||||
msg: (data: {msg: ClientMessage; chan: number; highlight?: number; unread?: number}) => void;
|
||||
|
||||
init: ({
|
||||
active,
|
||||
networks,
|
||||
token,
|
||||
}: {
|
||||
active: number;
|
||||
networks: ClientNetwork[];
|
||||
token: string;
|
||||
}) => void;
|
||||
|
||||
"search:results": (response: SearchResponse) => void;
|
||||
|
||||
quit: (args: {network: string}) => void;
|
||||
|
||||
error: (error: any) => void;
|
||||
connecting: () => void;
|
||||
|
||||
join: (args: {
|
||||
shouldOpen: boolean;
|
||||
index: number;
|
||||
network: string;
|
||||
chan: InitClientChan;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
interface ClientToServerEvents {
|
||||
"auth:perform":
|
||||
| (({user, password}: {user: string; password: string}) => void)
|
||||
| (({
|
||||
user,
|
||||
token,
|
||||
lastMessage,
|
||||
openChannel,
|
||||
hasConfig,
|
||||
}: {
|
||||
user: string;
|
||||
token: string;
|
||||
lastMessage: number;
|
||||
openChannel: number | null;
|
||||
hasConfig: boolean;
|
||||
}) => void);
|
||||
|
||||
changelog: () => void;
|
||||
|
||||
"change-password": ({
|
||||
old_password: string,
|
||||
new_password: string,
|
||||
verify_password: string,
|
||||
}) => void;
|
||||
|
||||
open: (channelId: number) => void;
|
||||
|
||||
names: ({target: number}) => void;
|
||||
|
||||
input: ({target, text}: {target: number; text: string}) => void;
|
||||
|
||||
"upload:auth": () => void;
|
||||
"upload:ping": (token: string) => void;
|
||||
|
||||
"mute:change": (response: {target: number; setMutedTo: boolean}) => void;
|
||||
|
||||
"push:register": (subscriptionJson: PushSubscriptionJSON) => void;
|
||||
"push:unregister": () => void;
|
||||
|
||||
"setting:get": () => void;
|
||||
"setting:set": ({name: string, value: any}) => void;
|
||||
|
||||
"sessions:get": () => void;
|
||||
|
||||
sort: ({type, order}: {type: string; order: any; target?: string}) => void;
|
||||
|
||||
"mentions:dismiss": (msgId: number) => void;
|
||||
"mentions:dismiss_all": () => void;
|
||||
"mentions:get": () => void;
|
||||
|
||||
more: ({
|
||||
target,
|
||||
lastId,
|
||||
condensed,
|
||||
}: {
|
||||
target: number;
|
||||
lastId: number;
|
||||
condensed: boolean;
|
||||
}) => void;
|
||||
|
||||
"msg:preview:toggle": ({
|
||||
target,
|
||||
messageIds,
|
||||
msgId,
|
||||
shown,
|
||||
link,
|
||||
}: {
|
||||
target: number;
|
||||
messageIds?: number[];
|
||||
msgId?: number;
|
||||
shown?: boolean | null;
|
||||
link?: string;
|
||||
}) => void;
|
||||
|
||||
"network:get": (uuid: string) => void;
|
||||
"network:edit": (data: Record<string, any>) => void;
|
||||
"network:new": (data: Record<string, any>) => void;
|
||||
|
||||
"sign-out": (token?: string) => void;
|
||||
|
||||
"history:clear": ({target}: {target: number}) => void;
|
||||
|
||||
search: ({
|
||||
networkUuid,
|
||||
channelName,
|
||||
searchTerm,
|
||||
offset,
|
||||
}: {
|
||||
networkUuid?: string;
|
||||
channelName?: string;
|
||||
searchTerm?: string;
|
||||
offset: number;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface InterServerEvents {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface SocketData {}
|
42
shared/types/chan.ts
Normal file
42
shared/types/chan.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import {SharedMsg} from "./msg";
|
||||
import {SharedUser} from "./user";
|
||||
import {SharedNetworkChan} from "./network";
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
export type SharedChan = {
|
||||
// TODO: don't force existence, figure out how to make TS infer it.
|
||||
id: number;
|
||||
messages: SharedMsg[];
|
||||
name: string;
|
||||
key: string;
|
||||
topic: string;
|
||||
firstUnread: number;
|
||||
unread: number;
|
||||
highlight: number;
|
||||
muted: boolean;
|
||||
type: ChanType;
|
||||
state: ChanState;
|
||||
|
||||
special?: SpecialChanType;
|
||||
data?: any;
|
||||
closed?: boolean;
|
||||
num_users?: number;
|
||||
};
|
15
shared/types/changelog.ts
Normal file
15
shared/types/changelog.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
export type SharedChangelogData = {
|
||||
current: {
|
||||
prerelease: boolean;
|
||||
version: string;
|
||||
changelog?: string;
|
||||
url: string;
|
||||
};
|
||||
expiresAt: number;
|
||||
latest?: {
|
||||
prerelease: boolean;
|
||||
version: string;
|
||||
url: string;
|
||||
};
|
||||
packages?: boolean;
|
||||
};
|
50
shared/types/config.ts
Normal file
50
shared/types/config.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
export type ConfigTheme = {
|
||||
displayName: string;
|
||||
name: string;
|
||||
themeColor: string | null;
|
||||
};
|
||||
type SharedConfigurationBase = {
|
||||
public: boolean;
|
||||
useHexIp: boolean;
|
||||
prefetch: boolean;
|
||||
fileUpload: boolean;
|
||||
ldapEnabled: boolean;
|
||||
isUpdateAvailable: boolean;
|
||||
applicationServerKey: string;
|
||||
version: string;
|
||||
gitCommit: string | null;
|
||||
themes: ConfigTheme[];
|
||||
defaultTheme: string;
|
||||
fileUploadMaxFileSize?: number;
|
||||
};
|
||||
|
||||
export type ConfigNetDefaults = {
|
||||
name: string;
|
||||
host: string;
|
||||
port: number;
|
||||
password: string;
|
||||
tls: boolean;
|
||||
rejectUnauthorized: boolean;
|
||||
nick: string;
|
||||
username: string;
|
||||
realname: string;
|
||||
join: string;
|
||||
leaveMessage: string;
|
||||
sasl: string;
|
||||
saslAccount: string;
|
||||
saslPassword: string;
|
||||
};
|
||||
export type LockedConfigNetDefaults = Pick<
|
||||
ConfigNetDefaults,
|
||||
"name" | "nick" | "username" | "password" | "realname" | "join"
|
||||
>;
|
||||
|
||||
export type LockedSharedConfiguration = SharedConfigurationBase & {
|
||||
lockNetwork: true;
|
||||
defaults: LockedConfigNetDefaults;
|
||||
};
|
||||
|
||||
export type SharedConfiguration = SharedConfigurationBase & {
|
||||
lockNetwork: false;
|
||||
defaults: ConfigNetDefaults;
|
||||
};
|
10
shared/types/mention.ts
Normal file
10
shared/types/mention.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import {MessageType, UserInMessage} from "./msg";
|
||||
|
||||
export type SharedMention = {
|
||||
chanId: number;
|
||||
msgId: number;
|
||||
type: MessageType;
|
||||
time: Date;
|
||||
text: string;
|
||||
from: UserInMessage;
|
||||
};
|
100
shared/types/msg.ts
Normal file
100
shared/types/msg.ts
Normal file
|
@ -0,0 +1,100 @@
|
|||
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",
|
||||
}
|
||||
|
||||
export type SharedUser = {
|
||||
modes: string[];
|
||||
// Users in the channel have only one mode assigned
|
||||
mode: string;
|
||||
away: string;
|
||||
nick: string;
|
||||
lastMessage: number;
|
||||
};
|
||||
|
||||
export type UserInMessage = Partial<SharedUser> & {
|
||||
mode: string;
|
||||
};
|
||||
|
||||
export type LinkPreview = {
|
||||
type: string;
|
||||
head: string;
|
||||
body: string;
|
||||
thumb: string;
|
||||
size: number;
|
||||
link: string; // Send original matched link to the client
|
||||
shown?: boolean | null;
|
||||
error?: string;
|
||||
message?: string;
|
||||
|
||||
media?: string;
|
||||
mediaType?: string;
|
||||
maxSize?: number;
|
||||
thumbActualUrl?: string;
|
||||
};
|
||||
|
||||
export type SharedMsg = {
|
||||
from?: UserInMessage;
|
||||
id: number;
|
||||
previews?: LinkPreview[];
|
||||
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;
|
||||
ctcpMessage?: string;
|
||||
command?: string;
|
||||
invitedYou?: boolean;
|
||||
gecos?: string;
|
||||
account?: boolean;
|
||||
|
||||
// these are all just for error:
|
||||
error?: string;
|
||||
nick?: string;
|
||||
channel?: string;
|
||||
reason?: string;
|
||||
|
||||
raw_modes?: any;
|
||||
when?: Date;
|
||||
whois?: any;
|
||||
|
||||
users: string[];
|
||||
|
||||
statusmsgGroup?: string;
|
||||
params?: string[];
|
||||
};
|
36
shared/types/network.ts
Normal file
36
shared/types/network.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import {SharedChan} from "./chan";
|
||||
|
||||
export type SharedPrefixObject = {
|
||||
symbol: string;
|
||||
mode: string;
|
||||
};
|
||||
|
||||
export type SharedNetworkChan = SharedChan & {
|
||||
totalMessages: number;
|
||||
};
|
||||
|
||||
export type SharedPrefix = {
|
||||
prefix: SharedPrefixObject[];
|
||||
modeToSymbol: {[mode: string]: string};
|
||||
symbols: string[];
|
||||
};
|
||||
|
||||
export type SharedServerOptions = {
|
||||
CHANTYPES: string[];
|
||||
PREFIX: SharedPrefix;
|
||||
NETWORK: string;
|
||||
};
|
||||
|
||||
export type SharedNetworkStatus = {
|
||||
connected: boolean;
|
||||
secure: boolean;
|
||||
};
|
||||
|
||||
export type SharedNetwork = {
|
||||
uuid: string;
|
||||
name: string;
|
||||
nick: string;
|
||||
serverOptions: SharedServerOptions;
|
||||
status: SharedNetworkStatus;
|
||||
channels: SharedNetworkChan[];
|
||||
};
|
181
shared/types/socket-events.d.ts
vendored
Normal file
181
shared/types/socket-events.d.ts
vendored
Normal file
|
@ -0,0 +1,181 @@
|
|||
import {SharedMention} from "./mention";
|
||||
import {ChanState, SharedChan} from "./chan";
|
||||
import {SharedNetwork, SharedServerOptions} from "./network";
|
||||
import {SharedMsg, LinkPreview} from "./msg";
|
||||
import {SharedUser} from "./user";
|
||||
import {SharedChangelogData} from "./changelog";
|
||||
import {SharedConfiguration, LockedSharedConfiguration} from "./config";
|
||||
import {SearchResponse, SearchQuery} from "./storage";
|
||||
|
||||
type Session = {
|
||||
current: boolean;
|
||||
active: number;
|
||||
lastUse: number;
|
||||
ip: string;
|
||||
agent: string;
|
||||
token: string;
|
||||
};
|
||||
|
||||
type EventHandler<T> = (data: T) => void;
|
||||
type NoPayloadEventHandler = EventHandler<void>;
|
||||
|
||||
interface ServerToClientEvents {
|
||||
"auth:start": (serverHash: number) => void;
|
||||
"auth:failed": NoPayloadEventHandler;
|
||||
"auth:success": NoPayloadEventHandler;
|
||||
|
||||
"upload:auth": (token: string) => void;
|
||||
|
||||
changelog: EventHandler<SharedChangelogData>;
|
||||
"changelog:newversion": NoPayloadEventHandler;
|
||||
|
||||
"channel:state": EventHandler<{chan: number; state: ChanState}>;
|
||||
|
||||
"change-password": EventHandler<{success: boolean; error?: any}>;
|
||||
|
||||
commands: EventHandler<string[]>;
|
||||
|
||||
configuration: EventHandler<SharedConfiguration | LockedSharedConfiguration>;
|
||||
|
||||
"push:issubscribed": EventHandler<boolean>;
|
||||
"push:unregister": NoPayloadEventHandler;
|
||||
|
||||
"sessions:list": EventHandler<Session[]>;
|
||||
|
||||
"mentions:list": EventHandler<SharedMention[]>;
|
||||
|
||||
"setting:new": EventHandler<{name: string; value: any}>;
|
||||
"setting:all": EventHandler<{[key: string]: any}>;
|
||||
|
||||
"history:clear": EventHandler<{target: number}>;
|
||||
|
||||
"mute:changed": EventHandler<{target: number; status: boolean}>;
|
||||
|
||||
names: EventHandler<{id: number; users: SharedUser[]}>;
|
||||
|
||||
network: EventHandler<{network: SharedNetwork}>;
|
||||
"network:options": EventHandler<{network: string; serverOptions: SharedServerOptions}>;
|
||||
"network:status": EventHandler<{network: string; connected: boolean; secure: boolean}>;
|
||||
"network:info": EventHandler<{uuid: string}>;
|
||||
"network:name": EventHandler<{uuid: string; name: string}>;
|
||||
|
||||
nick: EventHandler<{network: string; nick: string}>;
|
||||
|
||||
open: (id: number) => void;
|
||||
|
||||
part: EventHandler<{chan: number}>;
|
||||
|
||||
"sign-out": NoPayloadEventHandler;
|
||||
|
||||
"sync_sort:networks": EventHandler<{order: SharedNetwork["uuid"][]}>;
|
||||
"sync_sort:channels": EventHandler<{
|
||||
network: SharedNetwork["uuid"];
|
||||
order: SharedChan["id"][];
|
||||
}>;
|
||||
|
||||
topic: EventHandler<{chan: number; topic: string}>;
|
||||
|
||||
users: EventHandler<{chan: number}>;
|
||||
|
||||
more: EventHandler<{chan: number; messages: SharedMsg[]; totalMessages: number}>;
|
||||
|
||||
"msg:preview": EventHandler<{id: number; chan: number; preview: LinkPreview}>;
|
||||
"msg:special": EventHandler<{chan: number; data?: Record<string, any>}>;
|
||||
msg: EventHandler<{msg: SharedMsg; chan: number; highlight?: number; unread?: number}>;
|
||||
|
||||
init: EventHandler<{active: number; networks: SharedNetwork[]; token?: string}>;
|
||||
|
||||
"search:results": (response: SearchResponse) => void;
|
||||
|
||||
quit: EventHandler<{network: string}>;
|
||||
|
||||
error: (error: any) => void;
|
||||
|
||||
connecting: NoPayloadEventHandler;
|
||||
|
||||
join: EventHandler<{
|
||||
shouldOpen: boolean;
|
||||
index: number;
|
||||
network: string;
|
||||
chan: SharedNetworkChan;
|
||||
}>;
|
||||
}
|
||||
|
||||
type AuthPerformData =
|
||||
| Record<string, never> // funny way of saying an empty object
|
||||
| {user: string; password: string}
|
||||
| {
|
||||
user: string;
|
||||
token: string;
|
||||
lastMessage: number;
|
||||
openChannel: number | null;
|
||||
hasConfig: boolean;
|
||||
};
|
||||
|
||||
interface ClientToServerEvents {
|
||||
"auth:perform": EventHandler<AuthPerformData>;
|
||||
|
||||
changelog: NoPayloadEventHandler;
|
||||
|
||||
"change-password": EventHandler<{
|
||||
old_password: string;
|
||||
new_password: string;
|
||||
verify_password: string;
|
||||
}>;
|
||||
|
||||
open: (channelId: number) => void;
|
||||
|
||||
names: EventHandler<{target: number}>;
|
||||
|
||||
input: EventHandler<{target: number; text: string}>;
|
||||
|
||||
"upload:auth": NoPayloadEventHandler;
|
||||
"upload:ping": (token: string) => void;
|
||||
|
||||
"mute:change": EventHandler<{target: number; setMutedTo: boolean}>;
|
||||
|
||||
"push:register": EventHandler<PushSubscriptionJSON>;
|
||||
"push:unregister": NoPayloadEventHandler;
|
||||
|
||||
"setting:get": NoPayloadEventHandler;
|
||||
"setting:set": EventHandler<{name: string; value: any}>;
|
||||
|
||||
"sessions:get": NoPayloadEventHandler;
|
||||
|
||||
"sort:networks": EventHandler<{order: SharedNetwork["uuid"][]}>;
|
||||
"sort:channels": EventHandler<{
|
||||
network: SharedNetwork["uuid"];
|
||||
order: SharedChan["id"][];
|
||||
}>;
|
||||
|
||||
"mentions:dismiss": (msgId: number) => void;
|
||||
"mentions:dismiss_all": NoPayloadEventHandler;
|
||||
"mentions:get": NoPayloadEventHandler;
|
||||
|
||||
more: EventHandler<{target: number; lastId: number; condensed: boolean}>;
|
||||
|
||||
"msg:preview:toggle": EventHandler<{
|
||||
target: number;
|
||||
messageIds?: number[];
|
||||
msgId?: number;
|
||||
shown?: boolean | null;
|
||||
link?: string;
|
||||
}>;
|
||||
|
||||
"network:get": (uuid: string) => void;
|
||||
// TODO typing
|
||||
"network:edit": (data: Record<string, any>) => void;
|
||||
"network:new": (data: Record<string, any>) => void;
|
||||
|
||||
"sign-out": (token?: string) => void;
|
||||
|
||||
"history:clear": EventHandler<{target: number}>;
|
||||
|
||||
search: EventHandler<SearchQuery>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface InterServerEvents {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface SocketData {}
|
12
shared/types/storage.ts
Normal file
12
shared/types/storage.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import {SharedMsg} from "./msg";
|
||||
|
||||
export type SearchQuery = {
|
||||
searchTerm: string;
|
||||
networkUuid: string;
|
||||
channelName: string;
|
||||
offset: number;
|
||||
};
|
||||
|
||||
export type SearchResponse = SearchQuery & {
|
||||
results: SharedMsg[];
|
||||
};
|
8
shared/types/user.ts
Normal file
8
shared/types/user.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export type SharedUser = {
|
||||
modes: string[];
|
||||
// Users in the channel have only one mode assigned
|
||||
mode: string;
|
||||
away: string;
|
||||
nick: string;
|
||||
lastMessage: number;
|
||||
};
|
|
@ -1,6 +1,7 @@
|
|||
import {expect} from "chai";
|
||||
import {NetworkConfig} from "../server/models/network";
|
||||
import {ChanConfig, ChanType} from "../server/models/chan";
|
||||
import {ChanConfig} from "../server/models/chan";
|
||||
import {ChanType} from "../shared/types/chan";
|
||||
import ClientManager from "../server/clientManager";
|
||||
import Client from "../server/client";
|
||||
import log from "../server/log";
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
// @ts-nocheck TODO re-enable
|
||||
import {expect} from "chai";
|
||||
import Client from "../../server/client";
|
||||
|
||||
import Chan, {ChanType} from "../../server/models/chan";
|
||||
import Chan from "../../server/models/chan";
|
||||
import {ChanType} from "../../shared/types/chan";
|
||||
import ModeCommand from "../../server/plugins/inputs/mode";
|
||||
|
||||
describe("Commands", function () {
|
||||
|
@ -59,12 +57,16 @@ describe("Commands", function () {
|
|||
},
|
||||
});
|
||||
|
||||
it("should not mess with the given target", function (this: CommandContext) {
|
||||
function modeCommandInputCall(net, chan, cmd, args) {
|
||||
ModeCommand.input.call({} as any, net as any, chan, cmd, Array.from(args));
|
||||
}
|
||||
|
||||
it("should not mess with the given target", function () {
|
||||
const test = function (expected: string, args: string[]) {
|
||||
ModeCommand.input(testableNetwork, channel, "mode", Array.from(args));
|
||||
modeCommandInputCall(testableNetwork, channel, "mode", Array.from(args));
|
||||
expect(testableNetwork.lastCommand).to.equal(expected);
|
||||
|
||||
ModeCommand.input(testableNetwork, lobby, "mode", Array.from(args));
|
||||
modeCommandInputCall(testableNetwork, lobby, "mode", args);
|
||||
expect(testableNetwork.lastCommand).to.equal(expected);
|
||||
};
|
||||
|
||||
|
@ -77,51 +79,51 @@ describe("Commands", function () {
|
|||
});
|
||||
|
||||
it("should assume target if none given", function () {
|
||||
ModeCommand.input(testableNetwork, channel, "mode", []);
|
||||
modeCommandInputCall(testableNetwork, channel, "mode", []);
|
||||
expect(testableNetwork.lastCommand).to.equal("MODE #thelounge");
|
||||
|
||||
ModeCommand.input(testableNetwork, lobby, "mode", []);
|
||||
modeCommandInputCall(testableNetwork, lobby, "mode", []);
|
||||
expect(testableNetwork.lastCommand).to.equal("MODE xPaw");
|
||||
|
||||
ModeCommand.input(testableNetwork, channel, "mode", ["+b"]);
|
||||
modeCommandInputCall(testableNetwork, channel, "mode", ["+b"]);
|
||||
expect(testableNetwork.lastCommand).to.equal("MODE #thelounge +b");
|
||||
|
||||
ModeCommand.input(testableNetwork, lobby, "mode", ["+b"]);
|
||||
modeCommandInputCall(testableNetwork, lobby, "mode", ["+b"]);
|
||||
expect(testableNetwork.lastCommand).to.equal("MODE xPaw +b");
|
||||
|
||||
ModeCommand.input(testableNetwork, channel, "mode", ["-o", "hey"]);
|
||||
modeCommandInputCall(testableNetwork, channel, "mode", ["-o", "hey"]);
|
||||
expect(testableNetwork.lastCommand).to.equal("MODE #thelounge -o hey");
|
||||
|
||||
ModeCommand.input(testableNetwork, lobby, "mode", ["-i", "idk"]);
|
||||
modeCommandInputCall(testableNetwork, lobby, "mode", ["-i", "idk"]);
|
||||
expect(testableNetwork.lastCommand).to.equal("MODE xPaw -i idk");
|
||||
});
|
||||
|
||||
it("should support shorthand commands", function () {
|
||||
ModeCommand.input(testableNetwork, channel, "op", ["xPaw"]);
|
||||
modeCommandInputCall(testableNetwork, channel, "op", ["xPaw"]);
|
||||
expect(testableNetwork.lastCommand).to.equal("MODE #thelounge +o xPaw");
|
||||
|
||||
ModeCommand.input(testableNetwork, channel, "deop", ["xPaw"]);
|
||||
modeCommandInputCall(testableNetwork, channel, "deop", ["xPaw"]);
|
||||
expect(testableNetwork.lastCommand).to.equal("MODE #thelounge -o xPaw");
|
||||
|
||||
ModeCommand.input(testableNetwork, channel, "hop", ["xPaw"]);
|
||||
modeCommandInputCall(testableNetwork, channel, "hop", ["xPaw"]);
|
||||
expect(testableNetwork.lastCommand).to.equal("MODE #thelounge +h xPaw");
|
||||
|
||||
ModeCommand.input(testableNetwork, channel, "dehop", ["xPaw"]);
|
||||
modeCommandInputCall(testableNetwork, channel, "dehop", ["xPaw"]);
|
||||
expect(testableNetwork.lastCommand).to.equal("MODE #thelounge -h xPaw");
|
||||
|
||||
ModeCommand.input(testableNetwork, channel, "voice", ["xPaw"]);
|
||||
modeCommandInputCall(testableNetwork, channel, "voice", ["xPaw"]);
|
||||
expect(testableNetwork.lastCommand).to.equal("MODE #thelounge +v xPaw");
|
||||
|
||||
ModeCommand.input(testableNetwork, channel, "devoice", ["xPaw"]);
|
||||
modeCommandInputCall(testableNetwork, channel, "devoice", ["xPaw"]);
|
||||
expect(testableNetwork.lastCommand).to.equal("MODE #thelounge -v xPaw");
|
||||
});
|
||||
|
||||
it("should use ISUPPORT MODES on shorthand commands", function () {
|
||||
ModeCommand.input(testableNetwork, channel, "voice", ["xPaw", "Max-P"]);
|
||||
modeCommandInputCall(testableNetwork, channel, "voice", ["xPaw", "Max-P"]);
|
||||
expect(testableNetwork.lastCommand).to.equal("MODE #thelounge +vv xPaw Max-P");
|
||||
|
||||
// since the limit for modes on tests is 4, it should send two commands
|
||||
ModeCommand.input(testableNetwork, channel, "devoice", [
|
||||
modeCommandInputCall(testableNetwork, channel, "devoice", [
|
||||
"xPaw",
|
||||
"Max-P",
|
||||
"hey",
|
||||
|
@ -135,10 +137,10 @@ describe("Commands", function () {
|
|||
});
|
||||
|
||||
it("should fallback to all modes at once for shorthand commands", function () {
|
||||
ModeCommand.input(testableNetworkNoSupports, channel, "voice", ["xPaw"]);
|
||||
modeCommandInputCall(testableNetworkNoSupports, channel, "voice", ["xPaw"]);
|
||||
expect(testableNetworkNoSupports.lastCommand).to.equal("MODE #thelounge +v xPaw");
|
||||
|
||||
ModeCommand.input(testableNetworkNoSupports, channel, "devoice", ["xPaw", "Max-P"]);
|
||||
modeCommandInputCall(testableNetworkNoSupports, channel, "devoice", ["xPaw", "Max-P"]);
|
||||
expect(testableNetworkNoSupports.lastCommand).to.equal(
|
||||
"MODE #thelounge -vv xPaw Max-P"
|
||||
);
|
||||
|
|
|
@ -195,33 +195,28 @@ describe("Chan", function () {
|
|||
});
|
||||
|
||||
describe("#getFilteredClone(lastActiveChannel, lastMessage)", function () {
|
||||
it("should send empty user list", function () {
|
||||
const chan = new Chan();
|
||||
chan.setUser(new User({nick: "test"}));
|
||||
|
||||
expect(chan.getFilteredClone().users).to.be.empty;
|
||||
});
|
||||
|
||||
it("should keep necessary properties", function () {
|
||||
const chan = new Chan();
|
||||
|
||||
expect(chan.getFilteredClone())
|
||||
.to.be.an("object")
|
||||
.that.has.all.keys(
|
||||
"firstUnread",
|
||||
"highlight",
|
||||
"id",
|
||||
"key",
|
||||
"messages",
|
||||
"muted",
|
||||
"totalMessages",
|
||||
"name",
|
||||
"state",
|
||||
"topic",
|
||||
"type",
|
||||
"unread",
|
||||
"users"
|
||||
);
|
||||
expect(chan.getFilteredClone()).to.be.an("object").that.has.all.keys(
|
||||
"firstUnread",
|
||||
"highlight",
|
||||
"id",
|
||||
"key",
|
||||
"messages",
|
||||
"muted",
|
||||
"totalMessages",
|
||||
"name",
|
||||
"state",
|
||||
"topic",
|
||||
"type",
|
||||
"unread",
|
||||
// the following are there in special cases, need to fix the types
|
||||
"num_users",
|
||||
"special",
|
||||
"closed",
|
||||
"data"
|
||||
);
|
||||
});
|
||||
|
||||
it("should send only last message for non active channel", function () {
|
||||
|
|
|
@ -2,7 +2,7 @@ import {expect} from "chai";
|
|||
|
||||
import Msg from "../../server/models/msg";
|
||||
import User from "../../server/models/user";
|
||||
import {LinkPreview} from "../../server/plugins/irc-events/link";
|
||||
import {LinkPreview} from "../../shared/types/msg";
|
||||
|
||||
describe("Msg", function () {
|
||||
["from", "target"].forEach((prop) => {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
import {expect} from "chai";
|
||||
import sinon from "ts-sinon";
|
||||
import Chan, {ChanType} from "../../server/models/chan";
|
||||
import Chan from "../../server/models/chan";
|
||||
import {ChanType} from "../../shared/types/chan";
|
||||
import Msg from "../../server/models/msg";
|
||||
import User from "../../server/models/user";
|
||||
import Network from "../../server/models/network";
|
||||
|
|
|
@ -3,7 +3,8 @@ import path from "path";
|
|||
import {expect} from "chai";
|
||||
import util from "../util";
|
||||
import Config from "../../server/config";
|
||||
import link, {LinkPreview} from "../../server/plugins/irc-events/link";
|
||||
import link from "../../server/plugins/irc-events/link";
|
||||
import {LinkPreview} from "../../shared/types/msg";
|
||||
|
||||
describe("Link plugin", function () {
|
||||
// Increase timeout due to unpredictable I/O on CI services
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import {expect} from "chai";
|
||||
import util from "../util";
|
||||
import Msg, {MessageType} from "../../server/models/msg";
|
||||
import Msg from "../../server/models/msg";
|
||||
import {MessageType} from "../../shared/types/msg";
|
||||
import Config from "../../server/config";
|
||||
import MessageStorage, {
|
||||
currentSchemaVersion,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue