mirror of
https://github.com/thelounge/thelounge.git
synced 2024-06-04 23:02:18 +02: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
|
@ -59,7 +59,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {filter as fuzzyFilter} from "fuzzy";
|
import {filter as fuzzyFilter} from "fuzzy";
|
||||||
import {computed, defineComponent, nextTick, PropType, ref} from "vue";
|
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 type {ClientChan, ClientUser} from "../js/types";
|
||||||
import Username from "./Username.vue";
|
import Username from "./Username.vue";
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ export default defineComponent({
|
||||||
const result = filteredUsers.value;
|
const result = filteredUsers.value;
|
||||||
|
|
||||||
for (const user of result) {
|
for (const user of result) {
|
||||||
const mode = user.original.modes[0] || "";
|
const mode: string = user.original.modes[0] || "";
|
||||||
|
|
||||||
if (!groups[mode]) {
|
if (!groups[mode]) {
|
||||||
groups[mode] = [];
|
groups[mode] = [];
|
||||||
|
|
|
@ -41,9 +41,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import {computed, defineComponent, ref, watch} from "vue";
|
import {computed, defineComponent, ref, watch} from "vue";
|
||||||
import {onBeforeRouteLeave, onBeforeRouteUpdate} from "vue-router";
|
|
||||||
import eventbus from "../js/eventbus";
|
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({
|
export default defineComponent({
|
||||||
name: "ImageViewer",
|
name: "ImageViewer",
|
||||||
|
@ -104,9 +104,9 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
const links = channel.value.messages
|
const links = channel.value.messages
|
||||||
.map((msg) => msg.previews)
|
.map((msg: SharedMsg) => msg.previews)
|
||||||
.flat()
|
.flat()
|
||||||
.filter((preview) => preview.thumb);
|
.filter((preview) => preview && preview.thumb);
|
||||||
|
|
||||||
const currentIndex = links.indexOf(link.value);
|
const currentIndex = links.indexOf(link.value);
|
||||||
|
|
||||||
|
|
|
@ -150,10 +150,14 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
|
|
||||||
const messageComponent = computed(() => {
|
const messageComponent = computed(() => {
|
||||||
return "message-" + props.message.type;
|
return "message-" + (props.message.type || "invalid"); // TODO: force existence of type in sharedmsg
|
||||||
});
|
});
|
||||||
|
|
||||||
const isAction = () => {
|
const isAction = () => {
|
||||||
|
if (!props.message.type) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return typeof MessageTypes["message-" + props.message.type] !== "undefined";
|
return typeof MessageTypes["message-" + props.message.type] !== "undefined";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -79,7 +79,7 @@ import {
|
||||||
} from "vue";
|
} from "vue";
|
||||||
import {useStore} from "../js/store";
|
import {useStore} from "../js/store";
|
||||||
import {ClientChan, ClientMessage, ClientNetwork, ClientLinkPreview} from "../js/types";
|
import {ClientChan, ClientMessage, ClientNetwork, ClientLinkPreview} from "../js/types";
|
||||||
import Msg from "../../server/models/msg";
|
import {SharedMsg} from "../../shared/types/msg";
|
||||||
|
|
||||||
type CondensedMessageContainer = {
|
type CondensedMessageContainer = {
|
||||||
type: "condensed";
|
type: "condensed";
|
||||||
|
@ -242,7 +242,7 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
|
|
||||||
const shouldDisplayDateMarker = (
|
const shouldDisplayDateMarker = (
|
||||||
message: Msg | ClientMessage | CondensedMessageContainer,
|
message: SharedMsg | ClientMessage | CondensedMessageContainer,
|
||||||
id: number
|
id: number
|
||||||
) => {
|
) => {
|
||||||
const previousMessage = condensedMessages.value[id - 1];
|
const previousMessage = condensedMessages.value[id - 1];
|
||||||
|
@ -270,7 +270,7 @@ export default defineComponent({
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isPreviousSource = (currentMessage: ClientMessage | Msg, id: number) => {
|
const isPreviousSource = (currentMessage: ClientMessage | SharedMsg, id: number) => {
|
||||||
const previousMessage = condensedMessages.value[id - 1];
|
const previousMessage = condensedMessages.value[id - 1];
|
||||||
return !!(
|
return !!(
|
||||||
previousMessage &&
|
previousMessage &&
|
||||||
|
|
|
@ -26,36 +26,43 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const errorMessage = computed(() => {
|
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) {
|
switch (props.message.error) {
|
||||||
case "bad_channel_key":
|
case "bad_channel_key":
|
||||||
return `Cannot join ${props.message.channel} - Bad channel key.`;
|
return `Cannot join ${chan} - Bad channel key.`;
|
||||||
case "banned_from_channel":
|
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":
|
case "cannot_send_to_channel":
|
||||||
return `Cannot send to channel ${props.message.channel}`;
|
return `Cannot send to channel ${chan}`;
|
||||||
case "channel_is_full":
|
case "channel_is_full":
|
||||||
return `Cannot join ${props.message.channel} - Channel is full.`;
|
return `Cannot join ${chan} - Channel is full.`;
|
||||||
case "chanop_privs_needed":
|
case "chanop_privs_needed":
|
||||||
return "Cannot perform action: You're not a channel operator.";
|
return "Cannot perform action: You're not a channel operator.";
|
||||||
case "invite_only_channel":
|
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":
|
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":
|
case "not_on_channel":
|
||||||
return "Cannot perform action: You're not on the channel.";
|
return "Cannot perform action: You're not on the channel.";
|
||||||
case "password_mismatch":
|
case "password_mismatch":
|
||||||
return "Password mismatch.";
|
return "Password mismatch.";
|
||||||
case "too_many_channels":
|
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":
|
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":
|
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":
|
case "user_on_channel":
|
||||||
return `User ${props.message.nick} is already on the channel.`;
|
return `User ${nick} is already on the channel.`;
|
||||||
default:
|
default:
|
||||||
if (props.message.reason) {
|
if (props.message.reason) {
|
||||||
return `${props.message.reason} (${props.message.error})`;
|
return `${props.message.reason} (${
|
||||||
|
props.message.error || "!UNDEFINED_ERR"
|
||||||
|
})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return props.message.error;
|
return props.message.error;
|
||||||
|
|
|
@ -498,6 +498,7 @@ export default defineComponent({
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
// eslint-disable-next-line
|
||||||
() => props.defaults?.commands,
|
() => props.defaults?.commands,
|
||||||
() => {
|
() => {
|
||||||
void nextTick(() => {
|
void nextTick(() => {
|
||||||
|
@ -507,6 +508,7 @@ export default defineComponent({
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
// eslint-disable-next-line
|
||||||
() => props.defaults?.tls,
|
() => props.defaults?.tls,
|
||||||
(isSecureChecked) => {
|
(isSecureChecked) => {
|
||||||
const ports = [6667, 6697];
|
const ports = [6667, 6697];
|
||||||
|
|
|
@ -309,8 +309,7 @@ export default defineComponent({
|
||||||
|
|
||||||
moveItemInArray(store.state.networks, oldIndex, newIndex);
|
moveItemInArray(store.state.networks, oldIndex, newIndex);
|
||||||
|
|
||||||
socket.emit("sort", {
|
socket.emit("sort:networks", {
|
||||||
type: "networks",
|
|
||||||
order: store.state.networks.map((n) => n.uuid),
|
order: store.state.networks.map((n) => n.uuid),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -341,9 +340,8 @@ export default defineComponent({
|
||||||
|
|
||||||
moveItemInArray(netChan.network.channels, oldIndex, newIndex);
|
moveItemInArray(netChan.network.channels, oldIndex, newIndex);
|
||||||
|
|
||||||
socket.emit("sort", {
|
socket.emit("sort:channel", {
|
||||||
type: "channels",
|
network: netChan.network.uuid,
|
||||||
target: netChan.network.uuid,
|
|
||||||
order: netChan.network.channels.map((c) => c.id),
|
order: netChan.network.channels.map((c) => c.id),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,10 +12,10 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {computed, defineComponent, PropType} from "vue";
|
import {computed, defineComponent, PropType} from "vue";
|
||||||
import {UserInMessage} from "../../server/models/msg";
|
import {UserInMessage} from "../../shared/types/msg";
|
||||||
import eventbus from "../js/eventbus";
|
import eventbus from "../js/eventbus";
|
||||||
import colorClass from "../js/helpers/colorClass";
|
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";
|
import {useStore} from "../js/store";
|
||||||
|
|
||||||
type UsernameUser = Partial<UserInMessage> & {
|
type UsernameUser = Partial<UserInMessage> & {
|
||||||
|
|
|
@ -106,7 +106,7 @@ import type {ClientMessage} from "../../js/types";
|
||||||
import {useStore} from "../../js/store";
|
import {useStore} from "../../js/store";
|
||||||
import {useRoute, useRouter} from "vue-router";
|
import {useRoute, useRouter} from "vue-router";
|
||||||
import {switchToChannel} from "../../js/router";
|
import {switchToChannel} from "../../js/router";
|
||||||
import {SearchQuery} from "../../../server/plugins/messageStorage/types";
|
import {SearchQuery} from "../../../shared/types/storage";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "SearchResults",
|
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) {
|
for (const message of store.state.activeChannel.channel.messages) {
|
||||||
let toggled = false;
|
let toggled = false;
|
||||||
|
|
||||||
for (const preview of message.previews) {
|
for (const preview of message.previews || []) {
|
||||||
if (preview.shown) {
|
if (preview.shown) {
|
||||||
preview.shown = false;
|
preview.shown = false;
|
||||||
toggled = true;
|
toggled = true;
|
||||||
|
|
|
@ -11,7 +11,7 @@ function input() {
|
||||||
for (const message of store.state.activeChannel.channel.messages) {
|
for (const message of store.state.activeChannel.channel.messages) {
|
||||||
let toggled = false;
|
let toggled = false;
|
||||||
|
|
||||||
for (const preview of message.previews) {
|
for (const preview of message.previews || []) {
|
||||||
if (!preview.shown) {
|
if (!preview.shown) {
|
||||||
preview.shown = true;
|
preview.shown = true;
|
||||||
toggled = true;
|
toggled = true;
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import {nextTick} from "vue";
|
|
||||||
import socket from "../socket";
|
import socket from "../socket";
|
||||||
import storage from "../localStorage";
|
import storage from "../localStorage";
|
||||||
|
import {toClientChan} from "../chan";
|
||||||
import {router, switchToChannel, navigate} from "../router";
|
import {router, switchToChannel, navigate} from "../router";
|
||||||
import {store} from "../store";
|
import {store} from "../store";
|
||||||
import parseIrcUri from "../helpers/parseIrcUri";
|
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) {
|
socket.on("init", async function (data) {
|
||||||
store.commit("networks", mergeNetworkData(data.networks));
|
store.commit("networks", mergeNetworkData(data.networks));
|
||||||
|
@ -31,54 +32,54 @@ socket.on("init", async function (data) {
|
||||||
window.g_TheLoungeRemoveLoading();
|
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
|
// If we are on an unknown route or still on SignIn component
|
||||||
// connect parameters in public mode, then nothing to do here
|
// then we can open last known channel on server, or Connect window if none
|
||||||
if (!handledQuery) {
|
if (!router.currentRoute?.value?.name || router.currentRoute?.value?.name === "SignIn") {
|
||||||
// If we are on an unknown route or still on SignIn component
|
const channel = store.getters.findChannel(data.active);
|
||||||
// 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) {
|
if (channel) {
|
||||||
switchToChannel(channel.channel);
|
switchToChannel(channel.channel);
|
||||||
} else if (store.state.networks.length > 0) {
|
} else if (store.state.networks.length > 0) {
|
||||||
// Server is telling us to open a channel that does not exist
|
// 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
|
// For example, it can be unset if you first open the page after server start
|
||||||
switchToChannel(store.state.networks[0].channels[0]);
|
switchToChannel(store.state.networks[0].channels[0]);
|
||||||
} else {
|
} else {
|
||||||
await navigate("Connect");
|
await navigate("Connect");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function mergeNetworkData(newNetworks: ClientNetwork[]) {
|
function mergeNetworkData(newNetworks: SharedNetwork[]): ClientNetwork[] {
|
||||||
const stored = storage.get("thelounge.networks.collapsed");
|
const stored = storage.get("thelounge.networks.collapsed");
|
||||||
const collapsedNetworks = stored ? new Set(JSON.parse(stored)) : new Set();
|
const collapsedNetworks = stored ? new Set(JSON.parse(stored)) : new Set();
|
||||||
|
const result: ReturnType<typeof mergeNetworkData> = [];
|
||||||
|
|
||||||
for (let n = 0; n < newNetworks.length; n++) {
|
for (const sharedNet of newNetworks) {
|
||||||
const network = newNetworks[n];
|
const currentNetwork = store.getters.findNetwork(sharedNet.uuid);
|
||||||
const currentNetwork = store.getters.findNetwork(network.uuid);
|
|
||||||
|
|
||||||
// If this network is new, set some default variables and initalize channel variables
|
// If this network is new, set some default variables and initalize channel variables
|
||||||
if (!currentNetwork) {
|
if (!currentNetwork) {
|
||||||
network.isJoinChannelShown = false;
|
const newNet: ClientNetwork = {
|
||||||
network.isCollapsed = collapsedNetworks.has(network.uuid);
|
...sharedNet,
|
||||||
network.channels.forEach(store.getters.initChannel);
|
channels: sharedNet.channels.map(toClientChan),
|
||||||
|
isJoinChannelShown: false,
|
||||||
|
isCollapsed: collapsedNetworks.has(sharedNet.uuid),
|
||||||
|
};
|
||||||
|
result.push(newNet);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge received network object into existing network object on the client
|
// Merge received network object into existing network object on the client
|
||||||
// so the object reference stays the same (e.g. for currentChannel state)
|
// so the object reference stays the same (e.g. for currentChannel state)
|
||||||
for (const key in network) {
|
for (const key in sharedNet) {
|
||||||
if (!Object.prototype.hasOwnProperty.call(network, key)) {
|
if (!Object.prototype.hasOwnProperty.call(sharedNet, key)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,81 +87,82 @@ function mergeNetworkData(newNetworks: ClientNetwork[]) {
|
||||||
if (key === "channels") {
|
if (key === "channels") {
|
||||||
currentNetwork.channels = mergeChannelData(
|
currentNetwork.channels = mergeChannelData(
|
||||||
currentNetwork.channels,
|
currentNetwork.channels,
|
||||||
network.channels as InitClientChan[]
|
sharedNet.channels
|
||||||
);
|
);
|
||||||
} else {
|
} 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[]) {
|
function mergeChannelData(
|
||||||
for (let c = 0; c < newChannels.length; c++) {
|
oldChannels: ClientChan[],
|
||||||
const channel = newChannels[c];
|
newChannels: SharedNetworkChan[]
|
||||||
const currentChannel = oldChannels.find((chan) => chan.id === channel.id);
|
): 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) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge received channel object into existing currentChannel
|
// Merge received channel object into existing currentChannel
|
||||||
// so the object references are exactly the same (e.g. in store.state.activeChannel)
|
// 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
|
emitNamesOrMarkUsersOudated(currentChannel); // TODO: this should not carry logic like that
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (currentChannel.messages && newChannel.messages.length < 100) {
|
||||||
// Server sends total count of messages in memory, we compare it to amount of messages
|
currentChannel.messages = currentChannel.messages.concat(newChannel.messages);
|
||||||
// on the client, and decide whether theres more messages to load from server
|
} else {
|
||||||
if (key === "totalMessages") {
|
currentChannel.messages = newChannel.messages;
|
||||||
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];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
async function handleQueryParams() {
|
||||||
|
@ -170,30 +172,28 @@ async function handleQueryParams() {
|
||||||
|
|
||||||
const params = new URLSearchParams(document.location.search);
|
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")) {
|
if (params.has("uri")) {
|
||||||
// Set default connection settings from IRC protocol links
|
// Set default connection settings from IRC protocol links
|
||||||
const uri = params.get("uri");
|
const uri = params.get("uri");
|
||||||
const queryParams = parseIrcUri(String(uri));
|
const queryParams = parseIrcUri(String(uri));
|
||||||
|
removeQueryParams();
|
||||||
cleanParams();
|
|
||||||
await router.push({name: "Connect", query: queryParams});
|
await router.push({name: "Connect", query: queryParams});
|
||||||
|
|
||||||
return true;
|
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
|
// Set default connection settings from url params
|
||||||
const queryParams = Object.fromEntries(params.entries());
|
const queryParams = Object.fromEntries(params.entries());
|
||||||
|
removeQueryParams();
|
||||||
cleanParams();
|
|
||||||
await router.push({name: "Connect", query: queryParams});
|
await router.push({name: "Connect", query: queryParams});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
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 socket from "../socket";
|
||||||
import {store} from "../store";
|
import {store} from "../store";
|
||||||
import {switchToChannel} from "../router";
|
import {switchToChannel} from "../router";
|
||||||
|
import {ClientChan} from "../types";
|
||||||
|
import {toClientChan} from "../chan";
|
||||||
|
|
||||||
socket.on("join", function (data) {
|
socket.on("join", function (data) {
|
||||||
store.getters.initChannel(data.chan);
|
|
||||||
|
|
||||||
const network = store.getters.findNetwork(data.network);
|
const network = store.getters.findNetwork(data.network);
|
||||||
|
|
||||||
if (!network) {
|
if (!network) {
|
||||||
return;
|
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
|
// Queries do not automatically focus, unless the user did a whois
|
||||||
if (data.chan.type === "query" && !data.shouldOpen) {
|
if (data.chan.type === "query" && !data.shouldOpen) {
|
||||||
|
|
|
@ -1,7 +1,17 @@
|
||||||
import socket from "../socket";
|
import socket from "../socket";
|
||||||
import {store} from "../store";
|
import {store} from "../store";
|
||||||
import {ClientMention} from "../types";
|
import {ClientMention} from "../types";
|
||||||
|
import {SharedMention} from "../../../shared/types/mention";
|
||||||
|
|
||||||
socket.on("mentions:list", function (data) {
|
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 socket from "../socket";
|
||||||
import {store} from "../store";
|
import {store} from "../store";
|
||||||
import {ClientMessage} from "../types";
|
import type {ClientChan, ClientMessage} from "../types";
|
||||||
|
|
||||||
socket.on("more", async (data) => {
|
socket.on("more", async (data) => {
|
||||||
const channel = store.getters.findChannel(data.chan)?.channel;
|
const channel = store.getters.findChannel(data.chan)?.channel;
|
||||||
|
@ -14,13 +14,15 @@ socket.on("more", async (data) => {
|
||||||
channel.inputHistory = channel.inputHistory.concat(
|
channel.inputHistory = channel.inputHistory.concat(
|
||||||
data.messages
|
data.messages
|
||||||
.filter((m) => m.self && m.text && m.type === "message")
|
.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()
|
.reverse()
|
||||||
.slice(0, 100 - channel.inputHistory.length)
|
.slice(0, 100 - channel.inputHistory.length)
|
||||||
);
|
);
|
||||||
channel.moreHistoryAvailable =
|
channel.moreHistoryAvailable =
|
||||||
data.totalMessages > channel.messages.length + data.messages.length;
|
data.totalMessages > channel.messages.length + data.messages.length;
|
||||||
channel.messages.unshift(...(data.messages as ClientMessage[]));
|
channel.messages.unshift(...data.messages);
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
channel.historyLoading = false;
|
channel.historyLoading = false;
|
||||||
|
|
|
@ -3,7 +3,8 @@ import socket from "../socket";
|
||||||
import {cleanIrcMessage} from "../../../shared/irc";
|
import {cleanIrcMessage} from "../../../shared/irc";
|
||||||
import {store} from "../store";
|
import {store} from "../store";
|
||||||
import {switchToChannel} from "../router";
|
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;
|
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(
|
function notifyMessage(
|
||||||
targetId: number,
|
targetId: number,
|
||||||
channel: ClientChan,
|
channel: ClientChan,
|
||||||
|
@ -122,12 +131,14 @@ function notifyMessage(
|
||||||
) {
|
) {
|
||||||
let title: string;
|
let title: string;
|
||||||
let body: 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") {
|
if (msg.type === "invite") {
|
||||||
title = "New channel invite:";
|
title = "New channel invite:";
|
||||||
body = msg.from.nick + " invited you to " + msg.channel;
|
body = nick + " invited you to " + msg.channel;
|
||||||
} else {
|
} else {
|
||||||
title = String(msg.from.nick);
|
title = nick;
|
||||||
|
|
||||||
if (channel.type !== "query") {
|
if (channel.type !== "query") {
|
||||||
title += ` (${channel.name})`;
|
title += ` (${channel.name})`;
|
||||||
|
@ -137,7 +148,8 @@ function notifyMessage(
|
||||||
title += " says:";
|
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));
|
const timestamp = Date.parse(String(msg.time));
|
||||||
|
@ -184,24 +196,40 @@ function notifyMessage(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateUserList(channel, msg) {
|
function updateUserList(channel: ClientChan, msg: SharedMsg) {
|
||||||
if (msg.type === "message" || msg.type === "action") {
|
switch (msg.type) {
|
||||||
const user = channel.users.find((u) => u.nick === msg.from.nick);
|
case "message": // fallthrough
|
||||||
|
|
||||||
if (user) {
|
case "action": {
|
||||||
user.lastMessage = new Date(msg.time).getTime() || Date.now();
|
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) {
|
case "quit": // fallthrough
|
||||||
channel.users.splice(idx, 1);
|
|
||||||
|
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) {
|
case "kick": {
|
||||||
channel.users.splice(idx, 1);
|
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 netChan = store.getters.findChannel(data.chan);
|
||||||
const message = netChan?.channel.messages.find((m) => m.id === data.id);
|
const message = netChan?.channel.messages.find((m) => m.id === data.id);
|
||||||
|
|
||||||
if (!message) {
|
if (!message || !message.previews) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
import socket from "../socket";
|
import socket from "../socket";
|
||||||
import {store} from "../store";
|
import {store} from "../store";
|
||||||
import {switchToChannel} from "../router";
|
import {switchToChannel} from "../router";
|
||||||
|
import {toClientChan} from "../chan";
|
||||||
|
import {ClientNetwork} from "../types";
|
||||||
|
import {ChanState} from "../../../shared/types/chan";
|
||||||
|
|
||||||
socket.on("network", function (data) {
|
socket.on("network", function (data) {
|
||||||
const network = data.networks[0];
|
const network: ClientNetwork = {
|
||||||
|
...data.network,
|
||||||
network.isJoinChannelShown = false;
|
channels: data.network.channels.map(toClientChan),
|
||||||
network.isCollapsed = false;
|
isJoinChannelShown: false,
|
||||||
network.channels.forEach(store.getters.initChannel);
|
isCollapsed: false,
|
||||||
|
};
|
||||||
|
|
||||||
store.commit("networks", [...store.state.networks, network]);
|
store.commit("networks", [...store.state.networks, network]);
|
||||||
|
|
||||||
|
@ -19,7 +23,7 @@ socket.on("network:options", function (data) {
|
||||||
const network = store.getters.findNetwork(data.network);
|
const network = store.getters.findNetwork(data.network);
|
||||||
|
|
||||||
if (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) {
|
if (!data.connected) {
|
||||||
network.channels.forEach((channel) => {
|
network.channels.forEach((channel) => {
|
||||||
channel.users = [];
|
channel.users = []; // TODO: untangle this
|
||||||
channel.state = 0;
|
channel.state = ChanState.PARTED;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,30 +1,16 @@
|
||||||
import socket from "../socket";
|
import socket from "../socket";
|
||||||
import {store} from "../store";
|
import {store} from "../store";
|
||||||
|
|
||||||
socket.on("sync_sort", function (data) {
|
socket.on("sync_sort:networks", function (data) {
|
||||||
const order = data.order;
|
store.commit("sortNetworks", (a, b) => data.order.indexOf(a.uuid) - data.order.indexOf(b.uuid));
|
||||||
|
});
|
||||||
switch (data.type) {
|
|
||||||
case "networks":
|
socket.on("sync_sort:channels", function (data) {
|
||||||
store.commit(
|
const network = store.getters.findNetwork(data.network);
|
||||||
"sortNetworks",
|
|
||||||
(a, b) => (order as string[]).indexOf(a.uuid) - (order as string[]).indexOf(b.uuid)
|
if (!network) {
|
||||||
);
|
return;
|
||||||
|
}
|
||||||
break;
|
|
||||||
|
network.channels.sort((a, b) => data.order.indexOf(a.id) - data.order.indexOf(b.id));
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import io, {Socket} from "socket.io-client";
|
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({
|
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io({
|
||||||
transports: JSON.parse(document.body.dataset.transports || "['polling', 'websocket']"),
|
transports: JSON.parse(document.body.dataset.transports || "['polling', 'websocket']"),
|
||||||
|
|
|
@ -3,19 +3,12 @@
|
||||||
import {ActionContext, createStore, Store, useStore as baseUseStore} from "vuex";
|
import {ActionContext, createStore, Store, useStore as baseUseStore} from "vuex";
|
||||||
import {createSettingsStore} from "./store-settings";
|
import {createSettingsStore} from "./store-settings";
|
||||||
import storage from "./localStorage";
|
import storage from "./localStorage";
|
||||||
import type {
|
import type {ClientChan, ClientNetwork, NetChan, ClientMention, ClientMessage} from "./types";
|
||||||
ClientChan,
|
|
||||||
ClientConfiguration,
|
|
||||||
ClientNetwork,
|
|
||||||
InitClientChan,
|
|
||||||
NetChan,
|
|
||||||
ClientMessage,
|
|
||||||
ClientMention,
|
|
||||||
} from "./types";
|
|
||||||
import type {InjectionKey} from "vue";
|
import type {InjectionKey} from "vue";
|
||||||
|
|
||||||
import {SettingsState} from "./settings";
|
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;
|
const appName = document.title;
|
||||||
|
|
||||||
|
@ -59,7 +52,7 @@ export type State = {
|
||||||
mentions: ClientMention[];
|
mentions: ClientMention[];
|
||||||
hasServiceWorker: boolean;
|
hasServiceWorker: boolean;
|
||||||
pushNotificationState: string;
|
pushNotificationState: string;
|
||||||
serverConfiguration: ClientConfiguration | null;
|
serverConfiguration: SharedConfiguration | LockedSharedConfiguration | null;
|
||||||
sessions: ClientSession[];
|
sessions: ClientSession[];
|
||||||
sidebarOpen: boolean;
|
sidebarOpen: boolean;
|
||||||
sidebarDragging: boolean;
|
sidebarDragging: boolean;
|
||||||
|
@ -131,7 +124,6 @@ type Getters = {
|
||||||
findNetwork: (state: State) => (uuid: string) => ClientNetwork | null;
|
findNetwork: (state: State) => (uuid: string) => ClientNetwork | null;
|
||||||
highlightCount(state: State): number;
|
highlightCount(state: State): number;
|
||||||
title(state: State, getters: Omit<Getters, "title">): string;
|
title(state: State, getters: Omit<Getters, "title">): string;
|
||||||
initChannel: () => (channel: InitClientChan) => ClientChan;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// getters without the state argument
|
// getters without the state argument
|
||||||
|
@ -202,31 +194,6 @@ const getters: Getters = {
|
||||||
|
|
||||||
return alertEventCount + channelname + appName;
|
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 = {
|
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 {defineComponent} from "vue";
|
||||||
|
|
||||||
import Chan from "../../server/models/chan";
|
import {SharedChan} from "../../shared/types/chan";
|
||||||
import Network from "../../server/models/network";
|
import {SharedNetwork} from "../../shared/types/network";
|
||||||
import User from "../../server/models/user";
|
import {SharedUser} from "../../shared/types/user";
|
||||||
import Message from "../../server/models/msg";
|
import {SharedMention} from "../../shared/types/mention";
|
||||||
import {Mention} from "../../server/client";
|
import {SharedConfiguration, LockedSharedConfiguration} from "../../shared/types/config";
|
||||||
import {ClientConfiguration} from "../../server/server";
|
import {LinkPreview, SharedMsg} from "../../shared/types/msg";
|
||||||
import {LinkPreview} from "../../server/plugins/irc-events/link";
|
|
||||||
|
|
||||||
interface LoungeWindow extends Window {
|
interface LoungeWindow extends Window {
|
||||||
g_TheLoungeRemoveLoading?: () => void;
|
g_TheLoungeRemoveLoading?: () => void;
|
||||||
|
@ -16,19 +15,15 @@ interface LoungeWindow extends Window {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientUser = User & {
|
type ClientUser = SharedUser;
|
||||||
//
|
|
||||||
};
|
|
||||||
|
|
||||||
type ClientMessage = Omit<Message, "users"> & {
|
// we will eventually need to put client specific fields here
|
||||||
time: number;
|
// which are not shared with the server
|
||||||
users: string[];
|
export type ClientMessage = SharedMsg;
|
||||||
};
|
|
||||||
|
|
||||||
type ClientChan = Omit<Chan, "users" | "messages"> & {
|
type ClientChan = Omit<SharedChan, "messages"> & {
|
||||||
moreHistoryAvailable: boolean;
|
moreHistoryAvailable: boolean;
|
||||||
editTopic: boolean;
|
editTopic: boolean;
|
||||||
users: ClientUser[];
|
|
||||||
messages: ClientMessage[];
|
messages: ClientMessage[];
|
||||||
|
|
||||||
// these are added in store/initChannel
|
// these are added in store/initChannel
|
||||||
|
@ -38,6 +33,8 @@ type ClientChan = Omit<Chan, "users" | "messages"> & {
|
||||||
historyLoading: boolean;
|
historyLoading: boolean;
|
||||||
scrolledToBottom: boolean;
|
scrolledToBottom: boolean;
|
||||||
usersOutdated: boolean;
|
usersOutdated: boolean;
|
||||||
|
|
||||||
|
users: ClientUser[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type InitClientChan = ClientChan & {
|
type InitClientChan = ClientChan & {
|
||||||
|
@ -46,7 +43,7 @@ type InitClientChan = ClientChan & {
|
||||||
};
|
};
|
||||||
|
|
||||||
// We omit channels so we can use ClientChan[] instead of Chan[]
|
// We omit channels so we can use ClientChan[] instead of Chan[]
|
||||||
type ClientNetwork = Omit<Network, "channels"> & {
|
type ClientNetwork = Omit<SharedNetwork, "channels"> & {
|
||||||
isJoinChannelShown: boolean;
|
isJoinChannelShown: boolean;
|
||||||
isCollapsed: boolean;
|
isCollapsed: boolean;
|
||||||
channels: ClientChan[];
|
channels: ClientChan[];
|
||||||
|
@ -57,9 +54,8 @@ type NetChan = {
|
||||||
network: ClientNetwork;
|
network: ClientNetwork;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ClientConfiguration = ClientConfiguration;
|
type ClientMention = SharedMention & {
|
||||||
type ClientMention = Mention & {
|
localetime: string; // TODO: this needs to go the way of the dodo, nothing but a single component uses it
|
||||||
localetime: string;
|
|
||||||
channel: NetChan | null;
|
channel: NetChan | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,9 @@ import App from "../components/App.vue";
|
||||||
import storage from "./localStorage";
|
import storage from "./localStorage";
|
||||||
import {router} from "./router";
|
import {router} from "./router";
|
||||||
import socket from "./socket";
|
import socket from "./socket";
|
||||||
|
import "./socket-events"; // this sets up all socket event listeners, do not remove
|
||||||
import eventbus from "./eventbus";
|
import eventbus from "./eventbus";
|
||||||
|
|
||||||
import "./socket-events";
|
|
||||||
import "./webpush";
|
import "./webpush";
|
||||||
import "./keybinds";
|
import "./keybinds";
|
||||||
import {LoungeWindow} from "./types";
|
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. */,
|
] /* 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": [
|
"files": [
|
||||||
"../package.json",
|
"../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/fullnamemap.json",
|
||||||
"./js/helpers/simplemap.json",
|
"./js/helpers/simplemap.json"
|
||||||
"../webpack.config.ts",
|
|
||||||
"../babel.config.cjs"
|
|
||||||
] /* 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. */,
|
] /* 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": [],
|
// "exclude": [],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
|
115
server/client.ts
115
server/client.ts
|
@ -6,10 +6,12 @@ import crypto from "crypto";
|
||||||
import colors from "chalk";
|
import colors from "chalk";
|
||||||
|
|
||||||
import log from "./log";
|
import log from "./log";
|
||||||
import Chan, {ChanConfig, Channel, ChanType} from "./models/chan";
|
import Chan, {ChanConfig} from "./models/chan";
|
||||||
import Msg, {MessageType, UserInMessage} from "./models/msg";
|
import Msg from "./models/msg";
|
||||||
import Config from "./config";
|
import Config from "./config";
|
||||||
import {condensedTypes} from "../shared/irc";
|
import {condensedTypes} from "../shared/irc";
|
||||||
|
import {MessageType} from "../shared/types/msg";
|
||||||
|
import {SharedMention} from "../shared/types/mention";
|
||||||
|
|
||||||
import inputs from "./plugins/inputs";
|
import inputs from "./plugins/inputs";
|
||||||
import PublicClient from "./plugins/packages/publicClient";
|
import PublicClient from "./plugins/packages/publicClient";
|
||||||
|
@ -17,11 +19,12 @@ import SqliteMessageStorage from "./plugins/messageStorage/sqlite";
|
||||||
import TextFileMessageStorage from "./plugins/messageStorage/text";
|
import TextFileMessageStorage from "./plugins/messageStorage/text";
|
||||||
import Network, {IgnoreListItem, NetworkConfig, NetworkWithIrcFramework} from "./models/network";
|
import Network, {IgnoreListItem, NetworkConfig, NetworkWithIrcFramework} from "./models/network";
|
||||||
import ClientManager from "./clientManager";
|
import ClientManager from "./clientManager";
|
||||||
import {MessageStorage, SearchQuery, SearchResponse} from "./plugins/messageStorage/types";
|
import {MessageStorage} from "./plugins/messageStorage/types";
|
||||||
import {StorageCleaner} from "./storageCleaner";
|
import {StorageCleaner} from "./storageCleaner";
|
||||||
|
import {SearchQuery, SearchResponse} from "../shared/types/storage";
|
||||||
type OrderItem = Chan["id"] | Network["uuid"];
|
import {SharedChan, ChanType} from "../shared/types/chan";
|
||||||
type Order = OrderItem[];
|
import {SharedNetwork} from "../shared/types/network";
|
||||||
|
import {ServerToClientEvents} from "../shared/types/socket-events";
|
||||||
|
|
||||||
const events = [
|
const events = [
|
||||||
"away",
|
"away",
|
||||||
|
@ -82,15 +85,6 @@ export type UserConfig = {
|
||||||
networks?: NetworkConfig[];
|
networks?: NetworkConfig[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Mention = {
|
|
||||||
chanId: number;
|
|
||||||
msgId: number;
|
|
||||||
type: MessageType;
|
|
||||||
time: Date;
|
|
||||||
text: string;
|
|
||||||
from: UserInMessage;
|
|
||||||
};
|
|
||||||
|
|
||||||
class Client {
|
class Client {
|
||||||
awayMessage!: string;
|
awayMessage!: string;
|
||||||
lastActiveChannel!: number;
|
lastActiveChannel!: number;
|
||||||
|
@ -98,12 +92,12 @@ class Client {
|
||||||
[socketId: string]: {token: string; openChannel: number};
|
[socketId: string]: {token: string; openChannel: number};
|
||||||
};
|
};
|
||||||
config!: UserConfig;
|
config!: UserConfig;
|
||||||
id!: number;
|
id: string;
|
||||||
idMsg!: number;
|
idMsg!: number;
|
||||||
idChan!: number;
|
idChan!: number;
|
||||||
name!: string;
|
name!: string;
|
||||||
networks!: Network[];
|
networks!: Network[];
|
||||||
mentions!: Mention[];
|
mentions!: SharedMention[];
|
||||||
manager!: ClientManager;
|
manager!: ClientManager;
|
||||||
messageStorage!: MessageStorage[];
|
messageStorage!: MessageStorage[];
|
||||||
highlightRegex!: RegExp | null;
|
highlightRegex!: RegExp | null;
|
||||||
|
@ -113,12 +107,12 @@ class Client {
|
||||||
fileHash!: string;
|
fileHash!: string;
|
||||||
|
|
||||||
constructor(manager: ClientManager, name?: string, config = {} as UserConfig) {
|
constructor(manager: ClientManager, name?: string, config = {} as UserConfig) {
|
||||||
|
this.id = uuidv4();
|
||||||
_.merge(this, {
|
_.merge(this, {
|
||||||
awayMessage: "",
|
awayMessage: "",
|
||||||
lastActiveChannel: -1,
|
lastActiveChannel: -1,
|
||||||
attachedClients: {},
|
attachedClients: {},
|
||||||
config: config,
|
config: config,
|
||||||
id: uuidv4(),
|
|
||||||
idChan: 1,
|
idChan: 1,
|
||||||
idMsg: 1,
|
idMsg: 1,
|
||||||
name: name,
|
name: name,
|
||||||
|
@ -229,9 +223,12 @@ class Client {
|
||||||
return chan;
|
return chan;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(event: string, data?: any) {
|
emit<Ev extends keyof ServerToClientEvents>(
|
||||||
|
event: Ev,
|
||||||
|
...args: Parameters<ServerToClientEvents[Ev]>
|
||||||
|
) {
|
||||||
if (this.manager !== null) {
|
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.networks.push(network);
|
||||||
client.emit("network", {
|
client.emit("network", {
|
||||||
networks: [network.getFilteredClone(this.lastActiveChannel, -1)],
|
network: network.getFilteredClone(this.lastActiveChannel, -1),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!network.validate(client)) {
|
if (!network.validate(client)) {
|
||||||
|
@ -697,56 +694,39 @@ class Client {
|
||||||
this.emit("open", targetNetChan.chan.id);
|
this.emit("open", targetNetChan.chan.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
sort(data: {order: Order; type: "networks" | "channels"; target: string}) {
|
sortChannels(netid: SharedNetwork["uuid"], order: SharedChan["id"][]) {
|
||||||
const order = data.order;
|
const network = _.find(this.networks, {uuid: netid});
|
||||||
|
|
||||||
if (!_.isArray(order)) {
|
if (!network) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (data.type) {
|
network.channels.sort((a, b) => {
|
||||||
case "networks":
|
// Always sort lobby to the top regardless of what the client has sent
|
||||||
this.networks.sort((a, b) => order.indexOf(a.uuid) - order.indexOf(b.uuid));
|
// Because there's a lot of code that presumes channels[0] is the lobby
|
||||||
|
if (a.type === ChanType.LOBBY) {
|
||||||
// Sync order to connected clients
|
return -1;
|
||||||
this.emit("sync_sort", {
|
} else if (b.type === ChanType.LOBBY) {
|
||||||
order: this.networks.map((obj) => obj.uuid),
|
return 1;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
return order.indexOf(a.id) - order.indexOf(b.id);
|
||||||
|
});
|
||||||
this.save();
|
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}) {
|
names(data: {target: number}) {
|
||||||
|
@ -776,7 +756,7 @@ class Client {
|
||||||
|
|
||||||
quit(signOut?: boolean) {
|
quit(signOut?: boolean) {
|
||||||
const sockets = this.manager.sockets.sockets;
|
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) {
|
if (room) {
|
||||||
for (const user of room) {
|
for (const user of room) {
|
||||||
|
@ -836,12 +816,13 @@ class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: type session to this.attachedClients
|
// TODO: type session to this.attachedClients
|
||||||
registerPushSubscription(session: any, subscription: ClientPushSubscription, noSave = false) {
|
registerPushSubscription(session: any, subscription: PushSubscriptionJSON, noSave = false) {
|
||||||
if (
|
if (
|
||||||
!_.isPlainObject(subscription) ||
|
!_.isPlainObject(subscription) ||
|
||||||
!_.isPlainObject(subscription.keys) ||
|
|
||||||
typeof subscription.endpoint !== "string" ||
|
typeof subscription.endpoint !== "string" ||
|
||||||
!/^https?:\/\//.test(subscription.endpoint) ||
|
!/^https?:\/\//.test(subscription.endpoint) ||
|
||||||
|
!_.isPlainObject(subscription.keys) ||
|
||||||
|
!subscription.keys || // TS compiler doesn't understand isPlainObject
|
||||||
typeof subscription.keys.p256dh !== "string" ||
|
typeof subscription.keys.p256dh !== "string" ||
|
||||||
typeof subscription.keys.auth !== "string"
|
typeof subscription.keys.auth !== "string"
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import Config from "./config";
|
||||||
import {NetworkConfig} from "./models/network";
|
import {NetworkConfig} from "./models/network";
|
||||||
import WebPush from "./plugins/webpush";
|
import WebPush from "./plugins/webpush";
|
||||||
import log from "./log";
|
import log from "./log";
|
||||||
import {Server} from "socket.io";
|
import {Server} from "./server";
|
||||||
|
|
||||||
class ClientManager {
|
class ClientManager {
|
||||||
clients: Client[];
|
clients: Client[];
|
||||||
|
|
|
@ -4,6 +4,7 @@ import fs, {Stats} from "fs";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import colors from "chalk";
|
import colors from "chalk";
|
||||||
|
import {SearchOptions} from "ldapjs";
|
||||||
|
|
||||||
import log from "./log";
|
import log from "./log";
|
||||||
import Helper from "./helper";
|
import Helper from "./helper";
|
||||||
|
@ -44,7 +45,7 @@ export type Defaults = Pick<
|
||||||
| "saslAccount"
|
| "saslAccount"
|
||||||
| "saslPassword"
|
| "saslPassword"
|
||||||
> & {
|
> & {
|
||||||
join?: string;
|
join: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Identd = {
|
type Identd = {
|
||||||
|
@ -57,7 +58,7 @@ type SearchDN = {
|
||||||
rootPassword: string;
|
rootPassword: string;
|
||||||
filter: string;
|
filter: string;
|
||||||
base: string;
|
base: string;
|
||||||
scope: string;
|
scope: SearchOptions["scope"];
|
||||||
};
|
};
|
||||||
|
|
||||||
type Ldap = {
|
type Ldap = {
|
||||||
|
|
|
@ -2,36 +2,14 @@ import _ from "lodash";
|
||||||
import log from "../log";
|
import log from "../log";
|
||||||
import Config from "../config";
|
import Config from "../config";
|
||||||
import User from "./user";
|
import User from "./user";
|
||||||
import Msg, {MessageType} from "./msg";
|
import Msg from "./msg";
|
||||||
import storage from "../plugins/storage";
|
import storage from "../plugins/storage";
|
||||||
import Client from "../client";
|
import Client from "../client";
|
||||||
import Network from "./network";
|
import Network from "./network";
|
||||||
import Prefix from "./prefix";
|
import Prefix from "./prefix";
|
||||||
|
import {MessageType, SharedMsg} from "../../shared/types/msg";
|
||||||
export enum ChanType {
|
import {ChanType, SpecialChanType, ChanState} from "../../shared/types/chan";
|
||||||
CHANNEL = "channel",
|
import {SharedNetworkChan} from "../../shared/types/network";
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ChanConfig = {
|
export type ChanConfig = {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -60,7 +38,6 @@ class Chan {
|
||||||
data?: any;
|
data?: any;
|
||||||
closed?: boolean;
|
closed?: boolean;
|
||||||
num_users?: number;
|
num_users?: number;
|
||||||
static optionalProperties = ["userAway", "special", "data", "closed", "num_users"];
|
|
||||||
|
|
||||||
constructor(attr?: Partial<Chan>) {
|
constructor(attr?: Partial<Chan>) {
|
||||||
_.defaults(this, attr, {
|
_.defaults(this, attr, {
|
||||||
|
@ -84,18 +61,11 @@ class Chan {
|
||||||
}
|
}
|
||||||
|
|
||||||
pushMessage(client: Client, msg: Msg, increasesUnread = false) {
|
pushMessage(client: Client, msg: Msg, increasesUnread = false) {
|
||||||
const chan = this.id;
|
const chanId = this.id;
|
||||||
const obj = {chan, msg} as {
|
|
||||||
chan: number;
|
|
||||||
msg: Msg;
|
|
||||||
unread?: number;
|
|
||||||
highlight?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
msg.id = client.idMsg++;
|
msg.id = client.idMsg++;
|
||||||
|
|
||||||
// If this channel is open in any of the clients, do not increase unread counter
|
// 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) {
|
if (msg.self) {
|
||||||
// reset counters/markers when receiving self-/echo-message
|
// reset counters/markers when receiving self-/echo-message
|
||||||
|
@ -108,15 +78,15 @@ class Chan {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (increasesUnread || msg.highlight) {
|
if (increasesUnread || msg.highlight) {
|
||||||
obj.unread = ++this.unread;
|
this.unread++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.highlight) {
|
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
|
// Never store messages in public mode as the session
|
||||||
// is completely destroyed when the page gets closed
|
// 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) {
|
if (!Config.values.prefetch || !Config.values.prefetchStorage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -160,6 +131,7 @@ class Chan {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getSortedUsers(irc?: Network["irc"]) {
|
getSortedUsers(irc?: Network["irc"]) {
|
||||||
const users = Array.from(this.users.values());
|
const users = Array.from(this.users.values());
|
||||||
|
|
||||||
|
@ -182,21 +154,27 @@ class Chan {
|
||||||
return userModeSortPriority[a.mode] - userModeSortPriority[b.mode];
|
return userModeSortPriority[a.mode] - userModeSortPriority[b.mode];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
findMessage(msgId: number) {
|
findMessage(msgId: number) {
|
||||||
return this.messages.find((message) => message.id === msgId);
|
return this.messages.find((message) => message.id === msgId);
|
||||||
}
|
}
|
||||||
|
|
||||||
findUser(nick: string) {
|
findUser(nick: string) {
|
||||||
return this.users.get(nick.toLowerCase());
|
return this.users.get(nick.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
getUser(nick: string) {
|
getUser(nick: string) {
|
||||||
return this.findUser(nick) || new User({nick}, new Prefix([]));
|
return this.findUser(nick) || new User({nick}, new Prefix([]));
|
||||||
}
|
}
|
||||||
|
|
||||||
setUser(user: User) {
|
setUser(user: User) {
|
||||||
this.users.set(user.nick.toLowerCase(), user);
|
this.users.set(user.nick.toLowerCase(), user);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeUser(user: User) {
|
removeUser(user: User) {
|
||||||
this.users.delete(user.nick.toLowerCase());
|
this.users.delete(user.nick.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a clean clone of this channel that will be sent to the client.
|
* Get a clean clone of this channel that will be sent to the client.
|
||||||
* This function performs manual cloning of channel object for
|
* This function performs manual cloning of channel object for
|
||||||
|
@ -206,38 +184,54 @@ class Chan {
|
||||||
* If true, channel is assumed active.
|
* If true, channel is assumed active.
|
||||||
* @param {int} lastMessage - Last message id seen by active client to avoid sending duplicates.
|
* @param {int} lastMessage - Last message id seen by active client to avoid sending duplicates.
|
||||||
*/
|
*/
|
||||||
getFilteredClone(lastActiveChannel?: number | boolean, lastMessage?: number): FilteredChannel {
|
getFilteredClone(
|
||||||
return Object.keys(this).reduce((newChannel, prop) => {
|
lastActiveChannel?: number | boolean,
|
||||||
if (Chan.optionalProperties.includes(prop)) {
|
lastMessage?: number
|
||||||
if (this[prop] !== undefined || (Array.isArray(this[prop]) && this[prop].length)) {
|
): SharedNetworkChan {
|
||||||
newChannel[prop] = this[prop];
|
let msgs: SharedMsg[];
|
||||||
}
|
|
||||||
} 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;
|
|
||||||
|
|
||||||
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;
|
return {
|
||||||
} else {
|
id: this.id,
|
||||||
newChannel[prop] = this[prop];
|
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;
|
special: this.special,
|
||||||
}, {}) as FilteredChannel;
|
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) {
|
writeUserLog(client: Client, msg: Msg) {
|
||||||
this.messages.push(msg);
|
this.messages.push(msg);
|
||||||
|
|
||||||
|
@ -270,6 +264,7 @@ class Chan {
|
||||||
messageStorage.index(target.network, targetChannel, msg).catch((e) => log.error(e));
|
messageStorage.index(target.network, targetChannel, msg).catch((e) => log.error(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMessages(client: Client, network: Network) {
|
loadMessages(client: Client, network: Network) {
|
||||||
if (!this.isLoggable()) {
|
if (!this.isLoggable()) {
|
||||||
return;
|
return;
|
||||||
|
@ -326,15 +321,23 @@ class Chan {
|
||||||
log.error(`Failed to load messages for ${client.name}: ${err.toString()}`)
|
log.error(`Failed to load messages for ${client.name}: ${err.toString()}`)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoggable() {
|
isLoggable() {
|
||||||
return this.type === ChanType.CHANNEL || this.type === ChanType.QUERY;
|
return this.type === ChanType.CHANNEL || this.type === ChanType.QUERY;
|
||||||
}
|
}
|
||||||
|
|
||||||
setMuteStatus(muted: boolean) {
|
setMuteStatus(muted: boolean) {
|
||||||
this.muted = !!muted;
|
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());
|
network.irc.raw("ZNC", "*playback", "PLAY", channel.name, from.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,41 +1,5 @@
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import {LinkPreview} from "../plugins/irc-events/link";
|
import {MessageType, LinkPreview, UserInMessage} from "../../shared/types/msg";
|
||||||
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",
|
|
||||||
}
|
|
||||||
|
|
||||||
class Msg {
|
class Msg {
|
||||||
from!: UserInMessage;
|
from!: UserInMessage;
|
||||||
|
@ -70,7 +34,7 @@ class Msg {
|
||||||
raw_modes!: any;
|
raw_modes!: any;
|
||||||
when!: Date;
|
when!: Date;
|
||||||
whois!: any;
|
whois!: any;
|
||||||
users!: UserInMessage[] | string[];
|
users!: string[];
|
||||||
statusmsgGroup!: string;
|
statusmsgGroup!: string;
|
||||||
params!: string[];
|
params!: string[];
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,17 @@
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import {v4 as uuidv4} from "uuid";
|
import {v4 as uuidv4} from "uuid";
|
||||||
import IrcFramework, {Client as IRCClient} from "irc-framework";
|
import IrcFramework, {Client as IRCClient} from "irc-framework";
|
||||||
import Chan, {ChanConfig, Channel, ChanType} from "./chan";
|
import Chan, {ChanConfig, Channel} from "./chan";
|
||||||
import Msg, {MessageType} from "./msg";
|
import Msg from "./msg";
|
||||||
import Prefix from "./prefix";
|
import Prefix from "./prefix";
|
||||||
import Helper, {Hostmask} from "../helper";
|
import Helper, {Hostmask} from "../helper";
|
||||||
import Config, {WebIRC} from "../config";
|
import Config, {WebIRC} from "../config";
|
||||||
import STSPolicies from "../plugins/sts";
|
import STSPolicies from "../plugins/sts";
|
||||||
import ClientCertificate, {ClientCertificateType} from "../plugins/clientCertificate";
|
import ClientCertificate, {ClientCertificateType} from "../plugins/clientCertificate";
|
||||||
import Client from "../client";
|
import Client from "../client";
|
||||||
|
import {MessageType} from "../../shared/types/msg";
|
||||||
/**
|
import {ChanType} from "../../shared/types/chan";
|
||||||
* List of keys which should be sent to the client by default.
|
import {SharedNetwork} from "../../shared/types/network";
|
||||||
*/
|
|
||||||
const fieldsForClient = {
|
|
||||||
uuid: true,
|
|
||||||
name: true,
|
|
||||||
nick: true,
|
|
||||||
serverOptions: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
type NetworkIrcOptions = {
|
type NetworkIrcOptions = {
|
||||||
host: string;
|
host: string;
|
||||||
|
@ -52,7 +45,7 @@ type NetworkStatus = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IgnoreListItem = Hostmask & {
|
export type IgnoreListItem = Hostmask & {
|
||||||
when?: number;
|
when: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type IgnoreList = IgnoreListItem[];
|
type IgnoreList = IgnoreListItem[];
|
||||||
|
@ -505,24 +498,17 @@ class Network {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getFilteredClone(lastActiveChannel?: number, lastMessage?: number) {
|
getFilteredClone(lastActiveChannel?: number, lastMessage?: number): SharedNetwork {
|
||||||
const filteredNetwork = Object.keys(this).reduce((newNetwork, prop) => {
|
return {
|
||||||
if (prop === "channels") {
|
uuid: this.uuid,
|
||||||
// Channels objects perform their own cloning
|
name: this.name,
|
||||||
newNetwork[prop] = this[prop].map((channel) =>
|
nick: this.nick,
|
||||||
channel.getFilteredClone(lastActiveChannel, lastMessage)
|
serverOptions: this.serverOptions,
|
||||||
);
|
status: this.getNetworkStatus(),
|
||||||
} else if (fieldsForClient[prop]) {
|
channels: this.channels.map((channel) =>
|
||||||
// Some properties that are not useful for the client are skipped
|
channel.getFilteredClone(lastActiveChannel, lastMessage)
|
||||||
newNetwork[prop] = this[prop];
|
),
|
||||||
}
|
};
|
||||||
|
|
||||||
return newNetwork;
|
|
||||||
}, {}) as Network;
|
|
||||||
|
|
||||||
filteredNetwork.status = this.getNetworkStatus();
|
|
||||||
|
|
||||||
return filteredNetwork;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getNetworkStatus() {
|
getNetworkStatus() {
|
||||||
|
|
|
@ -67,11 +67,11 @@ function advancedLdapAuth(user: string, password: string, callback: (success: bo
|
||||||
});
|
});
|
||||||
|
|
||||||
const base = config.ldap.searchDN.base;
|
const base = config.ldap.searchDN.base;
|
||||||
const searchOptions = {
|
const searchOptions: SearchOptions = {
|
||||||
scope: config.ldap.searchDN.scope,
|
scope: config.ldap.searchDN.scope,
|
||||||
filter: `(&(${config.ldap.primaryKey}=${userDN})${config.ldap.searchDN.filter})`,
|
filter: `(&(${config.ldap.primaryKey}=${userDN})${config.ldap.searchDN.filter})`,
|
||||||
attributes: ["dn"],
|
attributes: ["dn"],
|
||||||
} as SearchOptions;
|
};
|
||||||
|
|
||||||
ldapclient.on("error", function (err: Error) {
|
ldapclient.on("error", function (err: Error) {
|
||||||
log.error(`Unable to connect to LDAP server: ${err.toString()}`);
|
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 remainingUsers = new Set(users);
|
||||||
|
|
||||||
const searchOptions = {
|
const searchOptions: SearchOptions = {
|
||||||
scope: config.ldap.searchDN.scope,
|
scope: config.ldap.searchDN.scope,
|
||||||
filter: `${config.ldap.searchDN.filter}`,
|
filter: `${config.ldap.searchDN.filter}`,
|
||||||
attributes: [config.ldap.primaryKey],
|
attributes: [config.ldap.primaryKey],
|
||||||
paged: true,
|
paged: true,
|
||||||
} as SearchOptions;
|
};
|
||||||
|
|
||||||
ldapclient.search(base, searchOptions, function (err2, res) {
|
ldapclient.search(base, searchOptions, function (err2, res) {
|
||||||
if (err2) {
|
if (err2) {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import log from "../log";
|
||||||
import pkg from "../../package.json";
|
import pkg from "../../package.json";
|
||||||
import ClientManager from "../clientManager";
|
import ClientManager from "../clientManager";
|
||||||
import Config from "../config";
|
import Config from "../config";
|
||||||
|
import {SharedChangelogData} from "../../shared/types/changelog";
|
||||||
|
|
||||||
const TIME_TO_LIVE = 15 * 60 * 1000; // 15 minutes, in milliseconds
|
const TIME_TO_LIVE = 15 * 60 * 1000; // 15 minutes, in milliseconds
|
||||||
|
|
||||||
|
@ -12,31 +13,17 @@ export default {
|
||||||
fetch,
|
fetch,
|
||||||
checkForUpdates,
|
checkForUpdates,
|
||||||
};
|
};
|
||||||
export type ChangelogData = {
|
const versions: SharedChangelogData = {
|
||||||
current: {
|
|
||||||
prerelease: boolean;
|
|
||||||
version: string;
|
|
||||||
changelog?: string;
|
|
||||||
url: string;
|
|
||||||
};
|
|
||||||
expiresAt: number;
|
|
||||||
latest?: {
|
|
||||||
prerelease: boolean;
|
|
||||||
version: string;
|
|
||||||
url: string;
|
|
||||||
};
|
|
||||||
packages?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const versions = {
|
|
||||||
current: {
|
current: {
|
||||||
|
prerelease: false,
|
||||||
version: `v${pkg.version}`,
|
version: `v${pkg.version}`,
|
||||||
changelog: undefined,
|
changelog: undefined,
|
||||||
|
url: "", // TODO: properly init
|
||||||
},
|
},
|
||||||
expiresAt: -1,
|
expiresAt: -1,
|
||||||
latest: undefined,
|
latest: undefined,
|
||||||
packages: undefined,
|
packages: undefined,
|
||||||
} as ChangelogData;
|
};
|
||||||
|
|
||||||
async function fetch() {
|
async function fetch() {
|
||||||
const time = Date.now();
|
const time = Date.now();
|
||||||
|
|
|
@ -31,7 +31,7 @@ function get(uuid: string): ClientCertificateType | null {
|
||||||
return {
|
return {
|
||||||
private_key: fs.readFileSync(paths.privateKeyPath, "utf-8"),
|
private_key: fs.readFileSync(paths.privateKeyPath, "utf-8"),
|
||||||
certificate: fs.readFileSync(paths.certificatePath, "utf-8"),
|
certificate: fs.readFileSync(paths.certificatePath, "utf-8"),
|
||||||
} as ClientCertificateType;
|
};
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
log.error("Unable to get certificate", e);
|
log.error("Unable to get certificate", e);
|
||||||
}
|
}
|
||||||
|
@ -122,10 +122,10 @@ function generate() {
|
||||||
// Sign this certificate with a SHA256 signature
|
// Sign this certificate with a SHA256 signature
|
||||||
cert.sign(keys.privateKey, md.sha256.create());
|
cert.sign(keys.privateKey, md.sha256.create());
|
||||||
|
|
||||||
const pem = {
|
const pem: ClientCertificateType = {
|
||||||
private_key: pki.privateKeyToPem(keys.privateKey),
|
private_key: pki.privateKeyToPem(keys.privateKey),
|
||||||
certificate: pki.certificateToPem(cert),
|
certificate: pki.certificateToPem(cert),
|
||||||
} as ClientCertificateType;
|
};
|
||||||
|
|
||||||
return pem;
|
return pem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {PluginInputHandler} from "./index";
|
import {PluginInputHandler} from "./index";
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import {ChanType} from "../../models/chan";
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
import {ChanType} from "../../../shared/types/chan";
|
||||||
|
|
||||||
const commands = ["slap", "me"];
|
const commands = ["slap", "me"];
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {ChanType} from "../../models/chan";
|
import Msg from "../../models/msg";
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
|
||||||
import {PluginInputHandler} from "./index";
|
import {PluginInputHandler} from "./index";
|
||||||
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
import {ChanType} from "../../../shared/types/chan";
|
||||||
|
|
||||||
const commands = ["ban", "unban", "banlist", "kickban"];
|
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 {PluginInputHandler} from "./index";
|
||||||
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
|
||||||
const commands = ["connect", "server"];
|
const commands = ["connect", "server"];
|
||||||
const allowDisconnected = true;
|
const allowDisconnected = true;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import {PluginInputHandler} from "./index";
|
import {PluginInputHandler} from "./index";
|
||||||
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
|
||||||
const commands = ["ctcp"];
|
const commands = ["ctcp"];
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,14 @@
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import Helper from "../../helper";
|
import Helper from "../../helper";
|
||||||
import {PluginInputHandler} from "./index";
|
import {PluginInputHandler} from "./index";
|
||||||
import {IgnoreListItem} from "../../models/network";
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
import {ChanType, SpecialChanType} from "../../models/chan";
|
|
||||||
|
|
||||||
const commands = ["ignore", "unignore", "ignorelist"];
|
const commands = ["ignore", "unignore"];
|
||||||
|
|
||||||
const input: PluginInputHandler = function (network, chan, cmd, args) {
|
const input: PluginInputHandler = function (network, chan, cmd, args) {
|
||||||
const client = this;
|
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(
|
chan.pushMessage(
|
||||||
client,
|
client,
|
||||||
new Msg({
|
new Msg({
|
||||||
|
@ -24,16 +20,13 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmd !== "ignorelist") {
|
const target = args[0].trim();
|
||||||
// Trim to remove any spaces from the hostmask
|
const hostmask = Helper.parseHostmask(target);
|
||||||
target = args[0].trim();
|
|
||||||
hostmask = Helper.parseHostmask(target) as IgnoreListItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (cmd) {
|
switch (cmd) {
|
||||||
case "ignore": {
|
case "ignore": {
|
||||||
// IRC nicks are case insensitive
|
// IRC nicks are case insensitive
|
||||||
if (hostmask!.nick.toLowerCase() === network.nick.toLowerCase()) {
|
if (hostmask.nick.toLowerCase() === network.nick.toLowerCase()) {
|
||||||
chan.pushMessage(
|
chan.pushMessage(
|
||||||
client,
|
client,
|
||||||
new Msg({
|
new Msg({
|
||||||
|
@ -41,25 +34,14 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
||||||
text: "You can't ignore yourself",
|
text: "You can't ignore yourself",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else if (
|
return;
|
||||||
!network.ignoreList.some(function (entry) {
|
}
|
||||||
return Helper.compareHostmask(entry, hostmask!);
|
|
||||||
|
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(
|
chan.pushMessage(
|
||||||
client,
|
client,
|
||||||
new Msg({
|
new Msg({
|
||||||
|
@ -67,32 +49,31 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
||||||
text: "The specified user/hostmask is already ignored",
|
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": {
|
case "unignore": {
|
||||||
const idx = network.ignoreList.findIndex(function (entry) {
|
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
|
if (idx === -1) {
|
||||||
// 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 {
|
|
||||||
chan.pushMessage(
|
chan.pushMessage(
|
||||||
client,
|
client,
|
||||||
new Msg({
|
new Msg({
|
||||||
|
@ -100,52 +81,20 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
||||||
text: "The specified user/hostmask is not ignored",
|
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",
|
"ctcp",
|
||||||
"disconnect",
|
"disconnect",
|
||||||
"ignore",
|
"ignore",
|
||||||
|
"ignorelist",
|
||||||
"invite",
|
"invite",
|
||||||
"kick",
|
"kick",
|
||||||
"kill",
|
"kill",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {PluginInputHandler} from "./index";
|
import {PluginInputHandler} from "./index";
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import {ChanType} from "../../models/chan";
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
import {ChanType} from "../../../shared/types/chan";
|
||||||
|
|
||||||
const commands = ["invite", "invitelist"];
|
const commands = ["invite", "invitelist"];
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {PluginInputHandler} from "./index";
|
import {PluginInputHandler} from "./index";
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import {ChanType} from "../../models/chan";
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
import {ChanType} from "../../../shared/types/chan";
|
||||||
|
|
||||||
const commands = ["kick"];
|
const commands = ["kick"];
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import {PluginInputHandler} from "./index";
|
import {PluginInputHandler} from "./index";
|
||||||
|
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import {ChanType} from "../../models/chan";
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
import {ChanType} from "../../../shared/types/chan";
|
||||||
|
|
||||||
const commands = ["mode", "umode", "op", "deop", "hop", "dehop", "voice", "devoice"];
|
const commands = ["mode", "umode", "op", "deop", "hop", "dehop", "voice", "devoice"];
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import {PluginInputHandler} from "./index";
|
import {PluginInputHandler} from "./index";
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import Chan, {ChanType} from "../../models/chan";
|
import Chan from "../../models/chan";
|
||||||
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
import {ChanType} from "../../../shared/types/chan";
|
||||||
|
|
||||||
const commands = ["query", "msg", "say"];
|
const commands = ["query", "msg", "say"];
|
||||||
|
|
||||||
|
@ -97,10 +99,10 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
||||||
// being sent back to us.
|
// being sent back to us.
|
||||||
if (!network.irc.network.cap.isEnabled("echo-message")) {
|
if (!network.irc.network.cap.isEnabled("echo-message")) {
|
||||||
const parsedTarget = network.irc.network.extractTargetGroup(targetName);
|
const parsedTarget = network.irc.network.extractTargetGroup(targetName);
|
||||||
let targetGroup;
|
let targetGroup: string | undefined = undefined;
|
||||||
|
|
||||||
if (parsedTarget) {
|
if (parsedTarget) {
|
||||||
targetName = parsedTarget.target as string;
|
targetName = parsedTarget.target;
|
||||||
targetGroup = parsedTarget.target_group;
|
targetGroup = parsedTarget.target_group;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,10 @@ import Chan from "../../models/chan";
|
||||||
import Network from "../../models/network";
|
import Network from "../../models/network";
|
||||||
import {PluginInputHandler} from "./index";
|
import {PluginInputHandler} from "./index";
|
||||||
|
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
|
|
||||||
import Client from "../../client";
|
import Client from "../../client";
|
||||||
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
|
||||||
const commands = ["mute", "unmute"];
|
const commands = ["mute", "unmute"];
|
||||||
const allowDisconnected = true;
|
const allowDisconnected = true;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {PluginInputHandler} from "./index";
|
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 commands = ["nick"];
|
||||||
const allowDisconnected = true;
|
const allowDisconnected = true;
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import {PluginInputHandler} from "./index";
|
import {PluginInputHandler} from "./index";
|
||||||
|
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import Config from "../../config";
|
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 commands = ["close", "leave", "part"];
|
||||||
const allowDisconnected = true;
|
const allowDisconnected = true;
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import {PluginInputHandler} from "./index";
|
import {PluginInputHandler} from "./index";
|
||||||
|
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import {ChanType} from "../../models/chan";
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
import {ChanType} from "../../../shared/types/chan";
|
||||||
|
|
||||||
const commands = ["cycle", "rejoin"];
|
const commands = ["cycle", "rejoin"];
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import {PluginInputHandler} from "./index";
|
import {PluginInputHandler} from "./index";
|
||||||
|
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import {ChanType} from "../../models/chan";
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
import {ChanType} from "../../../shared/types/chan";
|
||||||
|
|
||||||
const commands = ["topic"];
|
const commands = ["topic"];
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
import {IrcEventHandler} from "../../client";
|
||||||
import {ChanType} from "../../models/chan";
|
import Msg from "../../models/msg";
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
import {ChanType} from "../../../shared/types/chan";
|
||||||
|
|
||||||
export default <IrcEventHandler>function (irc, network) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -14,7 +14,7 @@ export default <IrcEventHandler>function (irc, network) {
|
||||||
handleSTS(data, false);
|
handleSTS(data, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleSTS(data, shouldReconnect) {
|
function handleSTS(data, shouldReconnect: boolean) {
|
||||||
if (!Object.prototype.hasOwnProperty.call(data.capabilities, "sts")) {
|
if (!Object.prototype.hasOwnProperty.call(data.capabilities, "sts")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -3,10 +3,11 @@ import _ from "lodash";
|
||||||
import {IrcEventHandler} from "../../client";
|
import {IrcEventHandler} from "../../client";
|
||||||
|
|
||||||
import log from "../../log";
|
import log from "../../log";
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import Helper from "../../helper";
|
import Helper from "../../helper";
|
||||||
import Config from "../../config";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import {IrcEventHandler} from "../../client";
|
import {IrcEventHandler} from "../../client";
|
||||||
import Helper from "../../helper";
|
import Helper from "../../helper";
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import User from "../../models/user";
|
import User from "../../models/user";
|
||||||
import pkg from "../../../package.json";
|
import pkg from "../../../package.json";
|
||||||
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
|
||||||
const ctcpResponses = {
|
const ctcpResponses = {
|
||||||
CLIENTINFO: () =>
|
CLIENTINFO: () =>
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
import {IrcEventHandler} from "../../client";
|
||||||
|
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import Config from "../../config";
|
import Config from "../../config";
|
||||||
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
|
||||||
export default <IrcEventHandler>function (irc, network) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import {IrcEventHandler} from "../../client";
|
import {IrcEventHandler} from "../../client";
|
||||||
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
|
||||||
export default <IrcEventHandler>function (irc, network) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import {IrcEventHandler} from "../../client";
|
import {IrcEventHandler} from "../../client";
|
||||||
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
|
||||||
export default <IrcEventHandler>function (irc, network) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import User from "../../models/user";
|
import User from "../../models/user";
|
||||||
import type {IrcEventHandler} from "../../client";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
@ -18,6 +19,7 @@ export default <IrcEventHandler>function (irc, network) {
|
||||||
client.emit("join", {
|
client.emit("join", {
|
||||||
network: network.uuid,
|
network: network.uuid,
|
||||||
chan: chan.getFilteredClone(true),
|
chan: chan.getFilteredClone(true),
|
||||||
|
shouldOpen: false,
|
||||||
index: network.addChannel(chan),
|
index: network.addChannel(chan),
|
||||||
});
|
});
|
||||||
client.save();
|
client.save();
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
import {IrcEventHandler} from "../../client";
|
||||||
import {ChanState} from "../../models/chan";
|
|
||||||
|
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import User from "../../models/user";
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
import {ChanState} from "../../../shared/types/chan";
|
||||||
|
|
||||||
export default <IrcEventHandler>function (irc, network) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
@ -14,11 +14,12 @@ export default <IrcEventHandler>function (irc, network) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const user = chan.getUser(data.kicked!);
|
||||||
const msg = new Msg({
|
const msg = new Msg({
|
||||||
type: MessageType.KICK,
|
type: MessageType.KICK,
|
||||||
time: data.time,
|
time: data.time,
|
||||||
from: chan.getUser(data.nick),
|
from: chan.getUser(data.nick),
|
||||||
target: chan.getUser(data.kicked!),
|
target: user,
|
||||||
text: data.message || "",
|
text: data.message || "",
|
||||||
highlight: data.kicked === irc.user.nick,
|
highlight: data.kicked === irc.user.nick,
|
||||||
self: data.nick === irc.user.nick,
|
self: data.nick === irc.user.nick,
|
||||||
|
@ -34,7 +35,7 @@ export default <IrcEventHandler>function (irc, network) {
|
||||||
state: chan.state,
|
state: chan.state,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
chan.removeUser(msg.target as User);
|
chan.removeUser(user);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,6 +6,7 @@ import mime from "mime-types";
|
||||||
import log from "../../log";
|
import log from "../../log";
|
||||||
import Config from "../../config";
|
import Config from "../../config";
|
||||||
import {findLinksWithSchema} from "../../../shared/linkify";
|
import {findLinksWithSchema} from "../../../shared/linkify";
|
||||||
|
import {LinkPreview} from "../../../shared/types/msg";
|
||||||
import storage from "../storage";
|
import storage from "../storage";
|
||||||
import Client from "../../client";
|
import Client from "../../client";
|
||||||
import Chan from "../../models/chan";
|
import Chan from "../../models/chan";
|
||||||
|
@ -20,23 +21,6 @@ const currentFetchPromises = new Map<string, Promise<FetchRequest>>();
|
||||||
const imageTypeRegex = /^image\/.+/;
|
const imageTypeRegex = /^image\/.+/;
|
||||||
const mediaTypeRegex = /^(audio|video)\/.+/;
|
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) {
|
export default function (client: Client, chan: Chan, msg: Msg, cleanText: string) {
|
||||||
if (!Config.values.prefetch) {
|
if (!Config.values.prefetch) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
@ -50,6 +51,7 @@ export default <IrcEventHandler>function (irc, network) {
|
||||||
client.emit("join", {
|
client.emit("join", {
|
||||||
network: network.uuid,
|
network: network.uuid,
|
||||||
chan: chan.getFilteredClone(true),
|
chan: chan.getFilteredClone(true),
|
||||||
|
shouldOpen: false,
|
||||||
index: network.addChannel(chan),
|
index: network.addChannel(chan),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,24 +1,38 @@
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import LinkPrefetch from "./link";
|
import LinkPrefetch from "./link";
|
||||||
import {cleanIrcMessage} from "../../../shared/irc";
|
import {cleanIrcMessage} from "../../../shared/irc";
|
||||||
import Helper from "../../helper";
|
import Helper from "../../helper";
|
||||||
import {IrcEventHandler} from "../../client";
|
import {IrcEventHandler} from "../../client";
|
||||||
import Chan, {ChanType} from "../../models/chan";
|
import Chan from "../../models/chan";
|
||||||
import User from "../../models/user";
|
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;
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
||||||
irc.on("notice", function (data) {
|
irc.on("notice", function (data) {
|
||||||
data.type = MessageType.NOTICE;
|
handleMessage(convertForHandle(MessageType.NOTICE, data));
|
||||||
|
|
||||||
type ModifiedData = typeof data & {
|
|
||||||
type: MessageType.NOTICE;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMessage(data as ModifiedData);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
irc.on("action", function (data) {
|
irc.on("action", function (data) {
|
||||||
|
@ -37,18 +51,7 @@ export default <IrcEventHandler>function (irc, network) {
|
||||||
handleMessage(data);
|
handleMessage(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleMessage(data: {
|
function handleMessage(data: HandleInput) {
|
||||||
nick: string;
|
|
||||||
hostname: string;
|
|
||||||
ident: string;
|
|
||||||
target: string;
|
|
||||||
type: MessageType;
|
|
||||||
time: number;
|
|
||||||
text?: string;
|
|
||||||
from_server?: boolean;
|
|
||||||
message: string;
|
|
||||||
group?: string;
|
|
||||||
}) {
|
|
||||||
let chan: Chan | undefined;
|
let chan: Chan | undefined;
|
||||||
let from: User;
|
let from: User;
|
||||||
let highlight = false;
|
let highlight = false;
|
||||||
|
@ -105,6 +108,7 @@ export default <IrcEventHandler>function (irc, network) {
|
||||||
client.emit("join", {
|
client.emit("join", {
|
||||||
network: network.uuid,
|
network: network.uuid,
|
||||||
chan: chan.getFilteredClone(true),
|
chan: chan.getFilteredClone(true),
|
||||||
|
shouldOpen: false,
|
||||||
index: network.addChannel(chan),
|
index: network.addChannel(chan),
|
||||||
});
|
});
|
||||||
client.save();
|
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
|
// msg is constructed down here because `from` is being copied in the constructor
|
||||||
const msg = new Msg({
|
const msg = new Msg({
|
||||||
type: data.type,
|
type: data.type,
|
||||||
time: data.time as any,
|
time: new Date(data.time),
|
||||||
text: data.message,
|
text: data.message,
|
||||||
self: self,
|
self: self,
|
||||||
from: from,
|
from: from,
|
||||||
|
@ -164,7 +168,6 @@ export default <IrcEventHandler>function (irc, network) {
|
||||||
|
|
||||||
while ((match = nickRegExp.exec(data.message))) {
|
while ((match = nickRegExp.exec(data.message))) {
|
||||||
if (chan.findUser(match[1])) {
|
if (chan.findUser(match[1])) {
|
||||||
// @ts-expect-error Type 'string' is not assignable to type '{ mode: string; }'.ts(2345)
|
|
||||||
msg.users.push(match[1]);
|
msg.users.push(match[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import {IrcEventHandler} from "../../client";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
@ -68,6 +69,7 @@ export default <IrcEventHandler>function (irc, network) {
|
||||||
client.emit("join", {
|
client.emit("join", {
|
||||||
network: network.uuid,
|
network: network.uuid,
|
||||||
chan: chan.getFilteredClone(true),
|
chan: chan.getFilteredClone(true),
|
||||||
|
shouldOpen: false,
|
||||||
index: network.addChannel(chan),
|
index: network.addChannel(chan),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
@ -28,9 +29,9 @@ export default <IrcEventHandler>function (irc, network) {
|
||||||
});
|
});
|
||||||
|
|
||||||
client.emit("join", {
|
client.emit("join", {
|
||||||
shouldOpen: true,
|
|
||||||
network: network.uuid,
|
network: network.uuid,
|
||||||
chan: chan.getFilteredClone(true),
|
chan: chan.getFilteredClone(true),
|
||||||
|
shouldOpen: true,
|
||||||
index: network.addChannel(chan),
|
index: network.addChannel(chan),
|
||||||
});
|
});
|
||||||
chan.loadMessages(client, network);
|
chan.loadMessages(client, network);
|
||||||
|
|
|
@ -7,8 +7,9 @@ import Config from "../../config";
|
||||||
import Msg, {Message} from "../../models/msg";
|
import Msg, {Message} from "../../models/msg";
|
||||||
import Chan, {Channel} from "../../models/chan";
|
import Chan, {Channel} from "../../models/chan";
|
||||||
import Helper from "../../helper";
|
import Helper from "../../helper";
|
||||||
import type {SearchResponse, SearchQuery, SearchableMessageStorage, DeletionRequest} from "./types";
|
import type {SearchableMessageStorage, DeletionRequest} from "./types";
|
||||||
import Network from "../../models/network";
|
import Network from "../../models/network";
|
||||||
|
import {SearchQuery, SearchResponse} from "../../../shared/types/storage";
|
||||||
|
|
||||||
// TODO; type
|
// TODO; type
|
||||||
let sqlite3: any;
|
let sqlite3: any;
|
||||||
|
|
|
@ -6,8 +6,9 @@ import filenamify from "filenamify";
|
||||||
import Config from "../../config";
|
import Config from "../../config";
|
||||||
import {MessageStorage} from "./types";
|
import {MessageStorage} from "./types";
|
||||||
import Channel from "../../models/chan";
|
import Channel from "../../models/chan";
|
||||||
import {Message, MessageType} from "../../models/msg";
|
import {Message} from "../../models/msg";
|
||||||
import Network from "../../models/network";
|
import Network from "../../models/network";
|
||||||
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
|
||||||
class TextFileMessageStorage implements MessageStorage {
|
class TextFileMessageStorage implements MessageStorage {
|
||||||
isEnabled: boolean;
|
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 {Message} from "../../models/message";
|
||||||
import {Network} from "../../models/network";
|
import {Network} from "../../models/network";
|
||||||
import Client from "../../client";
|
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 = {
|
export type DeletionRequest = {
|
||||||
olderThanDays: number;
|
olderThanDays: number;
|
||||||
|
@ -28,17 +29,6 @@ interface MessageStorage {
|
||||||
canProvideMessages(): boolean;
|
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>;
|
type SearchFunction = (query: SearchQuery) => Promise<SearchResponse>;
|
||||||
|
|
||||||
export interface SearchableMessageStorage extends MessageStorage {
|
export interface SearchableMessageStorage extends MessageStorage {
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import {PackageInfo} from "./index";
|
import {PackageInfo} from "./index";
|
||||||
import Client from "../../client";
|
import Client from "../../client";
|
||||||
import Chan from "../../models/chan";
|
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 {
|
export default class PublicClient {
|
||||||
private client: Client;
|
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 {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
|
* @param {Object} data - Body of the event, can be anything, but will need to be properly interpreted by the client
|
||||||
*/
|
*/
|
||||||
sendToBrowser(event: string, data) {
|
// FIXME: this is utterly bonkers
|
||||||
this.client.emit(event, data);
|
// 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,
|
text: text,
|
||||||
from: {
|
from: {
|
||||||
nick: this.packageInfo.name || this.packageInfo.packageName,
|
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 express, {NextFunction, Request, Response} from "express";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
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 dns from "dns";
|
||||||
import colors from "chalk";
|
import colors from "chalk";
|
||||||
import net from "net";
|
import net from "net";
|
||||||
|
@ -13,25 +13,32 @@ import Client from "./client";
|
||||||
import ClientManager from "./clientManager";
|
import ClientManager from "./clientManager";
|
||||||
import Uploader from "./plugins/uploader";
|
import Uploader from "./plugins/uploader";
|
||||||
import Helper from "./helper";
|
import Helper from "./helper";
|
||||||
import Config, {ConfigType, Defaults} from "./config";
|
import Config, {ConfigType} from "./config";
|
||||||
import Identification from "./identification";
|
import Identification from "./identification";
|
||||||
import changelog from "./plugins/changelog";
|
import changelog from "./plugins/changelog";
|
||||||
import inputs from "./plugins/inputs";
|
import inputs from "./plugins/inputs";
|
||||||
import Auth from "./plugins/auth";
|
import Auth from "./plugins/auth";
|
||||||
|
|
||||||
import themes, {ThemeForClient} from "./plugins/packages/themes";
|
import themes from "./plugins/packages/themes";
|
||||||
themes.loadLocalThemes();
|
themes.loadLocalThemes();
|
||||||
|
|
||||||
import packages from "./plugins/packages/index";
|
import packages from "./plugins/packages/index";
|
||||||
import {NetworkWithIrcFramework} from "./models/network";
|
import {NetworkWithIrcFramework} from "./models/network";
|
||||||
import {ChanType} from "./models/chan";
|
|
||||||
import Utils from "./command-line/utils";
|
import Utils from "./command-line/utils";
|
||||||
import type {
|
import type {
|
||||||
ClientToServerEvents,
|
ClientToServerEvents,
|
||||||
ServerToClientEvents,
|
ServerToClientEvents,
|
||||||
InterServerEvents,
|
InterServerEvents,
|
||||||
SocketData,
|
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 = {
|
type ServerOptions = {
|
||||||
dev: boolean;
|
dev: boolean;
|
||||||
|
@ -45,21 +52,13 @@ type IndexTemplateConfiguration = ServerConfiguration & {
|
||||||
cacheBust: string;
|
cacheBust: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ClientConfiguration = Pick<
|
type Socket = ioSocket<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>;
|
||||||
ConfigType,
|
export type Server = ioServer<
|
||||||
"public" | "lockNetwork" | "useHexIp" | "prefetch" | "defaults"
|
ClientToServerEvents,
|
||||||
> & {
|
ServerToClientEvents,
|
||||||
fileUpload: boolean;
|
InterServerEvents,
|
||||||
ldapEnabled: boolean;
|
SocketData
|
||||||
isUpdateAvailable: boolean;
|
>;
|
||||||
applicationServerKey: string;
|
|
||||||
version: string;
|
|
||||||
gitCommit: string | null;
|
|
||||||
defaultTheme: string;
|
|
||||||
themes: ThemeForClient[];
|
|
||||||
defaults: Defaults;
|
|
||||||
fileUploadMaxFileSize?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
// A random number that will force clients to reload the page if it differs
|
// A random number that will force clients to reload the page if it differs
|
||||||
const serverHash = Math.floor(Date.now() * Math.random());
|
const serverHash = Math.floor(Date.now() * Math.random());
|
||||||
|
@ -219,12 +218,7 @@ export default async function (
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sockets = new Server<
|
const sockets: Server = new ioServer(server, {
|
||||||
ClientToServerEvents,
|
|
||||||
ServerToClientEvents,
|
|
||||||
InterServerEvents,
|
|
||||||
SocketData
|
|
||||||
>(server, {
|
|
||||||
wsEngine: wsServer,
|
wsEngine: wsServer,
|
||||||
cookie: false,
|
cookie: false,
|
||||||
serveClient: false,
|
serveClient: false,
|
||||||
|
@ -330,7 +324,7 @@ export default async function (
|
||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getClientLanguage(socket: Socket): string | null {
|
function getClientLanguage(socket: Socket): string | undefined {
|
||||||
const acceptLanguage = socket.handshake.headers["accept-language"];
|
const acceptLanguage = socket.handshake.headers["accept-language"];
|
||||||
|
|
||||||
if (typeof acceptLanguage === "string" && /^[\x00-\x7F]{1,50}$/.test(acceptLanguage)) {
|
if (typeof acceptLanguage === "string" && /^[\x00-\x7F]{1,50}$/.test(acceptLanguage)) {
|
||||||
|
@ -338,10 +332,10 @@ function getClientLanguage(socket: Socket): string | null {
|
||||||
return acceptLanguage;
|
return acceptLanguage;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getClientIp(socket: Socket) {
|
function getClientIp(socket: Socket): string {
|
||||||
let ip = socket.handshake.address || "127.0.0.1";
|
let ip = socket.handshake.address || "127.0.0.1";
|
||||||
|
|
||||||
if (Config.values.reverseProxy) {
|
if (Config.values.reverseProxy) {
|
||||||
|
@ -367,12 +361,12 @@ function getClientSecure(socket: Socket) {
|
||||||
return secure;
|
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");
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
function addSecurityHeaders(req: Request, res: Response, next: NextFunction) {
|
function addSecurityHeaders(_req: Request, res: Response, next: NextFunction) {
|
||||||
const policies = [
|
const policies = [
|
||||||
"default-src 'none'", // default to nothing
|
"default-src 'none'", // default to nothing
|
||||||
"base-uri 'none'", // disallow <base>, has no fallback to default-src
|
"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();
|
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,
|
// Intermittent proxies must not cache the following requests,
|
||||||
// browsers must fetch the latest version of these files (service worker, source maps)
|
// browsers must fetch the latest version of these files (service worker, source maps)
|
||||||
res.setHeader("Cache-Control", "no-cache, no-transform");
|
res.setHeader("Cache-Control", "no-cache, no-transform");
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
function indexRequest(req: Request, res: Response) {
|
function indexRequest(_req: Request, res: Response) {
|
||||||
res.setHeader("Content-Type", "text/html");
|
res.setHeader("Content-Type", "text/html");
|
||||||
|
|
||||||
return fs.readFile(
|
fs.readFile(Utils.getFileFromRelativeToRoot("client/index.html.tpl"), "utf-8", (err, file) => {
|
||||||
Utils.getFileFromRelativeToRoot("client/index.html.tpl"),
|
if (err) {
|
||||||
"utf-8",
|
log.error(`failed to server index request: ${err.name}, ${err.message}`);
|
||||||
(err, file) => {
|
res.sendStatus(500);
|
||||||
if (err) {
|
return;
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
const config: IndexTemplateConfiguration = {
|
|
||||||
...getServerConfiguration(),
|
|
||||||
...{cacheBust: Helper.getVersionCacheBust()},
|
|
||||||
};
|
|
||||||
|
|
||||||
res.send(_.template(file)(config));
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
const config: IndexTemplateConfiguration = {
|
||||||
|
...getServerConfiguration(),
|
||||||
|
...{cacheBust: Helper.getVersionCacheBust()},
|
||||||
|
};
|
||||||
|
|
||||||
|
res.send(_.template(file)(config));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeClient(
|
function initializeClient(
|
||||||
|
@ -552,18 +544,10 @@ function initializeClient(
|
||||||
const hash = Helper.password.hash(p1);
|
const hash = Helper.password.hash(p1);
|
||||||
|
|
||||||
client.setPassword(hash, (success: boolean) => {
|
client.setPassword(hash, (success: boolean) => {
|
||||||
const obj = {success: false, error: undefined} as {
|
socket.emit("change-password", {
|
||||||
success: boolean;
|
success: success,
|
||||||
error: string | undefined;
|
error: success ? undefined : "update_failed",
|
||||||
};
|
});
|
||||||
|
|
||||||
if (success) {
|
|
||||||
obj.success = true;
|
|
||||||
} else {
|
|
||||||
obj.error = "update_failed";
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.emit("change-password", obj);
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((error: Error) => {
|
.catch((error: Error) => {
|
||||||
|
@ -577,10 +561,28 @@ function initializeClient(
|
||||||
client.open(socket.id, data);
|
client.open(socket.id, data);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("sort", (data) => {
|
socket.on("sort:networks", (data) => {
|
||||||
if (_.isPlainObject(data)) {
|
if (!_.isPlainObject(data)) {
|
||||||
client.sort(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) => {
|
socket.on("names", (data) => {
|
||||||
|
@ -630,13 +632,13 @@ function initializeClient(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = networkAndChan.chan.findMessage(data.msgId);
|
const message = data.msgId ? networkAndChan.chan.findMessage(data.msgId) : null;
|
||||||
|
|
||||||
if (!message) {
|
if (!message) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const preview = message.findPreview(data.link);
|
const preview = data.link ? message.findPreview(data.link) : null;
|
||||||
|
|
||||||
if (preview) {
|
if (preview) {
|
||||||
preview.shown = newState;
|
preview.shown = newState;
|
||||||
|
@ -828,9 +830,9 @@ function initializeClient(
|
||||||
});
|
});
|
||||||
|
|
||||||
// socket.join is a promise depending on the adapter.
|
// 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", {
|
socket.emit("init", {
|
||||||
active: openChannel,
|
active: openChannel,
|
||||||
networks: client.networks.map((network) =>
|
networks: client.networks.map((network) =>
|
||||||
|
@ -842,7 +844,7 @@ function initializeClient(
|
||||||
};
|
};
|
||||||
|
|
||||||
if (Config.values.public) {
|
if (Config.values.public) {
|
||||||
sendInitEvent(null);
|
sendInitEvent();
|
||||||
} else if (!token) {
|
} else if (!token) {
|
||||||
client.generateToken((newToken) => {
|
client.generateToken((newToken) => {
|
||||||
token = client.calculateTokenHash(newToken);
|
token = client.calculateTokenHash(newToken);
|
||||||
|
@ -853,73 +855,108 @@ function initializeClient(
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
client.updateSession(token, getClientIp(socket), socket.request);
|
client.updateSession(token, getClientIp(socket), socket.request);
|
||||||
sendInitEvent(null);
|
sendInitEvent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getClientConfiguration(): ClientConfiguration {
|
function getClientConfiguration(): SharedConfiguration | LockedSharedConfiguration {
|
||||||
const config = _.pick(Config.values, [
|
const common = {
|
||||||
"public",
|
fileUpload: Config.values.fileUpload.enable,
|
||||||
"lockNetwork",
|
ldapEnabled: Config.values.ldap.enable,
|
||||||
"useHexIp",
|
isUpdateAvailable: changelog.isUpdateAvailable,
|
||||||
"prefetch",
|
applicationServerKey: manager!.webPush.vapidKeys!.publicKey,
|
||||||
]) as ClientConfiguration;
|
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;
|
const defaultsOverride = {
|
||||||
config.ldapEnabled = Config.values.ldap.enable;
|
nick: Config.getDefaultNick(), // expand the number part
|
||||||
|
|
||||||
if (!config.lockNetwork) {
|
// TODO: this doesn't seem right, if the client needs this as a buffer
|
||||||
config.defaults = _.clone(Config.values.defaults);
|
// the client ought to add it on its own
|
||||||
} else {
|
sasl: "",
|
||||||
// Only send defaults that are visible on the client
|
saslAccount: "",
|
||||||
config.defaults = _.pick(Config.values.defaults, [
|
saslPassword: "",
|
||||||
"name",
|
};
|
||||||
"nick",
|
|
||||||
"username",
|
if (!Config.values.lockNetwork) {
|
||||||
"password",
|
const defaults: ConfigNetDefaults = {
|
||||||
"realname",
|
..._.clone(Config.values.defaults),
|
||||||
"join",
|
...defaultsOverride,
|
||||||
]) as Defaults;
|
};
|
||||||
|
const result: SharedConfiguration = {
|
||||||
|
...common,
|
||||||
|
defaults: defaults,
|
||||||
|
lockNetwork: Config.values.lockNetwork,
|
||||||
|
};
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
config.isUpdateAvailable = changelog.isUpdateAvailable;
|
// Only send defaults that are visible on the client
|
||||||
config.applicationServerKey = manager!.webPush.vapidKeys!.publicKey;
|
const defaults: LockedConfigNetDefaults = {
|
||||||
config.version = Helper.getVersionNumber();
|
..._.pick(Config.values.defaults, ["name", "username", "password", "realname", "join"]),
|
||||||
config.gitCommit = Helper.getGitCommit();
|
...defaultsOverride,
|
||||||
config.themes = themes.getAll();
|
};
|
||||||
config.defaultTheme = Config.values.theme;
|
|
||||||
config.defaults.nick = Config.getDefaultNick();
|
|
||||||
config.defaults.sasl = "";
|
|
||||||
config.defaults.saslAccount = "";
|
|
||||||
config.defaults.saslPassword = "";
|
|
||||||
|
|
||||||
if (Uploader) {
|
const result: LockedSharedConfiguration = {
|
||||||
config.fileUploadMaxFileSize = Uploader.getMaxFileSize();
|
...common,
|
||||||
}
|
lockNetwork: Config.values.lockNetwork,
|
||||||
|
defaults: defaults,
|
||||||
|
};
|
||||||
|
|
||||||
return config;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getServerConfiguration(): ServerConfiguration {
|
function getServerConfiguration(): ServerConfiguration {
|
||||||
return {...Config.values, ...{stylesheets: packages.getStylesheets()}};
|
return {...Config.values, ...{stylesheets: packages.getStylesheets()}};
|
||||||
}
|
}
|
||||||
|
|
||||||
function performAuthentication(this: Socket, data) {
|
function performAuthentication(this: Socket, data: AuthPerformData) {
|
||||||
if (!_.isPlainObject(data)) {
|
if (!_.isPlainObject(data)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const socket = this;
|
const socket = this;
|
||||||
let client;
|
let client: Client | undefined;
|
||||||
let token: string;
|
let token: string;
|
||||||
|
|
||||||
const finalInit = () =>
|
const finalInit = () => {
|
||||||
initializeClient(socket, client, token, data.lastMessage || -1, data.openChannel);
|
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 = () => {
|
const initClient = () => {
|
||||||
|
if (!client) {
|
||||||
|
throw new Error("initClient called with undefined client");
|
||||||
|
}
|
||||||
|
|
||||||
// Configuration does not change during runtime of TL,
|
// Configuration does not change during runtime of TL,
|
||||||
// and the client listens to this event only once
|
// 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("configuration", getClientConfiguration());
|
||||||
|
|
||||||
socket.emit(
|
socket.emit(
|
||||||
|
@ -928,8 +965,10 @@ function performAuthentication(this: Socket, data) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clientIP = getClientIp(socket);
|
||||||
|
|
||||||
client.config.browser = {
|
client.config.browser = {
|
||||||
ip: getClientIp(socket),
|
ip: clientIP,
|
||||||
isSecure: getClientSecure(socket),
|
isSecure: getClientSecure(socket),
|
||||||
language: getClientLanguage(socket),
|
language: getClientLanguage(socket),
|
||||||
};
|
};
|
||||||
|
@ -939,8 +978,9 @@ function performAuthentication(this: Socket, data) {
|
||||||
return finalInit();
|
return finalInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
reverseDnsLookup(client.config.browser?.ip, (hostname) => {
|
const cb_client = client; // ensure that TS figures out that client can't be nil
|
||||||
client.config.browser!.hostname = hostname;
|
reverseDnsLookup(clientIP, (hostname) => {
|
||||||
|
cb_client.config.browser!.hostname = hostname;
|
||||||
|
|
||||||
finalInit();
|
finalInit();
|
||||||
});
|
});
|
||||||
|
@ -951,9 +991,10 @@ function performAuthentication(this: Socket, data) {
|
||||||
client.connect();
|
client.connect();
|
||||||
manager!.clients.push(client);
|
manager!.clients.push(client);
|
||||||
|
|
||||||
|
const cb_client = client; // ensure TS can see we never have a nil client
|
||||||
socket.on("disconnect", function () {
|
socket.on("disconnect", function () {
|
||||||
manager!.clients = _.without(manager!.clients, client);
|
manager!.clients = _.without(manager!.clients, cb_client);
|
||||||
client.quit();
|
cb_client.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
initClient();
|
initClient();
|
||||||
|
@ -965,7 +1006,7 @@ function performAuthentication(this: Socket, data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const authCallback = (success) => {
|
const authCallback = (success: boolean) => {
|
||||||
// Authorization failed
|
// Authorization failed
|
||||||
if (!success) {
|
if (!success) {
|
||||||
if (!client) {
|
if (!client) {
|
||||||
|
@ -990,6 +1031,10 @@ function performAuthentication(this: Socket, data) {
|
||||||
// load it and find the user again (this happens with LDAP)
|
// load it and find the user again (this happens with LDAP)
|
||||||
if (!client) {
|
if (!client) {
|
||||||
client = manager!.loadUser(data.user);
|
client = manager!.loadUser(data.user);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
throw new Error(`authCallback: ${data.user} not found after second lookup`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initClient();
|
initClient();
|
||||||
|
@ -998,16 +1043,23 @@ function performAuthentication(this: Socket, data) {
|
||||||
client = manager!.findClient(data.user);
|
client = manager!.findClient(data.user);
|
||||||
|
|
||||||
// We have found an existing user and client has provided a token
|
// 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);
|
const providedToken = client.calculateTokenHash(data.token);
|
||||||
|
|
||||||
if (Object.prototype.hasOwnProperty.call(client.config.sessions, providedToken)) {
|
if (Object.prototype.hasOwnProperty.call(client.config.sessions, providedToken)) {
|
||||||
token = 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(() => {
|
Auth.initialize().then(() => {
|
||||||
// Perform password checking
|
// Perform password checking
|
||||||
Auth.auth(manager, client, data.user, data.password, authCallback);
|
Auth.auth(manager, client, data.user, data.password, authCallback);
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import SqliteMessageStorage from "./plugins/messageStorage/sqlite";
|
import SqliteMessageStorage from "./plugins/messageStorage/sqlite";
|
||||||
import {MessageType} from "./models/msg";
|
|
||||||
import Config from "./config";
|
import Config from "./config";
|
||||||
import {DeletionRequest} from "./plugins/messageStorage/types";
|
import {DeletionRequest} from "./plugins/messageStorage/types";
|
||||||
import log from "./log";
|
import log from "./log";
|
||||||
|
import {MessageType} from "../shared/types/msg";
|
||||||
|
|
||||||
const status_types = [
|
const status_types = [
|
||||||
MessageType.AWAY,
|
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. */,
|
"extends": "../tsconfig.base.json" /* Path to base configuration file to inherit from. Requires TypeScript version 2.1 or later. */,
|
||||||
"include": [
|
"include": [
|
||||||
"**/*",
|
".",
|
||||||
"../shared/"
|
"../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. */,
|
] /* 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": [
|
"files": [
|
||||||
|
|
1
server/types/index.d.ts
vendored
1
server/types/index.d.ts
vendored
|
@ -1,2 +1 @@
|
||||||
import "./modules";
|
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;
|
reply: (message: string) => void;
|
||||||
tags: {[key: string]: string};
|
tags: {[key: string]: string};
|
||||||
target: string;
|
target: string;
|
||||||
time?: any;
|
time?: number;
|
||||||
type: "privmsg" | "action" | "notice" | "wallops";
|
|
||||||
}
|
}
|
||||||
export interface JoinEventArgs {
|
export interface JoinEventArgs {
|
||||||
account: boolean;
|
account: boolean;
|
||||||
|
@ -117,7 +116,11 @@ declare module "irc-framework" {
|
||||||
isEnabled: (cap: string) => boolean;
|
isEnabled: (cap: string) => boolean;
|
||||||
enabled: string[];
|
enabled: string[];
|
||||||
};
|
};
|
||||||
extractTargetGroup: (target: string) => any;
|
extractTargetGroup: (target: string) => {
|
||||||
|
target: string;
|
||||||
|
target_group: string;
|
||||||
|
};
|
||||||
|
|
||||||
supports(feature: "MODES"): string;
|
supports(feature: "MODES"): string;
|
||||||
supports(feature: string): boolean;
|
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 {expect} from "chai";
|
||||||
import {NetworkConfig} from "../server/models/network";
|
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 ClientManager from "../server/clientManager";
|
||||||
import Client from "../server/client";
|
import Client from "../server/client";
|
||||||
import log from "../server/log";
|
import log from "../server/log";
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
// @ts-nocheck TODO re-enable
|
|
||||||
import {expect} from "chai";
|
import {expect} from "chai";
|
||||||
import Client from "../../server/client";
|
import Chan from "../../server/models/chan";
|
||||||
|
import {ChanType} from "../../shared/types/chan";
|
||||||
import Chan, {ChanType} from "../../server/models/chan";
|
|
||||||
import ModeCommand from "../../server/plugins/inputs/mode";
|
import ModeCommand from "../../server/plugins/inputs/mode";
|
||||||
|
|
||||||
describe("Commands", function () {
|
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[]) {
|
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);
|
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);
|
expect(testableNetwork.lastCommand).to.equal(expected);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -77,51 +79,51 @@ describe("Commands", function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should assume target if none given", 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");
|
expect(testableNetwork.lastCommand).to.equal("MODE #thelounge");
|
||||||
|
|
||||||
ModeCommand.input(testableNetwork, lobby, "mode", []);
|
modeCommandInputCall(testableNetwork, lobby, "mode", []);
|
||||||
expect(testableNetwork.lastCommand).to.equal("MODE xPaw");
|
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");
|
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");
|
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");
|
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");
|
expect(testableNetwork.lastCommand).to.equal("MODE xPaw -i idk");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should support shorthand commands", function () {
|
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");
|
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");
|
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");
|
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");
|
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");
|
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");
|
expect(testableNetwork.lastCommand).to.equal("MODE #thelounge -v xPaw");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should use ISUPPORT MODES on shorthand commands", function () {
|
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");
|
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
|
// since the limit for modes on tests is 4, it should send two commands
|
||||||
ModeCommand.input(testableNetwork, channel, "devoice", [
|
modeCommandInputCall(testableNetwork, channel, "devoice", [
|
||||||
"xPaw",
|
"xPaw",
|
||||||
"Max-P",
|
"Max-P",
|
||||||
"hey",
|
"hey",
|
||||||
|
@ -135,10 +137,10 @@ describe("Commands", function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should fallback to all modes at once for shorthand 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");
|
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(
|
expect(testableNetworkNoSupports.lastCommand).to.equal(
|
||||||
"MODE #thelounge -vv xPaw Max-P"
|
"MODE #thelounge -vv xPaw Max-P"
|
||||||
);
|
);
|
||||||
|
|
|
@ -195,33 +195,28 @@ describe("Chan", function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#getFilteredClone(lastActiveChannel, lastMessage)", 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 () {
|
it("should keep necessary properties", function () {
|
||||||
const chan = new Chan();
|
const chan = new Chan();
|
||||||
|
|
||||||
expect(chan.getFilteredClone())
|
expect(chan.getFilteredClone()).to.be.an("object").that.has.all.keys(
|
||||||
.to.be.an("object")
|
"firstUnread",
|
||||||
.that.has.all.keys(
|
"highlight",
|
||||||
"firstUnread",
|
"id",
|
||||||
"highlight",
|
"key",
|
||||||
"id",
|
"messages",
|
||||||
"key",
|
"muted",
|
||||||
"messages",
|
"totalMessages",
|
||||||
"muted",
|
"name",
|
||||||
"totalMessages",
|
"state",
|
||||||
"name",
|
"topic",
|
||||||
"state",
|
"type",
|
||||||
"topic",
|
"unread",
|
||||||
"type",
|
// the following are there in special cases, need to fix the types
|
||||||
"unread",
|
"num_users",
|
||||||
"users"
|
"special",
|
||||||
);
|
"closed",
|
||||||
|
"data"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should send only last message for non active channel", function () {
|
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 Msg from "../../server/models/msg";
|
||||||
import User from "../../server/models/user";
|
import User from "../../server/models/user";
|
||||||
import {LinkPreview} from "../../server/plugins/irc-events/link";
|
import {LinkPreview} from "../../shared/types/msg";
|
||||||
|
|
||||||
describe("Msg", function () {
|
describe("Msg", function () {
|
||||||
["from", "target"].forEach((prop) => {
|
["from", "target"].forEach((prop) => {
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||||
import {expect} from "chai";
|
import {expect} from "chai";
|
||||||
import sinon from "ts-sinon";
|
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 Msg from "../../server/models/msg";
|
||||||
import User from "../../server/models/user";
|
import User from "../../server/models/user";
|
||||||
import Network from "../../server/models/network";
|
import Network from "../../server/models/network";
|
||||||
|
|
|
@ -3,7 +3,8 @@ import path from "path";
|
||||||
import {expect} from "chai";
|
import {expect} from "chai";
|
||||||
import util from "../util";
|
import util from "../util";
|
||||||
import Config from "../../server/config";
|
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 () {
|
describe("Link plugin", function () {
|
||||||
// Increase timeout due to unpredictable I/O on CI services
|
// 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 fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import {expect} from "chai";
|
import {expect} from "chai";
|
||||||
import util from "../util";
|
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 Config from "../../server/config";
|
||||||
import MessageStorage, {
|
import MessageStorage, {
|
||||||
currentSchemaVersion,
|
currentSchemaVersion,
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue