mirror of
https://github.com/thelounge/thelounge.git
synced 2024-06-06 15:52:17 +02:00
Compare commits
129 commits
Author | SHA1 | Date | |
---|---|---|---|
0d9c184f19 | |||
4de413070d | |||
0955d9df06 | |||
cb4aaf6a97 | |||
45c2fc87ee | |||
29fcc2da05 | |||
12679081c8 | |||
0e48014d5a | |||
4819406af5 | |||
74563effa7 | |||
cbab10f416 | |||
4dfeb899b4 | |||
3259ac596d | |||
3fbbc39cd6 | |||
9ae9482223 | |||
a3953405ed | |||
9086bc648d | |||
da2572fe25 | |||
d9977df315 | |||
cc0aa5e8e5 | |||
02df78b0f2 | |||
18b0e06855 | |||
d5db9c653b | |||
f7926267d9 | |||
8eb398c5cc | |||
36cb75ee99 | |||
1ec67a6605 | |||
8372c5a57e | |||
5567f07a7c | |||
a200bab8bd | |||
91ac363cc6 | |||
6c9d2c36a1 | |||
6241eed8f4 | |||
03151e0ab1 | |||
7f5e0f3ebf | |||
5e444be37b | |||
c8664301ba | |||
1edb5a72c1 | |||
31d987283a | |||
4ceafb653f | |||
f25fee4c6c | |||
96848c1c1b | |||
4b07e05491 | |||
fc9805545b | |||
82e4150cc8 | |||
e61e356f1e | |||
5001d607b1 | |||
8c41356ae9 | |||
e2b56cf16b | |||
92a0affba1 | |||
edb96f683b | |||
5c8951ffc3 | |||
c3fc54e158 | |||
917fdb2a0a | |||
b8400a3a46 | |||
071a5afda6 | |||
5274fdc21a | |||
b8a9fe08ab | |||
a4afa08add | |||
4614c35486 | |||
540144c417 | |||
bb7c3925c6 | |||
9898f38de6 | |||
9f2c82e152 | |||
17ba07db3b | |||
0311e5f836 | |||
4d0474b897 | |||
14b9169899 | |||
50037644c0 | |||
7287c6bcaa | |||
bfca0ca612 | |||
300bd4c84c | |||
42ea66c343 | |||
1565eb8d05 | |||
29750a3e51 | |||
3ea5170e6a | |||
fe4f497fad | |||
c20cd6bda1 | |||
1c4ce5d4a5 | |||
9c4d24d1f7 | |||
35e38d13c4 | |||
bf7eb0e727 | |||
5ee9c2b338 | |||
e15b121080 | |||
98452ccc18 | |||
a8e7022d04 | |||
60486bf5e3 | |||
46f3fd9682 | |||
56215382a3 | |||
9ab9ad0f56 | |||
0660a8772c | |||
f5c691f37b | |||
0067c30273 | |||
843db1727b | |||
e9ef59b641 | |||
fceffd42b9 | |||
b89b0cad53 | |||
c869ea9a73 | |||
9aee3e3e98 | |||
636b5c5b04 | |||
6984e8f25a | |||
e43cbb139c | |||
e57e547b74 | |||
3217536245 | |||
194b4e1a2f | |||
88c8830a17 | |||
7073584f1c | |||
8e6920af1d | |||
7bc184b252 | |||
4d237600d5 | |||
383907c2b8 | |||
f0ee3be6fb | |||
12a0b0b6f9 | |||
d716402da2 | |||
d0b71aba32 | |||
3f0ee6a961 | |||
b67e4699f5 | |||
68ba13ca12 | |||
3eb19135f5 | |||
549c445853 | |||
2466c1b1e4 | |||
f5867c3643 | |||
231c498def | |||
e8f6ba5b08 | |||
07276bbde4 | |||
9ad92e1860 | |||
7923d4a2cd | |||
9248358169 | |||
6ab52bc9a9 |
|
@ -93,6 +93,7 @@ const tsRules = defineConfig({
|
|||
// note you must disable the base rule as it can report incorrect errors
|
||||
"no-shadow": "off",
|
||||
"@typescript-eslint/no-shadow": ["error"],
|
||||
"@typescript-eslint/no-redundant-type-constituents": "off",
|
||||
},
|
||||
}).rules;
|
||||
|
||||
|
|
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
|
@ -20,6 +20,7 @@ jobs:
|
|||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "latest"
|
||||
registry-url: "https://registry.npmjs.org/"
|
||||
|
||||
- name: Install
|
||||
|
@ -33,9 +34,6 @@ jobs:
|
|||
- name: Test
|
||||
run: yarn test
|
||||
|
||||
- name: Update npm
|
||||
run: npm install -g npm
|
||||
|
||||
- name: Publish latest
|
||||
if: "!contains(github.ref, '-')"
|
||||
run: npm publish --tag latest --provenance
|
||||
|
|
|
@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
<!-- New entries go after this line -->
|
||||
|
||||
## v4.4.2 - 2024-04-01
|
||||
## v4.4.3 - 2024-04-01
|
||||
|
||||
The Lounge finally gains the ability to automatically clean up sqlite databases.
|
||||
Note that cleaning existing, large databases can take a significant amount of time
|
||||
|
@ -19,7 +19,7 @@ As usual, we follow the Node.js release schedule, so the minimum Node.js version
|
|||
Many thanks to all the contributors to this release, be that documentation, code or maintaining the packages.
|
||||
Your help is greatly appreciated!
|
||||
|
||||
For more details, [see the full changelog](https://github.com/thelounge/thelounge/compare/v4.4.1...v4.4.2)
|
||||
For more details, [see the full changelog](https://github.com/thelounge/thelounge/compare/v4.4.1...v4.4.3)
|
||||
|
||||
### Added
|
||||
|
||||
|
|
|
@ -136,6 +136,7 @@ import ListIgnored from "./Special/ListIgnored.vue";
|
|||
import {defineComponent, PropType, ref, computed, watch, nextTick, onMounted, Component} from "vue";
|
||||
import type {ClientNetwork, ClientChan} from "../js/types";
|
||||
import {useStore} from "../js/store";
|
||||
import {SpecialChanType, ChanType} from "../../shared/types/chan";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Chat",
|
||||
|
@ -161,13 +162,13 @@ export default defineComponent({
|
|||
|
||||
const specialComponent = computed(() => {
|
||||
switch (props.channel.special) {
|
||||
case "list_bans":
|
||||
case SpecialChanType.BANLIST:
|
||||
return ListBans as Component;
|
||||
case "list_invites":
|
||||
case SpecialChanType.INVITELIST:
|
||||
return ListInvites as Component;
|
||||
case "list_channels":
|
||||
case SpecialChanType.CHANNELLIST:
|
||||
return ListChannels as Component;
|
||||
case "list_ignored":
|
||||
case SpecialChanType.IGNORELIST:
|
||||
return ListIgnored as Component;
|
||||
}
|
||||
|
||||
|
@ -194,7 +195,7 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
const editTopic = () => {
|
||||
if (props.channel.type === "channel") {
|
||||
if (props.channel.type === ChanType.CHANNEL) {
|
||||
props.channel.editTopic = true;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -56,13 +56,14 @@
|
|||
import Mousetrap from "mousetrap";
|
||||
import {wrapCursor} from "undate";
|
||||
import autocompletion from "../js/autocompletion";
|
||||
import commands from "../js/commands/index";
|
||||
import {commands} from "../js/commands/index";
|
||||
import socket from "../js/socket";
|
||||
import upload from "../js/upload";
|
||||
import eventbus from "../js/eventbus";
|
||||
import {watch, defineComponent, nextTick, onMounted, PropType, ref, onUnmounted} from "vue";
|
||||
import type {ClientNetwork, ClientChan} from "../js/types";
|
||||
import {useStore} from "../js/store";
|
||||
import {ChanType} from "../../shared/types/chan";
|
||||
|
||||
const formattingHotkeys = {
|
||||
"mod+k": "\x03",
|
||||
|
@ -130,7 +131,7 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
const getInputPlaceholder = (channel: ClientChan) => {
|
||||
if (channel.type === "channel" || channel.type === "query") {
|
||||
if (channel.type === ChanType.CHANNEL || channel.type === ChanType.QUERY) {
|
||||
return `Write to ${channel.name}`;
|
||||
}
|
||||
|
||||
|
@ -185,10 +186,7 @@ export default defineComponent({
|
|||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(commands, cmd) &&
|
||||
commands[cmd].input(args)
|
||||
) {
|
||||
if (Object.prototype.hasOwnProperty.call(commands, cmd) && commands[cmd](args)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
<script lang="ts">
|
||||
import {filter as fuzzyFilter} from "fuzzy";
|
||||
import {computed, defineComponent, nextTick, PropType, ref} from "vue";
|
||||
import type {UserInMessage} from "../../server/models/msg";
|
||||
import type {UserInMessage} from "../../shared/types/msg";
|
||||
import type {ClientChan, ClientUser} from "../js/types";
|
||||
import Username from "./Username.vue";
|
||||
|
||||
|
@ -104,7 +104,7 @@ export default defineComponent({
|
|||
const result = filteredUsers.value;
|
||||
|
||||
for (const user of result) {
|
||||
const mode = user.original.modes[0] || "";
|
||||
const mode: string = user.original.modes[0] || "";
|
||||
|
||||
if (!groups[mode]) {
|
||||
groups[mode] = [];
|
||||
|
|
|
@ -41,9 +41,9 @@
|
|||
<script lang="ts">
|
||||
import Mousetrap from "mousetrap";
|
||||
import {computed, defineComponent, ref, watch} from "vue";
|
||||
import {onBeforeRouteLeave, onBeforeRouteUpdate} from "vue-router";
|
||||
import eventbus from "../js/eventbus";
|
||||
import {ClientChan, ClientMessage, ClientLinkPreview} from "../js/types";
|
||||
import {ClientChan, ClientLinkPreview} from "../js/types";
|
||||
import {SharedMsg} from "../../shared/types/msg";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ImageViewer",
|
||||
|
@ -104,9 +104,9 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
const links = channel.value.messages
|
||||
.map((msg) => msg.previews)
|
||||
.map((msg: SharedMsg) => msg.previews)
|
||||
.flat()
|
||||
.filter((preview) => preview.thumb);
|
||||
.filter((preview) => preview && preview.thumb);
|
||||
|
||||
const currentIndex = links.indexOf(link.value);
|
||||
|
||||
|
|
|
@ -150,10 +150,14 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
const messageComponent = computed(() => {
|
||||
return "message-" + props.message.type;
|
||||
return "message-" + (props.message.type || "invalid"); // TODO: force existence of type in sharedmsg
|
||||
});
|
||||
|
||||
const isAction = () => {
|
||||
if (!props.message.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return typeof MessageTypes["message-" + props.message.type] !== "undefined";
|
||||
};
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
<script lang="ts">
|
||||
import {computed, defineComponent, PropType, ref} from "vue";
|
||||
import {condensedTypes} from "../../shared/irc";
|
||||
import {MessageType} from "../../shared/types/msg";
|
||||
import {ClientMessage, ClientNetwork} from "../js/types";
|
||||
import Message from "./Message.vue";
|
||||
|
||||
|
@ -57,16 +58,23 @@ export default defineComponent({
|
|||
|
||||
for (const message of props.messages) {
|
||||
// special case since one MODE message can change multiple modes
|
||||
if (message.type === "mode") {
|
||||
if (message.type === MessageType.MODE) {
|
||||
// syntax: +vv-t maybe-some targets
|
||||
// we want the number of mode changes in the message, so count the
|
||||
// number of chars other than + and - before the first space
|
||||
const modeChangesCount = message.text
|
||||
const text = message.text ? message.text : "";
|
||||
const modeChangesCount = text
|
||||
.split(" ")[0]
|
||||
.split("")
|
||||
.filter((char) => char !== "+" && char !== "-").length;
|
||||
obj[message.type] += modeChangesCount;
|
||||
} else {
|
||||
if (!message.type) {
|
||||
/* eslint-disable no-console */
|
||||
console.log(`empty message type, this should not happen: ${message.id}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
obj[message.type]++;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,6 +59,8 @@
|
|||
|
||||
<script lang="ts">
|
||||
import {condensedTypes} from "../../shared/irc";
|
||||
import {ChanType} from "../../shared/types/chan";
|
||||
import {MessageType, SharedMsg} from "../../shared/types/msg";
|
||||
import eventbus from "../js/eventbus";
|
||||
import clipboard from "../js/clipboard";
|
||||
import socket from "../js/socket";
|
||||
|
@ -79,7 +81,6 @@ import {
|
|||
} from "vue";
|
||||
import {useStore} from "../js/store";
|
||||
import {ClientChan, ClientMessage, ClientNetwork, ClientLinkPreview} from "../js/types";
|
||||
import Msg from "../../server/models/msg";
|
||||
|
||||
type CondensedMessageContainer = {
|
||||
type: "condensed";
|
||||
|
@ -103,7 +104,7 @@ export default defineComponent({
|
|||
channel: {type: Object as PropType<ClientChan>, required: true},
|
||||
focused: Number,
|
||||
},
|
||||
setup(props, {emit}) {
|
||||
setup(props) {
|
||||
const store = useStore();
|
||||
|
||||
const chat = ref<HTMLDivElement | null>(null);
|
||||
|
@ -177,14 +178,14 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
const condensedMessages = computed(() => {
|
||||
if (props.channel.type !== "channel" && props.channel.type !== "query") {
|
||||
if (props.channel.type !== ChanType.CHANNEL && props.channel.type !== ChanType.QUERY) {
|
||||
return props.channel.messages;
|
||||
}
|
||||
|
||||
// If actions are hidden, just return a message list with them excluded
|
||||
if (store.state.settings.statusMessages === "hidden") {
|
||||
return props.channel.messages.filter(
|
||||
(message) => !condensedTypes.has(message.type)
|
||||
(message) => !condensedTypes.has(message.type || "")
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -200,7 +201,7 @@ export default defineComponent({
|
|||
for (const message of props.channel.messages) {
|
||||
// If this message is not condensable, or its an action affecting our user,
|
||||
// then just append the message to container and be done with it
|
||||
if (message.self || message.highlight || !condensedTypes.has(message.type)) {
|
||||
if (message.self || message.highlight || !condensedTypes.has(message.type || "")) {
|
||||
lastCondensedContainer = null;
|
||||
|
||||
condensed.push(message);
|
||||
|
@ -242,7 +243,7 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
const shouldDisplayDateMarker = (
|
||||
message: Msg | ClientMessage | CondensedMessageContainer,
|
||||
message: SharedMsg | CondensedMessageContainer,
|
||||
id: number
|
||||
) => {
|
||||
const previousMessage = condensedMessages.value[id - 1];
|
||||
|
@ -270,12 +271,13 @@ export default defineComponent({
|
|||
return false;
|
||||
};
|
||||
|
||||
const isPreviousSource = (currentMessage: ClientMessage | Msg, id: number) => {
|
||||
const isPreviousSource = (currentMessage: ClientMessage, id: number) => {
|
||||
const previousMessage = condensedMessages.value[id - 1];
|
||||
return !!(
|
||||
return (
|
||||
previousMessage &&
|
||||
currentMessage.type === "message" &&
|
||||
previousMessage.type === "message" &&
|
||||
currentMessage.type === MessageType.MESSAGE &&
|
||||
previousMessage.type === MessageType.MESSAGE &&
|
||||
currentMessage.from &&
|
||||
previousMessage.from &&
|
||||
currentMessage.from.nick === previousMessage.from.nick
|
||||
);
|
||||
|
|
|
@ -26,36 +26,43 @@ export default defineComponent({
|
|||
},
|
||||
setup(props) {
|
||||
const errorMessage = computed(() => {
|
||||
// TODO: enforce chan and nick fields so that we can get rid of that
|
||||
const chan = props.message.channel || "!UNKNOWN_CHAN";
|
||||
const nick = props.message.nick || "!UNKNOWN_NICK";
|
||||
|
||||
switch (props.message.error) {
|
||||
case "bad_channel_key":
|
||||
return `Cannot join ${props.message.channel} - Bad channel key.`;
|
||||
return `Cannot join ${chan} - Bad channel key.`;
|
||||
case "banned_from_channel":
|
||||
return `Cannot join ${props.message.channel} - You have been banned from the channel.`;
|
||||
return `Cannot join ${chan} - You have been banned from the channel.`;
|
||||
case "cannot_send_to_channel":
|
||||
return `Cannot send to channel ${props.message.channel}`;
|
||||
return `Cannot send to channel ${chan}`;
|
||||
case "channel_is_full":
|
||||
return `Cannot join ${props.message.channel} - Channel is full.`;
|
||||
return `Cannot join ${chan} - Channel is full.`;
|
||||
case "chanop_privs_needed":
|
||||
return "Cannot perform action: You're not a channel operator.";
|
||||
case "invite_only_channel":
|
||||
return `Cannot join ${props.message.channel} - Channel is invite only.`;
|
||||
return `Cannot join ${chan} - Channel is invite only.`;
|
||||
case "no_such_nick":
|
||||
return `User ${props.message.nick} hasn't logged in or does not exist.`;
|
||||
return `User ${nick} hasn't logged in or does not exist.`;
|
||||
case "not_on_channel":
|
||||
return "Cannot perform action: You're not on the channel.";
|
||||
case "password_mismatch":
|
||||
return "Password mismatch.";
|
||||
case "too_many_channels":
|
||||
return `Cannot join ${props.message.channel} - You've already reached the maximum number of channels allowed.`;
|
||||
return `Cannot join ${chan} - You've already reached the maximum number of channels allowed.`;
|
||||
case "unknown_command":
|
||||
return `Unknown command: ${props.message.command}`;
|
||||
// TODO: not having message.command should never happen, so force existence
|
||||
return `Unknown command: ${props.message.command || "!UNDEFINED_COMMAND_BUG"}`;
|
||||
case "user_not_in_channel":
|
||||
return `User ${props.message.nick} is not on the channel.`;
|
||||
return `User ${nick} is not on the channel.`;
|
||||
case "user_on_channel":
|
||||
return `User ${props.message.nick} is already on the channel.`;
|
||||
return `User ${nick} is already on the channel.`;
|
||||
default:
|
||||
if (props.message.reason) {
|
||||
return `${props.message.reason} (${props.message.error})`;
|
||||
return `${props.message.reason} (${
|
||||
props.message.error || "!UNDEFINED_ERR"
|
||||
})`;
|
||||
}
|
||||
|
||||
return props.message.error;
|
||||
|
|
|
@ -498,6 +498,7 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
watch(
|
||||
// eslint-disable-next-line
|
||||
() => props.defaults?.commands,
|
||||
() => {
|
||||
void nextTick(() => {
|
||||
|
@ -507,6 +508,7 @@ export default defineComponent({
|
|||
);
|
||||
|
||||
watch(
|
||||
// eslint-disable-next-line
|
||||
() => props.defaults?.tls,
|
||||
(isSecureChecked) => {
|
||||
const ports = [6667, 6697];
|
||||
|
|
|
@ -309,8 +309,7 @@ export default defineComponent({
|
|||
|
||||
moveItemInArray(store.state.networks, oldIndex, newIndex);
|
||||
|
||||
socket.emit("sort", {
|
||||
type: "networks",
|
||||
socket.emit("sort:networks", {
|
||||
order: store.state.networks.map((n) => n.uuid),
|
||||
});
|
||||
};
|
||||
|
@ -341,9 +340,8 @@ export default defineComponent({
|
|||
|
||||
moveItemInArray(netChan.network.channels, oldIndex, newIndex);
|
||||
|
||||
socket.emit("sort", {
|
||||
type: "channels",
|
||||
target: netChan.network.uuid,
|
||||
socket.emit("sort:channel", {
|
||||
network: netChan.network.uuid,
|
||||
order: netChan.network.channels.map((c) => c.id),
|
||||
});
|
||||
};
|
||||
|
|
|
@ -12,10 +12,10 @@
|
|||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, PropType} from "vue";
|
||||
import {UserInMessage} from "../../server/models/msg";
|
||||
import {UserInMessage} from "../../shared/types/msg";
|
||||
import eventbus from "../js/eventbus";
|
||||
import colorClass from "../js/helpers/colorClass";
|
||||
import type {ClientChan, ClientNetwork, ClientUser} from "../js/types";
|
||||
import type {ClientChan, ClientNetwork} from "../js/types";
|
||||
import {useStore} from "../js/store";
|
||||
|
||||
type UsernameUser = Partial<UserInMessage> & {
|
||||
|
|
|
@ -48,7 +48,7 @@ export default defineComponent({
|
|||
|
||||
watch(
|
||||
() => route.params.uuid,
|
||||
(newValue) => {
|
||||
() => {
|
||||
setNetworkData();
|
||||
}
|
||||
);
|
||||
|
|
|
@ -106,7 +106,7 @@ import type {ClientMessage} from "../../js/types";
|
|||
import {useStore} from "../../js/store";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import {switchToChannel} from "../../js/router";
|
||||
import {SearchQuery} from "../../../server/plugins/messageStorage/types";
|
||||
import {SearchQuery} from "../../../shared/types/storage";
|
||||
|
||||
export default defineComponent({
|
||||
name: "SearchResults",
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import constants from "./constants";
|
||||
|
||||
import Mousetrap from "mousetrap";
|
||||
import {Strategy, Textcomplete, StrategyProps} from "@textcomplete/core";
|
||||
import {Textcomplete, StrategyProps} from "@textcomplete/core";
|
||||
import {TextareaEditor} from "@textcomplete/textarea";
|
||||
|
||||
import fuzzy from "fuzzy";
|
||||
|
||||
import emojiMap from "./helpers/simplemap.json";
|
||||
import {store} from "./store";
|
||||
import {ChanType} from "../../shared/types/chan";
|
||||
|
||||
export default enableAutocomplete;
|
||||
|
||||
|
@ -38,7 +39,6 @@ const nicksStrategy: StrategyProps = {
|
|||
|
||||
if (term[0] === "@") {
|
||||
// TODO: type
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
||||
callback(completeNicks(term.slice(1), true).map((val) => ["@" + val[0], "@" + val[1]]));
|
||||
} else {
|
||||
callback(completeNicks(term, true));
|
||||
|
@ -292,7 +292,7 @@ function rawNicks() {
|
|||
const otherUser = store.state.activeChannel.channel.name;
|
||||
|
||||
// If this is a query, add their name to autocomplete
|
||||
if (me !== otherUser && store.state.activeChannel.channel.type === "query") {
|
||||
if (me !== otherUser && store.state.activeChannel.channel.type === ChanType.QUERY) {
|
||||
return [otherUser, me];
|
||||
}
|
||||
|
||||
|
@ -332,7 +332,7 @@ function completeChans(word: string) {
|
|||
if (store.state.activeChannel) {
|
||||
for (const channel of store.state.activeChannel.network.channels) {
|
||||
// Push all channels that start with the same CHANTYPE
|
||||
if (channel.type === "channel" && channel.name[0] === word[0]) {
|
||||
if (channel.type === ChanType.CHANNEL && channel.name[0] === word[0]) {
|
||||
words.push(channel.name);
|
||||
}
|
||||
}
|
||||
|
|
36
client/js/chan.ts
Normal file
36
client/js/chan.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import {ClientChan, ClientMessage} from "./types";
|
||||
import {SharedNetworkChan} from "../../shared/types/network";
|
||||
import {SharedMsg, MessageType} from "../../shared/types/msg";
|
||||
import {ChanType} from "../../shared/types/chan";
|
||||
|
||||
export function toClientChan(shared: SharedNetworkChan): ClientChan {
|
||||
const history: string[] = [""].concat(
|
||||
shared.messages
|
||||
.filter((m) => m.self && m.text && m.type === MessageType.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 === ChanType.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;
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
|
||||
function input() {
|
||||
export function input(): boolean {
|
||||
if (!store.state.activeChannel) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const messageIds: number[] = [];
|
||||
|
@ -11,7 +11,7 @@ function input() {
|
|||
for (const message of store.state.activeChannel.channel.messages) {
|
||||
let toggled = false;
|
||||
|
||||
for (const preview of message.previews) {
|
||||
for (const preview of message.previews || []) {
|
||||
if (preview.shown) {
|
||||
preview.shown = false;
|
||||
toggled = true;
|
||||
|
@ -34,5 +34,3 @@ function input() {
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default {input};
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
|
||||
function input() {
|
||||
export function input(): boolean {
|
||||
if (!store.state.activeChannel) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const messageIds: number[] = [];
|
||||
|
@ -11,7 +11,7 @@ function input() {
|
|||
for (const message of store.state.activeChannel.channel.messages) {
|
||||
let toggled = false;
|
||||
|
||||
for (const preview of message.previews) {
|
||||
for (const preview of message.previews || []) {
|
||||
if (!preview.shown) {
|
||||
preview.shown = true;
|
||||
toggled = true;
|
||||
|
@ -34,5 +34,3 @@ function input() {
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default {input};
|
||||
|
|
|
@ -1,19 +1,11 @@
|
|||
// Taken from views/index.js
|
||||
import {input as collapse} from "./collapse";
|
||||
import {input as expand} from "./expand";
|
||||
import {input as join} from "./join";
|
||||
import {input as search} from "./search";
|
||||
|
||||
// This creates a version of `require()` in the context of the current
|
||||
// directory, so we iterate over its content, which is a map statically built by
|
||||
// Webpack.
|
||||
// Second argument says it's recursive, third makes sure we only load javascript.
|
||||
const commands = require.context("./", true, /\.ts$/);
|
||||
|
||||
export default commands.keys().reduce<Record<string, unknown>>((acc, path) => {
|
||||
const command = path.substring(2, path.length - 3);
|
||||
|
||||
if (command === "index") {
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc[command] = commands(path).default;
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
export const commands = {
|
||||
collapse: collapse,
|
||||
expand: expand,
|
||||
join: join,
|
||||
search: search,
|
||||
};
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
import {switchToChannel} from "../router";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
function input(args: string[]) {
|
||||
export function input(args: string[]): boolean {
|
||||
if (args.length > 0) {
|
||||
let channels = args[0];
|
||||
|
||||
|
@ -35,7 +36,7 @@ function input(args: string[]) {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
} else if (store.state.activeChannel?.channel.type === "channel") {
|
||||
} else if (store.state.activeChannel?.channel.type === ChanType.CHANNEL) {
|
||||
// If `/join` command is used without any arguments, re-join current channel
|
||||
socket.emit("input", {
|
||||
target: store.state.activeChannel.channel.id,
|
||||
|
@ -44,6 +45,6 @@ function input(args: string[]) {
|
|||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default {input};
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {store} from "../store";
|
||||
import {router} from "../router";
|
||||
|
||||
function input(args: string[]) {
|
||||
export function input(args: string[]): boolean {
|
||||
if (!store.state.settings.searchEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
@ -23,5 +23,3 @@ function input(args: string[]) {
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default {input};
|
||||
|
|
|
@ -4,6 +4,7 @@ import type {ClientChan, ClientNetwork, ClientUser} from "../types";
|
|||
import {switchToChannel} from "../router";
|
||||
import {TypedStore} from "../store";
|
||||
import useCloseChannel from "../hooks/use-close-channel";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
type BaseContextMenuItem = {
|
||||
label: string;
|
||||
|
@ -61,7 +62,7 @@ export function generateChannelContextMenu(
|
|||
];
|
||||
|
||||
// Add menu items for lobbies
|
||||
if (channel.type === "lobby") {
|
||||
if (channel.type === ChanType.LOBBY) {
|
||||
items = [
|
||||
...items,
|
||||
{
|
||||
|
@ -121,7 +122,7 @@ export function generateChannelContextMenu(
|
|||
}
|
||||
|
||||
// Add menu items for channels
|
||||
if (channel.type === "channel") {
|
||||
if (channel.type === ChanType.CHANNEL) {
|
||||
items.push({
|
||||
label: "Edit topic",
|
||||
type: "item",
|
||||
|
@ -145,7 +146,7 @@ export function generateChannelContextMenu(
|
|||
}
|
||||
|
||||
// Add menu items for queries
|
||||
if (channel.type === "query") {
|
||||
if (channel.type === ChanType.QUERY) {
|
||||
items.push(
|
||||
{
|
||||
label: "User information",
|
||||
|
@ -173,7 +174,7 @@ export function generateChannelContextMenu(
|
|||
);
|
||||
}
|
||||
|
||||
if (channel.type === "channel" || channel.type === "query") {
|
||||
if (channel.type === ChanType.CHANNEL || channel.type === ChanType.QUERY) {
|
||||
items.push({
|
||||
label: "Clear history",
|
||||
type: "item",
|
||||
|
|
|
@ -185,8 +185,7 @@ function parse(text: string, message?: ClientMessage, network?: ClientNetwork) {
|
|||
} else if (textPart.emoji) {
|
||||
const emojiWithoutModifiers = textPart.emoji.replace(emojiModifiersRegex, "");
|
||||
const title = emojiMap[emojiWithoutModifiers]
|
||||
? // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
`Emoji: ${emojiMap[emojiWithoutModifiers]}`
|
||||
? `Emoji: ${emojiMap[emojiWithoutModifiers]}`
|
||||
: null;
|
||||
|
||||
return createElement(
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import eventbus from "../eventbus";
|
||||
import socket from "../socket";
|
||||
import {ClientChan} from "../types";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
export default function useCloseChannel(channel: ClientChan) {
|
||||
return () => {
|
||||
if (channel.type === "lobby") {
|
||||
if (channel.type === ChanType.LOBBY) {
|
||||
eventbus.emit(
|
||||
"confirm-dialog",
|
||||
{
|
||||
|
|
|
@ -6,6 +6,7 @@ import isChannelCollapsed from "./helpers/isChannelCollapsed";
|
|||
import isIgnoredKeybind from "./helpers/isIgnoredKeybind";
|
||||
import listenForTwoFingerSwipes from "./helpers/listenForTwoFingerSwipes";
|
||||
import {ClientChan} from "./types";
|
||||
import {ChanType} from "../../shared/types/chan";
|
||||
|
||||
// Switch to the next/previous window in the channel list.
|
||||
Mousetrap.bind(["alt+up", "alt+down"], function (e, keys) {
|
||||
|
@ -73,7 +74,7 @@ Mousetrap.bind(["alt+shift+up", "alt+shift+down"], function (e, keys) {
|
|||
index = store.state.networks.findIndex((n) => n === store.state.activeChannel?.network);
|
||||
|
||||
// If we're in a channel, and it's not the lobby, jump to lobby of this network when going up
|
||||
if (direction !== -1 || store.state.activeChannel?.channel.type === "lobby") {
|
||||
if (direction !== -1 || store.state.activeChannel?.channel.type === ChanType.LOBBY) {
|
||||
index = (((index + direction) % length) + length) % length;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -162,7 +162,6 @@ async function navigate(routeName: string, params: any = {}) {
|
|||
// If current route is null, replace the history entry
|
||||
// This prevents invalid entries from lingering in history,
|
||||
// and then the route guard preventing proper navigation
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
await router.replace({name: routeName, params}).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ import socket from "./socket";
|
|||
import type {TypedStore} from "./store";
|
||||
|
||||
const defaultSettingConfig = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
apply() {},
|
||||
default: null,
|
||||
sync: null,
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import {nextTick} from "vue";
|
||||
import socket from "../socket";
|
||||
import storage from "../localStorage";
|
||||
import {toClientChan} from "../chan";
|
||||
import {router, switchToChannel, navigate} from "../router";
|
||||
import {store} from "../store";
|
||||
import parseIrcUri from "../helpers/parseIrcUri";
|
||||
import {ClientNetwork, InitClientChan} from "../types";
|
||||
import {ClientNetwork, ClientChan} from "../types";
|
||||
import {SharedNetwork, SharedNetworkChan} from "../../../shared/types/network";
|
||||
|
||||
socket.on("init", async function (data) {
|
||||
store.commit("networks", mergeNetworkData(data.networks));
|
||||
|
@ -31,54 +32,54 @@ socket.on("init", async function (data) {
|
|||
window.g_TheLoungeRemoveLoading();
|
||||
}
|
||||
|
||||
const handledQuery = await handleQueryParams();
|
||||
if (await handleQueryParams()) {
|
||||
// If we handled query parameters like irc:// links or just general
|
||||
// connect parameters in public mode, then nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
// If we handled query parameters like irc:// links or just general
|
||||
// connect parameters in public mode, then nothing to do here
|
||||
if (!handledQuery) {
|
||||
// If we are on an unknown route or still on SignIn component
|
||||
// then we can open last known channel on server, or Connect window if none
|
||||
if (
|
||||
!router.currentRoute?.value?.name ||
|
||||
router.currentRoute?.value?.name === "SignIn"
|
||||
) {
|
||||
const channel = store.getters.findChannel(data.active);
|
||||
// If we are on an unknown route or still on SignIn component
|
||||
// then we can open last known channel on server, or Connect window if none
|
||||
if (!router.currentRoute?.value?.name || router.currentRoute?.value?.name === "SignIn") {
|
||||
const channel = store.getters.findChannel(data.active);
|
||||
|
||||
if (channel) {
|
||||
switchToChannel(channel.channel);
|
||||
} else if (store.state.networks.length > 0) {
|
||||
// Server is telling us to open a channel that does not exist
|
||||
// For example, it can be unset if you first open the page after server start
|
||||
switchToChannel(store.state.networks[0].channels[0]);
|
||||
} else {
|
||||
await navigate("Connect");
|
||||
}
|
||||
if (channel) {
|
||||
switchToChannel(channel.channel);
|
||||
} else if (store.state.networks.length > 0) {
|
||||
// Server is telling us to open a channel that does not exist
|
||||
// For example, it can be unset if you first open the page after server start
|
||||
switchToChannel(store.state.networks[0].channels[0]);
|
||||
} else {
|
||||
await navigate("Connect");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function mergeNetworkData(newNetworks: ClientNetwork[]) {
|
||||
function mergeNetworkData(newNetworks: SharedNetwork[]): ClientNetwork[] {
|
||||
const stored = storage.get("thelounge.networks.collapsed");
|
||||
const collapsedNetworks = stored ? new Set(JSON.parse(stored)) : new Set();
|
||||
const result: ReturnType<typeof mergeNetworkData> = [];
|
||||
|
||||
for (let n = 0; n < newNetworks.length; n++) {
|
||||
const network = newNetworks[n];
|
||||
const currentNetwork = store.getters.findNetwork(network.uuid);
|
||||
for (const sharedNet of newNetworks) {
|
||||
const currentNetwork = store.getters.findNetwork(sharedNet.uuid);
|
||||
|
||||
// If this network is new, set some default variables and initalize channel variables
|
||||
if (!currentNetwork) {
|
||||
network.isJoinChannelShown = false;
|
||||
network.isCollapsed = collapsedNetworks.has(network.uuid);
|
||||
network.channels.forEach(store.getters.initChannel);
|
||||
|
||||
const newNet: ClientNetwork = {
|
||||
...sharedNet,
|
||||
channels: sharedNet.channels.map(toClientChan),
|
||||
isJoinChannelShown: false,
|
||||
isCollapsed: collapsedNetworks.has(sharedNet.uuid),
|
||||
};
|
||||
result.push(newNet);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Merge received network object into existing network object on the client
|
||||
// so the object reference stays the same (e.g. for currentChannel state)
|
||||
for (const key in network) {
|
||||
if (!Object.prototype.hasOwnProperty.call(network, key)) {
|
||||
for (const key in sharedNet) {
|
||||
if (!Object.prototype.hasOwnProperty.call(sharedNet, key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -86,81 +87,82 @@ function mergeNetworkData(newNetworks: ClientNetwork[]) {
|
|||
if (key === "channels") {
|
||||
currentNetwork.channels = mergeChannelData(
|
||||
currentNetwork.channels,
|
||||
network.channels as InitClientChan[]
|
||||
sharedNet.channels
|
||||
);
|
||||
} else {
|
||||
currentNetwork[key] = network[key];
|
||||
currentNetwork[key] = sharedNet[key];
|
||||
}
|
||||
}
|
||||
|
||||
newNetworks[n] = currentNetwork;
|
||||
result.push(currentNetwork);
|
||||
}
|
||||
|
||||
return newNetworks;
|
||||
return result;
|
||||
}
|
||||
|
||||
function mergeChannelData(oldChannels: InitClientChan[], newChannels: InitClientChan[]) {
|
||||
for (let c = 0; c < newChannels.length; c++) {
|
||||
const channel = newChannels[c];
|
||||
const currentChannel = oldChannels.find((chan) => chan.id === channel.id);
|
||||
function mergeChannelData(
|
||||
oldChannels: ClientChan[],
|
||||
newChannels: SharedNetworkChan[]
|
||||
): ClientChan[] {
|
||||
const result: ReturnType<typeof mergeChannelData> = [];
|
||||
|
||||
for (const newChannel of newChannels) {
|
||||
const currentChannel = oldChannels.find((chan) => chan.id === newChannel.id);
|
||||
|
||||
// This is a new channel that was joined while client was disconnected, initialize it
|
||||
if (!currentChannel) {
|
||||
store.getters.initChannel(channel);
|
||||
|
||||
// This is a new channel that was joined while client was disconnected, initialize it
|
||||
const current = toClientChan(newChannel);
|
||||
result.push(current);
|
||||
emitNamesOrMarkUsersOudated(current); // TODO: this should not carry logic like that
|
||||
continue;
|
||||
}
|
||||
|
||||
// Merge received channel object into existing currentChannel
|
||||
// so the object references are exactly the same (e.g. in store.state.activeChannel)
|
||||
for (const key in channel) {
|
||||
if (!Object.prototype.hasOwnProperty.call(channel, key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Server sends an empty users array, client requests it whenever needed
|
||||
if (key === "users") {
|
||||
if (channel.type === "channel") {
|
||||
if (
|
||||
store.state.activeChannel &&
|
||||
store.state.activeChannel.channel === currentChannel
|
||||
) {
|
||||
// For currently open channel, request the user list straight away
|
||||
socket.emit("names", {
|
||||
target: channel.id,
|
||||
});
|
||||
} else {
|
||||
// For all other channels, mark the user list as outdated
|
||||
// so an update will be requested whenever user switches to these channels
|
||||
currentChannel.usersOutdated = true;
|
||||
}
|
||||
}
|
||||
emitNamesOrMarkUsersOudated(currentChannel); // TODO: this should not carry logic like that
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Server sends total count of messages in memory, we compare it to amount of messages
|
||||
// on the client, and decide whether theres more messages to load from server
|
||||
if (key === "totalMessages") {
|
||||
currentChannel.moreHistoryAvailable =
|
||||
channel.totalMessages! > currentChannel.messages.length;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reconnection only sends new messages, so merge it on the client
|
||||
// Only concat if server sent us less than 100 messages so we don't introduce gaps
|
||||
if (key === "messages" && currentChannel.messages && channel.messages.length < 100) {
|
||||
currentChannel.messages = currentChannel.messages.concat(channel.messages);
|
||||
} else {
|
||||
currentChannel[key] = channel[key];
|
||||
}
|
||||
// Reconnection only sends new messages, so merge it on the client
|
||||
// Only concat if server sent us less than 100 messages so we don't introduce gaps
|
||||
if (currentChannel.messages && newChannel.messages.length < 100) {
|
||||
currentChannel.messages = currentChannel.messages.concat(newChannel.messages);
|
||||
} else {
|
||||
currentChannel.messages = newChannel.messages;
|
||||
}
|
||||
|
||||
newChannels[c] = currentChannel;
|
||||
// TODO: this is copies more than what the compiler knows about
|
||||
for (const key in newChannel) {
|
||||
if (!Object.hasOwn(currentChannel, key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key === "messages") {
|
||||
// already handled
|
||||
continue;
|
||||
}
|
||||
|
||||
currentChannel[key] = newChannel[key];
|
||||
}
|
||||
|
||||
result.push(currentChannel);
|
||||
}
|
||||
|
||||
return newChannels;
|
||||
return result;
|
||||
}
|
||||
|
||||
function emitNamesOrMarkUsersOudated(chan: ClientChan) {
|
||||
if (store.state.activeChannel && store.state.activeChannel.channel === chan) {
|
||||
// For currently open channel, request the user list straight away
|
||||
socket.emit("names", {
|
||||
target: chan.id,
|
||||
});
|
||||
chan.usersOutdated = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// For all other channels, mark the user list as outdated
|
||||
// so an update will be requested whenever user switches to these channels
|
||||
chan.usersOutdated = true;
|
||||
}
|
||||
|
||||
async function handleQueryParams() {
|
||||
|
@ -170,30 +172,28 @@ async function handleQueryParams() {
|
|||
|
||||
const params = new URLSearchParams(document.location.search);
|
||||
|
||||
const cleanParams = () => {
|
||||
// Remove query parameters from url without reloading the page
|
||||
const cleanUri = window.location.origin + window.location.pathname + window.location.hash;
|
||||
window.history.replaceState({}, document.title, cleanUri);
|
||||
};
|
||||
|
||||
if (params.has("uri")) {
|
||||
// Set default connection settings from IRC protocol links
|
||||
const uri = params.get("uri");
|
||||
const queryParams = parseIrcUri(String(uri));
|
||||
|
||||
cleanParams();
|
||||
removeQueryParams();
|
||||
await router.push({name: "Connect", query: queryParams});
|
||||
|
||||
return true;
|
||||
} else if (document.body.classList.contains("public") && document.location.search) {
|
||||
}
|
||||
|
||||
if (document.body.classList.contains("public") && document.location.search) {
|
||||
// Set default connection settings from url params
|
||||
const queryParams = Object.fromEntries(params.entries());
|
||||
|
||||
cleanParams();
|
||||
removeQueryParams();
|
||||
await router.push({name: "Connect", query: queryParams});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove query parameters from url without reloading the page
|
||||
function removeQueryParams() {
|
||||
const cleanUri = window.location.origin + window.location.pathname + window.location.hash;
|
||||
window.history.replaceState(null, "", cleanUri);
|
||||
}
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
import {switchToChannel} from "../router";
|
||||
import {ClientChan} from "../types";
|
||||
import {toClientChan} from "../chan";
|
||||
|
||||
socket.on("join", function (data) {
|
||||
store.getters.initChannel(data.chan);
|
||||
|
||||
const network = store.getters.findNetwork(data.network);
|
||||
|
||||
if (!network) {
|
||||
return;
|
||||
}
|
||||
|
||||
network.channels.splice(data.index || -1, 0, data.chan);
|
||||
const clientChan: ClientChan = toClientChan(data.chan);
|
||||
network.channels.splice(data.index || -1, 0, clientChan);
|
||||
|
||||
// Queries do not automatically focus, unless the user did a whois
|
||||
if (data.chan.type === "query" && !data.shouldOpen) {
|
||||
|
|
|
@ -1,7 +1,17 @@
|
|||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
import {ClientMention} from "../types";
|
||||
import {SharedMention} from "../../../shared/types/mention";
|
||||
|
||||
socket.on("mentions:list", function (data) {
|
||||
store.commit("mentions", data as ClientMention[]);
|
||||
store.commit("mentions", data.map(sharedToClientMention));
|
||||
});
|
||||
|
||||
function sharedToClientMention(shared: SharedMention): ClientMention {
|
||||
const mention: ClientMention = {
|
||||
...shared,
|
||||
localetime: "", // TODO: can't be right
|
||||
channel: null,
|
||||
};
|
||||
return mention;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import {nextTick} from "vue";
|
|||
|
||||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
import {ClientMessage} from "../types";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
socket.on("more", async (data) => {
|
||||
const channel = store.getters.findChannel(data.chan)?.channel;
|
||||
|
@ -13,14 +13,16 @@ socket.on("more", async (data) => {
|
|||
|
||||
channel.inputHistory = channel.inputHistory.concat(
|
||||
data.messages
|
||||
.filter((m) => m.self && m.text && m.type === "message")
|
||||
.map((m) => m.text)
|
||||
.filter((m) => m.self && m.text && m.type === MessageType.MESSAGE)
|
||||
// TS is too stupid to see the guard in .filter(), so we monkey patch it
|
||||
// to please the compiler
|
||||
.map((m) => (m.text ? m.text : ""))
|
||||
.reverse()
|
||||
.slice(0, 100 - channel.inputHistory.length)
|
||||
);
|
||||
channel.moreHistoryAvailable =
|
||||
data.totalMessages > channel.messages.length + data.messages.length;
|
||||
channel.messages.unshift(...(data.messages as ClientMessage[]));
|
||||
channel.messages.unshift(...data.messages);
|
||||
|
||||
await nextTick();
|
||||
channel.historyLoading = false;
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
/* eslint-disable @typescript-eslint/restrict-plus-operands */
|
||||
import socket from "../socket";
|
||||
import {cleanIrcMessage} from "../../../shared/irc";
|
||||
import {store} from "../store";
|
||||
import {switchToChannel} from "../router";
|
||||
import {ClientChan, ClientMention, ClientMessage, NetChan} from "../types";
|
||||
import {ClientChan, NetChan, ClientMessage} from "../types";
|
||||
import {SharedMsg, MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
let pop;
|
||||
|
||||
|
@ -12,7 +13,6 @@ try {
|
|||
pop.src = "audio/pop.wav";
|
||||
} catch (e) {
|
||||
pop = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
play() {},
|
||||
};
|
||||
}
|
||||
|
@ -90,11 +90,19 @@ socket.on("msg", function (data) {
|
|||
channel.moreHistoryAvailable = true;
|
||||
}
|
||||
|
||||
if (channel.type === "channel") {
|
||||
if (channel.type === ChanType.CHANNEL) {
|
||||
updateUserList(channel, data.msg);
|
||||
}
|
||||
});
|
||||
|
||||
declare global {
|
||||
// this extends the interface from lib.dom with additional stuff which is not
|
||||
// exactly standard but implemented in some browsers
|
||||
interface NotificationOptions {
|
||||
timestamp?: number; // chrome has it, other browsers ignore it
|
||||
}
|
||||
}
|
||||
|
||||
function notifyMessage(
|
||||
targetId: number,
|
||||
channel: ClientChan,
|
||||
|
@ -105,7 +113,10 @@ function notifyMessage(
|
|||
return;
|
||||
}
|
||||
|
||||
if (msg.highlight || (store.state.settings.notifyAllMessages && msg.type === "message")) {
|
||||
if (
|
||||
msg.highlight ||
|
||||
(store.state.settings.notifyAllMessages && msg.type === MessageType.MESSAGE)
|
||||
) {
|
||||
if (!document.hasFocus() || !activeChannel || activeChannel.channel !== channel) {
|
||||
if (store.state.settings.notification) {
|
||||
try {
|
||||
|
@ -122,22 +133,25 @@ function notifyMessage(
|
|||
) {
|
||||
let title: string;
|
||||
let body: string;
|
||||
// TODO: fix msg type and get rid of that conditional
|
||||
const nick = msg.from && msg.from.nick ? msg.from.nick : "unkonown";
|
||||
|
||||
if (msg.type === "invite") {
|
||||
if (msg.type === MessageType.INVITE) {
|
||||
title = "New channel invite:";
|
||||
body = msg.from.nick + " invited you to " + msg.channel;
|
||||
body = nick + " invited you to " + msg.channel;
|
||||
} else {
|
||||
title = String(msg.from.nick);
|
||||
title = nick;
|
||||
|
||||
if (channel.type !== "query") {
|
||||
if (channel.type !== ChanType.QUERY) {
|
||||
title += ` (${channel.name})`;
|
||||
}
|
||||
|
||||
if (msg.type === "message") {
|
||||
if (msg.type === MessageType.MESSAGE) {
|
||||
title += " says:";
|
||||
}
|
||||
|
||||
body = cleanIrcMessage(msg.text);
|
||||
// TODO: fix msg type and get rid of that conditional
|
||||
body = cleanIrcMessage(msg.text ? msg.text : "");
|
||||
}
|
||||
|
||||
const timestamp = Date.parse(String(msg.time));
|
||||
|
@ -184,24 +198,40 @@ function notifyMessage(
|
|||
}
|
||||
}
|
||||
|
||||
function updateUserList(channel, msg) {
|
||||
if (msg.type === "message" || msg.type === "action") {
|
||||
const user = channel.users.find((u) => u.nick === msg.from.nick);
|
||||
function updateUserList(channel: ClientChan, msg: SharedMsg) {
|
||||
switch (msg.type) {
|
||||
case MessageType.MESSAGE: // fallthrough
|
||||
|
||||
if (user) {
|
||||
user.lastMessage = new Date(msg.time).getTime() || Date.now();
|
||||
case MessageType.ACTION: {
|
||||
const user = channel.users.find((u) => u.nick === msg.from?.nick);
|
||||
|
||||
if (user) {
|
||||
user.lastMessage = new Date(msg.time).getTime() || Date.now();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
} else if (msg.type === "quit" || msg.type === "part") {
|
||||
const idx = channel.users.findIndex((u) => u.nick === msg.from.nick);
|
||||
|
||||
if (idx > -1) {
|
||||
channel.users.splice(idx, 1);
|
||||
case MessageType.QUIT: // fallthrough
|
||||
|
||||
case MessageType.PART: {
|
||||
const idx = channel.users.findIndex((u) => u.nick === msg.from?.nick);
|
||||
|
||||
if (idx > -1) {
|
||||
channel.users.splice(idx, 1);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
} else if (msg.type === "kick") {
|
||||
const idx = channel.users.findIndex((u) => u.nick === msg.target.nick);
|
||||
|
||||
if (idx > -1) {
|
||||
channel.users.splice(idx, 1);
|
||||
case MessageType.KICK: {
|
||||
const idx = channel.users.findIndex((u) => u.nick === msg.target?.nick);
|
||||
|
||||
if (idx > -1) {
|
||||
channel.users.splice(idx, 1);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ socket.on("msg:preview", function (data) {
|
|||
const netChan = store.getters.findChannel(data.chan);
|
||||
const message = netChan?.channel.messages.find((m) => m.id === data.id);
|
||||
|
||||
if (!message) {
|
||||
if (!message || !message.previews) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
socket.on("mute:changed", (response) => {
|
||||
const {target, status} = response;
|
||||
|
||||
const netChan = store.getters.findChannel(target);
|
||||
|
||||
if (netChan?.channel.type === "lobby") {
|
||||
if (netChan?.channel.type === ChanType.LOBBY) {
|
||||
for (const chan of netChan.network.channels) {
|
||||
if (chan.type !== "special") {
|
||||
if (chan.type !== ChanType.SPECIAL) {
|
||||
chan.muted = status;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
import {switchToChannel} from "../router";
|
||||
import {toClientChan} from "../chan";
|
||||
import {ClientNetwork} from "../types";
|
||||
import {ChanState} from "../../../shared/types/chan";
|
||||
|
||||
socket.on("network", function (data) {
|
||||
const network = data.networks[0];
|
||||
|
||||
network.isJoinChannelShown = false;
|
||||
network.isCollapsed = false;
|
||||
network.channels.forEach(store.getters.initChannel);
|
||||
const network: ClientNetwork = {
|
||||
...data.network,
|
||||
channels: data.network.channels.map(toClientChan),
|
||||
isJoinChannelShown: false,
|
||||
isCollapsed: false,
|
||||
};
|
||||
|
||||
store.commit("networks", [...store.state.networks, network]);
|
||||
|
||||
|
@ -19,7 +23,7 @@ socket.on("network:options", function (data) {
|
|||
const network = store.getters.findNetwork(data.network);
|
||||
|
||||
if (network) {
|
||||
network.serverOptions = data.serverOptions as typeof network.serverOptions;
|
||||
network.serverOptions = data.serverOptions;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -35,8 +39,8 @@ socket.on("network:status", function (data) {
|
|||
|
||||
if (!data.connected) {
|
||||
network.channels.forEach((channel) => {
|
||||
channel.users = [];
|
||||
channel.state = 0;
|
||||
channel.users = []; // TODO: untangle this
|
||||
channel.state = ChanState.PARTED;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,30 +1,16 @@
|
|||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
|
||||
socket.on("sync_sort", function (data) {
|
||||
const order = data.order;
|
||||
|
||||
switch (data.type) {
|
||||
case "networks":
|
||||
store.commit(
|
||||
"sortNetworks",
|
||||
(a, b) => (order as string[]).indexOf(a.uuid) - (order as string[]).indexOf(b.uuid)
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case "channels": {
|
||||
const network = store.getters.findNetwork(data.target);
|
||||
|
||||
if (!network) {
|
||||
return;
|
||||
}
|
||||
|
||||
network.channels.sort(
|
||||
(a, b) => (order as number[]).indexOf(a.id) - (order as number[]).indexOf(b.id)
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
socket.on("sync_sort:networks", function (data) {
|
||||
store.commit("sortNetworks", (a, b) => data.order.indexOf(a.uuid) - data.order.indexOf(b.uuid));
|
||||
});
|
||||
|
||||
socket.on("sync_sort:channels", function (data) {
|
||||
const network = store.getters.findNetwork(data.network);
|
||||
|
||||
if (!network) {
|
||||
return;
|
||||
}
|
||||
|
||||
network.channels.sort((a, b) => data.order.indexOf(a.id) - data.order.indexOf(b.id));
|
||||
});
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import io, {Socket} from "socket.io-client";
|
||||
import type {ServerToClientEvents, ClientToServerEvents} from "../../server/types/socket-events";
|
||||
import io, {Socket as rawSocket} from "socket.io-client";
|
||||
import type {ServerToClientEvents, ClientToServerEvents} from "../../shared/types/socket-events";
|
||||
|
||||
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io({
|
||||
type Socket = rawSocket<ServerToClientEvents, ClientToServerEvents>;
|
||||
|
||||
const socket: Socket = io({
|
||||
transports: JSON.parse(document.body.dataset.transports || "['polling', 'websocket']"),
|
||||
path: window.location.pathname + "socket.io/",
|
||||
autoConnect: false,
|
||||
|
|
|
@ -3,19 +3,12 @@
|
|||
import {ActionContext, createStore, Store, useStore as baseUseStore} from "vuex";
|
||||
import {createSettingsStore} from "./store-settings";
|
||||
import storage from "./localStorage";
|
||||
import type {
|
||||
ClientChan,
|
||||
ClientConfiguration,
|
||||
ClientNetwork,
|
||||
InitClientChan,
|
||||
NetChan,
|
||||
ClientMessage,
|
||||
ClientMention,
|
||||
} from "./types";
|
||||
import type {ClientChan, ClientNetwork, NetChan, ClientMention, ClientMessage} from "./types";
|
||||
import type {InjectionKey} from "vue";
|
||||
|
||||
import {SettingsState} from "./settings";
|
||||
import {SearchQuery} from "../../server/plugins/messageStorage/types";
|
||||
import {SearchQuery} from "../../shared/types/storage";
|
||||
import {SharedConfiguration, LockedSharedConfiguration} from "../../shared/types/config";
|
||||
|
||||
const appName = document.title;
|
||||
|
||||
|
@ -59,7 +52,7 @@ export type State = {
|
|||
mentions: ClientMention[];
|
||||
hasServiceWorker: boolean;
|
||||
pushNotificationState: string;
|
||||
serverConfiguration: ClientConfiguration | null;
|
||||
serverConfiguration: SharedConfiguration | LockedSharedConfiguration | null;
|
||||
sessions: ClientSession[];
|
||||
sidebarOpen: boolean;
|
||||
sidebarDragging: boolean;
|
||||
|
@ -131,7 +124,6 @@ type Getters = {
|
|||
findNetwork: (state: State) => (uuid: string) => ClientNetwork | null;
|
||||
highlightCount(state: State): number;
|
||||
title(state: State, getters: Omit<Getters, "title">): string;
|
||||
initChannel: () => (channel: InitClientChan) => ClientChan;
|
||||
};
|
||||
|
||||
// getters without the state argument
|
||||
|
@ -202,31 +194,6 @@ const getters: Getters = {
|
|||
|
||||
return alertEventCount + channelname + appName;
|
||||
},
|
||||
initChannel: () => (channel: InitClientChan) => {
|
||||
// TODO: This should be a mutation
|
||||
channel.pendingMessage = "";
|
||||
channel.inputHistoryPosition = 0;
|
||||
|
||||
channel.inputHistory = [""].concat(
|
||||
channel.messages
|
||||
.filter((m) => m.self && m.text && m.type === "message")
|
||||
.map((m) => m.text)
|
||||
.reverse()
|
||||
.slice(0, 99)
|
||||
);
|
||||
channel.historyLoading = false;
|
||||
channel.scrolledToBottom = true;
|
||||
channel.editTopic = false;
|
||||
|
||||
channel.moreHistoryAvailable = channel.totalMessages! > channel.messages.length;
|
||||
delete channel.totalMessages;
|
||||
|
||||
if (channel.type === "channel") {
|
||||
channel.usersOutdated = true;
|
||||
}
|
||||
|
||||
return channel as ClientChan;
|
||||
},
|
||||
};
|
||||
|
||||
type Mutations = {
|
||||
|
|
40
client/js/types.d.ts
vendored
40
client/js/types.d.ts
vendored
|
@ -1,34 +1,25 @@
|
|||
import {defineComponent} from "vue";
|
||||
|
||||
import Chan from "../../server/models/chan";
|
||||
import Network from "../../server/models/network";
|
||||
import User from "../../server/models/user";
|
||||
import Message from "../../server/models/msg";
|
||||
import {Mention} from "../../server/client";
|
||||
import {ClientConfiguration} from "../../server/server";
|
||||
import {LinkPreview} from "../../server/plugins/irc-events/link";
|
||||
import {SharedChan} from "../../shared/types/chan";
|
||||
import {SharedNetwork} from "../../shared/types/network";
|
||||
import {SharedUser} from "../../shared/types/user";
|
||||
import {SharedMention} from "../../shared/types/mention";
|
||||
import {SharedConfiguration, LockedSharedConfiguration} from "../../shared/types/config";
|
||||
import {LinkPreview, SharedMsg} from "../../shared/types/msg";
|
||||
|
||||
interface LoungeWindow extends Window {
|
||||
g_TheLoungeRemoveLoading?: () => void;
|
||||
navigator: Window["navigator"] & {
|
||||
setAppBadge?: (highlightCount: number) => void;
|
||||
clearAppBadge?: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
type ClientUser = User & {
|
||||
//
|
||||
};
|
||||
type ClientUser = SharedUser;
|
||||
|
||||
type ClientMessage = Omit<Message, "users"> & {
|
||||
time: number;
|
||||
users: string[];
|
||||
};
|
||||
// we will eventually need to put client specific fields here
|
||||
// which are not shared with the server
|
||||
export type ClientMessage = SharedMsg;
|
||||
|
||||
type ClientChan = Omit<Chan, "users" | "messages"> & {
|
||||
type ClientChan = Omit<SharedChan, "messages"> & {
|
||||
moreHistoryAvailable: boolean;
|
||||
editTopic: boolean;
|
||||
users: ClientUser[];
|
||||
messages: ClientMessage[];
|
||||
|
||||
// these are added in store/initChannel
|
||||
|
@ -38,6 +29,8 @@ type ClientChan = Omit<Chan, "users" | "messages"> & {
|
|||
historyLoading: boolean;
|
||||
scrolledToBottom: boolean;
|
||||
usersOutdated: boolean;
|
||||
|
||||
users: ClientUser[];
|
||||
};
|
||||
|
||||
type InitClientChan = ClientChan & {
|
||||
|
@ -46,7 +39,7 @@ type InitClientChan = ClientChan & {
|
|||
};
|
||||
|
||||
// We omit channels so we can use ClientChan[] instead of Chan[]
|
||||
type ClientNetwork = Omit<Network, "channels"> & {
|
||||
type ClientNetwork = Omit<SharedNetwork, "channels"> & {
|
||||
isJoinChannelShown: boolean;
|
||||
isCollapsed: boolean;
|
||||
channels: ClientChan[];
|
||||
|
@ -57,9 +50,8 @@ type NetChan = {
|
|||
network: ClientNetwork;
|
||||
};
|
||||
|
||||
type ClientConfiguration = ClientConfiguration;
|
||||
type ClientMention = Mention & {
|
||||
localetime: string;
|
||||
type ClientMention = SharedMention & {
|
||||
localetime: string; // TODO: this needs to go the way of the dodo, nothing but a single component uses it
|
||||
channel: NetChan | null;
|
||||
};
|
||||
|
||||
|
|
|
@ -7,9 +7,9 @@ import App from "../components/App.vue";
|
|||
import storage from "./localStorage";
|
||||
import {router} from "./router";
|
||||
import socket from "./socket";
|
||||
import "./socket-events"; // this sets up all socket event listeners, do not remove
|
||||
import eventbus from "./eventbus";
|
||||
|
||||
import "./socket-events";
|
||||
import "./webpush";
|
||||
import "./keybinds";
|
||||
import {LoungeWindow} from "./types";
|
||||
|
@ -61,10 +61,10 @@ store.watch(
|
|||
|
||||
if (nav.setAppBadge) {
|
||||
if (highlightCount > 0) {
|
||||
nav.setAppBadge(highlightCount);
|
||||
nav.setAppBadge(highlightCount).catch(() => {});
|
||||
} else {
|
||||
if (nav.clearAppBadge) {
|
||||
nav.clearAppBadge();
|
||||
nav.clearAppBadge().catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,40 +6,8 @@
|
|||
] /* Specifies a list of glob patterns that match files to be included in compilation. If no 'files' or 'include' property is present in a tsconfig.json, the compiler defaults to including all files in the containing directory and subdirectories except those specified by 'exclude'. Requires TypeScript version 2.0 or later. */,
|
||||
"files": [
|
||||
"../package.json",
|
||||
"../server/types/socket-events.d.ts",
|
||||
"../server/helper.ts",
|
||||
"../server/log.ts",
|
||||
"../server/config.ts",
|
||||
"../server/client.ts",
|
||||
"../server/storageCleaner.ts",
|
||||
"../server/clientManager.ts",
|
||||
"../server/identification.ts",
|
||||
"../server/plugins/changelog.ts",
|
||||
"../server/plugins/uploader.ts",
|
||||
"../server/plugins/storage.ts",
|
||||
"../server/plugins/inputs/index.ts",
|
||||
"../server/plugins/messageStorage/sqlite.ts",
|
||||
"../server/plugins/messageStorage/text.ts",
|
||||
"../server/plugins/packages/index.ts",
|
||||
"../server/plugins/packages/publicClient.ts",
|
||||
"../server/plugins/packages/themes.ts",
|
||||
"../server/plugins/dev-server.ts",
|
||||
"../server/plugins/webpush.ts",
|
||||
"../server/plugins/sts.ts",
|
||||
"../server/plugins/clientCertificate.ts",
|
||||
"../server/plugins/auth.ts",
|
||||
"../server/plugins/auth/local.ts",
|
||||
"../server/plugins/auth/ldap.ts",
|
||||
"../server/plugins/irc-events/link.ts",
|
||||
"../server/command-line/utils.ts",
|
||||
"../server/models/network.ts",
|
||||
"../server/models/user.ts",
|
||||
"../server/models/msg.ts",
|
||||
"../server/models/prefix.ts",
|
||||
"./js/helpers/fullnamemap.json",
|
||||
"./js/helpers/simplemap.json",
|
||||
"../webpack.config.ts",
|
||||
"../babel.config.cjs"
|
||||
"./js/helpers/simplemap.json"
|
||||
] /* If no 'files' or 'include' property is present in a tsconfig.json, the compiler defaults to including all files in the containing directory and subdirectories except those specified by 'exclude'. When a 'files' property is specified, only those files and those specified by 'include' are included. */,
|
||||
// "exclude": [],
|
||||
"compilerOptions": {
|
||||
|
|
30
package.json
30
package.json
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "thelounge",
|
||||
"description": "The self-hosted Web IRC client",
|
||||
"version": "4.4.2",
|
||||
"version": "4.4.3",
|
||||
"preferGlobal": true,
|
||||
"bin": {
|
||||
"thelounge": "index.js"
|
||||
|
@ -61,7 +61,7 @@
|
|||
"cheerio": "1.0.0-rc.12",
|
||||
"commander": "9.0.0",
|
||||
"content-disposition": "0.5.4",
|
||||
"express": "4.17.3",
|
||||
"express": "4.19.2",
|
||||
"file-type": "16.5.4",
|
||||
"filenamify": "4.3.0",
|
||||
"got": "11.8.5",
|
||||
|
@ -98,7 +98,7 @@
|
|||
"@types/chai": "4.3.5",
|
||||
"@types/cheerio": "0.22.35",
|
||||
"@types/content-disposition": "0.5.8",
|
||||
"@types/express": "4.17.13",
|
||||
"@types/express": "4.17.21",
|
||||
"@types/is-utf8": "0.2.3",
|
||||
"@types/ldapjs": "2.2.2",
|
||||
"@types/linkify-it": "3.0.5",
|
||||
|
@ -107,18 +107,18 @@
|
|||
"@types/mocha": "9.1.1",
|
||||
"@types/mousetrap": "1.6.15",
|
||||
"@types/node": "17.0.45",
|
||||
"@types/read": "0.0.29",
|
||||
"@types/read": "0.0.32",
|
||||
"@types/semver": "7.3.9",
|
||||
"@types/sortablejs": "1.13.0",
|
||||
"@types/sqlite3": "3.1.8",
|
||||
"@types/ua-parser-js": "0.7.36",
|
||||
"@types/sqlite3": "3.1.11",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/uuid": "8.3.4",
|
||||
"@types/web-push": "3.3.2",
|
||||
"@types/webpack-env": "1.16.4",
|
||||
"@types/webpack-hot-middleware": "2.25.6",
|
||||
"@types/ws": "8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.22.0",
|
||||
"@typescript-eslint/parser": "5.22.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.8.0",
|
||||
"@typescript-eslint/parser": "7.8.0",
|
||||
"@vue/runtime-dom": "3.2.33",
|
||||
"@vue/test-utils": "2.4.0",
|
||||
"babel-loader": "8.2.5",
|
||||
|
@ -131,10 +131,10 @@
|
|||
"cssnano": "5.0.17",
|
||||
"dayjs": "1.10.8",
|
||||
"emoji-regex": "10.2.1",
|
||||
"eslint": "8.16.0",
|
||||
"eslint-config-prettier": "8.3.0",
|
||||
"eslint-define-config": "1.5.1",
|
||||
"eslint-plugin-vue": "9.0.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-define-config": "2.1.0",
|
||||
"eslint-plugin-vue": "9.25.0",
|
||||
"fork-ts-checker-webpack-plugin": "7.2.13",
|
||||
"fuzzy": "0.1.3",
|
||||
"mini-css-extract-plugin": "2.5.3",
|
||||
|
@ -158,16 +158,16 @@
|
|||
"ts-loader": "9.3.0",
|
||||
"ts-node": "10.7.0",
|
||||
"ts-sinon": "2.0.2",
|
||||
"typescript": "4.7.2",
|
||||
"typescript": "5.4.5",
|
||||
"undate": "0.3.0",
|
||||
"vue": "3.2.35",
|
||||
"vue-eslint-parser": "8.3.0",
|
||||
"vue-eslint-parser": "9.4.2",
|
||||
"vue-loader": "17.0.1",
|
||||
"vue-router": "4.0.15",
|
||||
"vuex": "4.0.2",
|
||||
"webpack": "5.76.0",
|
||||
"webpack-cli": "4.9.2",
|
||||
"webpack-dev-middleware": "5.3.3",
|
||||
"webpack-dev-middleware": "5.3.4",
|
||||
"webpack-hot-middleware": "2.25.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,10 +10,35 @@
|
|||
|
||||
const {readFileSync, writeFileSync} = require("fs");
|
||||
const colors = require("chalk");
|
||||
const log = require("../server/log").default;
|
||||
const {join} = require("path");
|
||||
const {spawnSync} = require("child_process");
|
||||
|
||||
function timestamp() {
|
||||
const datetime = new Date().toISOString().split(".")[0].replace("T", " ");
|
||||
|
||||
return colors.dim(datetime);
|
||||
}
|
||||
|
||||
const log = {
|
||||
/* eslint-disable no-console */
|
||||
error(...args) {
|
||||
console.error(timestamp(), colors.red("[ERROR]"), ...args);
|
||||
},
|
||||
warn(...args) {
|
||||
console.error(timestamp(), colors.yellow("[WARN]"), ...args);
|
||||
},
|
||||
info(...args) {
|
||||
console.log(timestamp(), colors.blue("[INFO]"), ...args);
|
||||
},
|
||||
debug(...args) {
|
||||
console.log(timestamp(), colors.green("[DEBUG]"), ...args);
|
||||
},
|
||||
raw(...args) {
|
||||
console.log(...args);
|
||||
},
|
||||
/* eslint-enable no-console */
|
||||
};
|
||||
|
||||
function getGitUsername() {
|
||||
return spawnSync("git", ["config", "user.name"], {encoding: "utf8"}).stdout.trim();
|
||||
}
|
||||
|
|
115
server/client.ts
115
server/client.ts
|
@ -6,10 +6,12 @@ import crypto from "crypto";
|
|||
import colors from "chalk";
|
||||
|
||||
import log from "./log";
|
||||
import Chan, {ChanConfig, Channel, ChanType} from "./models/chan";
|
||||
import Msg, {MessageType, UserInMessage} from "./models/msg";
|
||||
import Chan, {ChanConfig} from "./models/chan";
|
||||
import Msg from "./models/msg";
|
||||
import Config from "./config";
|
||||
import {condensedTypes} from "../shared/irc";
|
||||
import {MessageType} from "../shared/types/msg";
|
||||
import {SharedMention} from "../shared/types/mention";
|
||||
|
||||
import inputs from "./plugins/inputs";
|
||||
import PublicClient from "./plugins/packages/publicClient";
|
||||
|
@ -17,11 +19,12 @@ import SqliteMessageStorage from "./plugins/messageStorage/sqlite";
|
|||
import TextFileMessageStorage from "./plugins/messageStorage/text";
|
||||
import Network, {IgnoreListItem, NetworkConfig, NetworkWithIrcFramework} from "./models/network";
|
||||
import ClientManager from "./clientManager";
|
||||
import {MessageStorage, SearchQuery, SearchResponse} from "./plugins/messageStorage/types";
|
||||
import {MessageStorage} from "./plugins/messageStorage/types";
|
||||
import {StorageCleaner} from "./storageCleaner";
|
||||
|
||||
type OrderItem = Chan["id"] | Network["uuid"];
|
||||
type Order = OrderItem[];
|
||||
import {SearchQuery, SearchResponse} from "../shared/types/storage";
|
||||
import {SharedChan, ChanType} from "../shared/types/chan";
|
||||
import {SharedNetwork} from "../shared/types/network";
|
||||
import {ServerToClientEvents} from "../shared/types/socket-events";
|
||||
|
||||
const events = [
|
||||
"away",
|
||||
|
@ -82,15 +85,6 @@ export type UserConfig = {
|
|||
networks?: NetworkConfig[];
|
||||
};
|
||||
|
||||
export type Mention = {
|
||||
chanId: number;
|
||||
msgId: number;
|
||||
type: MessageType;
|
||||
time: Date;
|
||||
text: string;
|
||||
from: UserInMessage;
|
||||
};
|
||||
|
||||
class Client {
|
||||
awayMessage!: string;
|
||||
lastActiveChannel!: number;
|
||||
|
@ -98,12 +92,12 @@ class Client {
|
|||
[socketId: string]: {token: string; openChannel: number};
|
||||
};
|
||||
config!: UserConfig;
|
||||
id!: number;
|
||||
id: string;
|
||||
idMsg!: number;
|
||||
idChan!: number;
|
||||
name!: string;
|
||||
networks!: Network[];
|
||||
mentions!: Mention[];
|
||||
mentions!: SharedMention[];
|
||||
manager!: ClientManager;
|
||||
messageStorage!: MessageStorage[];
|
||||
highlightRegex!: RegExp | null;
|
||||
|
@ -113,12 +107,12 @@ class Client {
|
|||
fileHash!: string;
|
||||
|
||||
constructor(manager: ClientManager, name?: string, config = {} as UserConfig) {
|
||||
this.id = uuidv4();
|
||||
_.merge(this, {
|
||||
awayMessage: "",
|
||||
lastActiveChannel: -1,
|
||||
attachedClients: {},
|
||||
config: config,
|
||||
id: uuidv4(),
|
||||
idChan: 1,
|
||||
idMsg: 1,
|
||||
name: name,
|
||||
|
@ -229,9 +223,12 @@ class Client {
|
|||
return chan;
|
||||
}
|
||||
|
||||
emit(event: string, data?: any) {
|
||||
emit<Ev extends keyof ServerToClientEvents>(
|
||||
event: Ev,
|
||||
...args: Parameters<ServerToClientEvents[Ev]>
|
||||
) {
|
||||
if (this.manager !== null) {
|
||||
this.manager.sockets.in(this.id.toString()).emit(event, data);
|
||||
this.manager.sockets.in(this.id).emit(event, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -351,7 +348,7 @@ class Client {
|
|||
|
||||
client.networks.push(network);
|
||||
client.emit("network", {
|
||||
networks: [network.getFilteredClone(this.lastActiveChannel, -1)],
|
||||
network: network.getFilteredClone(this.lastActiveChannel, -1),
|
||||
});
|
||||
|
||||
if (!network.validate(client)) {
|
||||
|
@ -697,56 +694,39 @@ class Client {
|
|||
this.emit("open", targetNetChan.chan.id);
|
||||
}
|
||||
|
||||
sort(data: {order: Order; type: "networks" | "channels"; target: string}) {
|
||||
const order = data.order;
|
||||
sortChannels(netid: SharedNetwork["uuid"], order: SharedChan["id"][]) {
|
||||
const network = _.find(this.networks, {uuid: netid});
|
||||
|
||||
if (!_.isArray(order)) {
|
||||
if (!network) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (data.type) {
|
||||
case "networks":
|
||||
this.networks.sort((a, b) => order.indexOf(a.uuid) - order.indexOf(b.uuid));
|
||||
|
||||
// Sync order to connected clients
|
||||
this.emit("sync_sort", {
|
||||
order: this.networks.map((obj) => obj.uuid),
|
||||
type: data.type,
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
case "channels": {
|
||||
const network = _.find(this.networks, {uuid: data.target});
|
||||
|
||||
if (!network) {
|
||||
return;
|
||||
}
|
||||
|
||||
network.channels.sort((a, b) => {
|
||||
// Always sort lobby to the top regardless of what the client has sent
|
||||
// Because there's a lot of code that presumes channels[0] is the lobby
|
||||
if (a.type === ChanType.LOBBY) {
|
||||
return -1;
|
||||
} else if (b.type === ChanType.LOBBY) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return order.indexOf(a.id) - order.indexOf(b.id);
|
||||
});
|
||||
|
||||
// Sync order to connected clients
|
||||
this.emit("sync_sort", {
|
||||
order: network.channels.map((obj) => obj.id),
|
||||
type: data.type,
|
||||
target: network.uuid,
|
||||
});
|
||||
|
||||
break;
|
||||
network.channels.sort((a, b) => {
|
||||
// Always sort lobby to the top regardless of what the client has sent
|
||||
// Because there's a lot of code that presumes channels[0] is the lobby
|
||||
if (a.type === ChanType.LOBBY) {
|
||||
return -1;
|
||||
} else if (b.type === ChanType.LOBBY) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return order.indexOf(a.id) - order.indexOf(b.id);
|
||||
});
|
||||
this.save();
|
||||
// Sync order to connected clients
|
||||
this.emit("sync_sort:channels", {
|
||||
network: network.uuid,
|
||||
order: network.channels.map((obj) => obj.id),
|
||||
});
|
||||
}
|
||||
|
||||
sortNetworks(order: SharedNetwork["uuid"][]) {
|
||||
this.networks.sort((a, b) => order.indexOf(a.uuid) - order.indexOf(b.uuid));
|
||||
this.save();
|
||||
// Sync order to connected clients
|
||||
this.emit("sync_sort:networks", {
|
||||
order: this.networks.map((obj) => obj.uuid),
|
||||
});
|
||||
}
|
||||
|
||||
names(data: {target: number}) {
|
||||
|
@ -776,7 +756,7 @@ class Client {
|
|||
|
||||
quit(signOut?: boolean) {
|
||||
const sockets = this.manager.sockets.sockets;
|
||||
const room = sockets.adapter.rooms.get(this.id.toString());
|
||||
const room = sockets.adapter.rooms.get(this.id);
|
||||
|
||||
if (room) {
|
||||
for (const user of room) {
|
||||
|
@ -836,12 +816,13 @@ class Client {
|
|||
}
|
||||
|
||||
// TODO: type session to this.attachedClients
|
||||
registerPushSubscription(session: any, subscription: ClientPushSubscription, noSave = false) {
|
||||
registerPushSubscription(session: any, subscription: PushSubscriptionJSON, noSave = false) {
|
||||
if (
|
||||
!_.isPlainObject(subscription) ||
|
||||
!_.isPlainObject(subscription.keys) ||
|
||||
typeof subscription.endpoint !== "string" ||
|
||||
!/^https?:\/\//.test(subscription.endpoint) ||
|
||||
!_.isPlainObject(subscription.keys) ||
|
||||
!subscription.keys || // TS compiler doesn't understand isPlainObject
|
||||
typeof subscription.keys.p256dh !== "string" ||
|
||||
typeof subscription.keys.auth !== "string"
|
||||
) {
|
||||
|
|
|
@ -7,10 +7,9 @@ import path from "path";
|
|||
import Auth from "./plugins/auth";
|
||||
import Client, {UserConfig} from "./client";
|
||||
import Config from "./config";
|
||||
import {NetworkConfig} from "./models/network";
|
||||
import WebPush from "./plugins/webpush";
|
||||
import log from "./log";
|
||||
import {Server} from "socket.io";
|
||||
import {Server} from "./server";
|
||||
|
||||
class ClientManager {
|
||||
clients: Client[];
|
||||
|
@ -185,7 +184,6 @@ class ClientManager {
|
|||
mode: 0o600,
|
||||
});
|
||||
} catch (e: any) {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
log.error(`Failed to create user ${colors.green(name)} (${e})`);
|
||||
throw e;
|
||||
}
|
||||
|
@ -253,7 +251,6 @@ class ClientManager {
|
|||
|
||||
return callback ? callback() : true;
|
||||
} catch (e: any) {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
log.error(`Failed to update user ${colors.green(client.name)} (${e})`);
|
||||
|
||||
if (callback) {
|
||||
|
@ -287,7 +284,6 @@ class ClientManager {
|
|||
const data = fs.readFileSync(userPath, "utf-8");
|
||||
return JSON.parse(data) as UserConfig;
|
||||
} catch (e: any) {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
log.error(`Failed to read user ${colors.bold(name)}: ${e}`);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import fs, {Stats} from "fs";
|
|||
import os from "os";
|
||||
import _ from "lodash";
|
||||
import colors from "chalk";
|
||||
import {SearchOptions} from "ldapjs";
|
||||
|
||||
import log from "./log";
|
||||
import Helper from "./helper";
|
||||
|
@ -44,7 +45,7 @@ export type Defaults = Pick<
|
|||
| "saslAccount"
|
||||
| "saslPassword"
|
||||
> & {
|
||||
join?: string;
|
||||
join: string;
|
||||
};
|
||||
|
||||
type Identd = {
|
||||
|
@ -57,7 +58,7 @@ type SearchDN = {
|
|||
rootPassword: string;
|
||||
filter: string;
|
||||
base: string;
|
||||
scope: string;
|
||||
scope: SearchOptions["scope"];
|
||||
};
|
||||
|
||||
type Ldap = {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import log from "./log";
|
||||
import fs from "fs";
|
||||
import net, {Socket} from "net";
|
||||
import colors from "chalk";
|
||||
import Helper from "./helper";
|
||||
import Config from "./config";
|
||||
import log from "./log";
|
||||
|
||||
type Connection = {
|
||||
socket: Socket;
|
||||
|
@ -66,31 +66,56 @@ class Identification {
|
|||
|
||||
serverConnection(socket: Socket) {
|
||||
socket.on("error", (err: string) => log.error(`Identd socket error: ${err}`));
|
||||
socket.on("data", (data) => {
|
||||
socket.setTimeout(5000, () => {
|
||||
log.warn(
|
||||
`identd: no data received, closing connection to ${
|
||||
socket.remoteAddress || "undefined"
|
||||
}`
|
||||
);
|
||||
socket.destroy();
|
||||
});
|
||||
socket.once("data", (data) => {
|
||||
this.respondToIdent(socket, data);
|
||||
socket.end();
|
||||
});
|
||||
}
|
||||
|
||||
respondToIdent(socket: Socket, buffer: Buffer) {
|
||||
if (!socket.remoteAddress) {
|
||||
log.warn("identd: no remote address");
|
||||
return;
|
||||
}
|
||||
|
||||
const data = buffer.toString().split(",");
|
||||
|
||||
const lport = parseInt(data[0], 10) || 0;
|
||||
const fport = parseInt(data[1], 10) || 0;
|
||||
|
||||
if (lport < 1 || fport < 1 || lport > 65535 || fport > 65535) {
|
||||
log.warn(`identd: bogus request from ${socket.remoteAddress}`);
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug(`identd: remote ${socket.remoteAddress} query ${lport}, ${fport}`);
|
||||
|
||||
for (const connection of this.connections.values()) {
|
||||
if (connection.socket.remotePort === fport && connection.socket.localPort === lport) {
|
||||
return socket.write(
|
||||
`${lport}, ${fport} : USERID : TheLounge : ${connection.user}\r\n`
|
||||
);
|
||||
// we only want to respond if all the ip,port tuples match, to avoid user enumeration
|
||||
if (
|
||||
connection.socket.remotePort === fport &&
|
||||
connection.socket.localPort === lport &&
|
||||
socket.remoteAddress === connection.socket.remoteAddress &&
|
||||
socket.localAddress === connection.socket.localAddress
|
||||
) {
|
||||
const reply = `${lport}, ${fport} : USERID : TheLounge : ${connection.user}\r\n`;
|
||||
log.debug(`identd: reply is ${reply.trimEnd()}`);
|
||||
socket.write(reply);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
socket.write(`${lport}, ${fport} : ERROR : NO-USER\r\n`);
|
||||
const reply = `${lport}, ${fport} : ERROR : NO-USER\r\n`;
|
||||
log.debug(`identd: reply is ${reply.trimEnd()}`);
|
||||
socket.write(reply);
|
||||
}
|
||||
|
||||
addSocket(socket: Socket, user: string) {
|
||||
|
@ -127,8 +152,21 @@ class Identification {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!connection.socket.remoteAddress) {
|
||||
log.warn(`oidentd: socket has no remote address, will not respond to queries`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!connection.socket.localAddress) {
|
||||
log.warn(`oidentd: socket has no local address, will not respond to queries`);
|
||||
return;
|
||||
}
|
||||
|
||||
// we only want to respond if all the ip,port tuples match, to avoid user enumeration
|
||||
file +=
|
||||
`fport ${connection.socket.remotePort}` +
|
||||
`to ${connection.socket.remoteAddress}` +
|
||||
` fport ${connection.socket.remotePort}` +
|
||||
` from ${connection.socket.localAddress}` +
|
||||
` lport ${connection.socket.localPort}` +
|
||||
` { reply "${connection.user}" }\n`;
|
||||
});
|
||||
|
|
1
server/index.d.ts
vendored
1
server/index.d.ts
vendored
|
@ -1 +0,0 @@
|
|||
import "./types";
|
|
@ -2,36 +2,14 @@ import _ from "lodash";
|
|||
import log from "../log";
|
||||
import Config from "../config";
|
||||
import User from "./user";
|
||||
import Msg, {MessageType} from "./msg";
|
||||
import Msg from "./msg";
|
||||
import storage from "../plugins/storage";
|
||||
import Client from "../client";
|
||||
import Network from "./network";
|
||||
import Prefix from "./prefix";
|
||||
|
||||
export enum ChanType {
|
||||
CHANNEL = "channel",
|
||||
LOBBY = "lobby",
|
||||
QUERY = "query",
|
||||
SPECIAL = "special",
|
||||
}
|
||||
|
||||
export enum SpecialChanType {
|
||||
BANLIST = "list_bans",
|
||||
INVITELIST = "list_invites",
|
||||
CHANNELLIST = "list_channels",
|
||||
IGNORELIST = "list_ignored",
|
||||
}
|
||||
|
||||
export enum ChanState {
|
||||
PARTED = 0,
|
||||
JOINED = 1,
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export type FilteredChannel = Chan & {
|
||||
users: [];
|
||||
totalMessages: number;
|
||||
};
|
||||
import {MessageType, SharedMsg} from "../../shared/types/msg";
|
||||
import {ChanType, SpecialChanType, ChanState} from "../../shared/types/chan";
|
||||
import {SharedNetworkChan} from "../../shared/types/network";
|
||||
|
||||
export type ChanConfig = {
|
||||
name: string;
|
||||
|
@ -60,7 +38,6 @@ class Chan {
|
|||
data?: any;
|
||||
closed?: boolean;
|
||||
num_users?: number;
|
||||
static optionalProperties = ["userAway", "special", "data", "closed", "num_users"];
|
||||
|
||||
constructor(attr?: Partial<Chan>) {
|
||||
_.defaults(this, attr, {
|
||||
|
@ -84,18 +61,11 @@ class Chan {
|
|||
}
|
||||
|
||||
pushMessage(client: Client, msg: Msg, increasesUnread = false) {
|
||||
const chan = this.id;
|
||||
const obj = {chan, msg} as {
|
||||
chan: number;
|
||||
msg: Msg;
|
||||
unread?: number;
|
||||
highlight?: number;
|
||||
};
|
||||
|
||||
const chanId = this.id;
|
||||
msg.id = client.idMsg++;
|
||||
|
||||
// If this channel is open in any of the clients, do not increase unread counter
|
||||
const isOpen = _.find(client.attachedClients, {openChannel: chan}) !== undefined;
|
||||
const isOpen = _.find(client.attachedClients, {openChannel: chanId}) !== undefined;
|
||||
|
||||
if (msg.self) {
|
||||
// reset counters/markers when receiving self-/echo-message
|
||||
|
@ -108,15 +78,15 @@ class Chan {
|
|||
}
|
||||
|
||||
if (increasesUnread || msg.highlight) {
|
||||
obj.unread = ++this.unread;
|
||||
this.unread++;
|
||||
}
|
||||
|
||||
if (msg.highlight) {
|
||||
obj.highlight = ++this.highlight;
|
||||
this.highlight++;
|
||||
}
|
||||
}
|
||||
|
||||
client.emit("msg", obj);
|
||||
client.emit("msg", {chan: chanId, msg, unread: this.unread, highlight: this.highlight});
|
||||
|
||||
// Never store messages in public mode as the session
|
||||
// is completely destroyed when the page gets closed
|
||||
|
@ -144,7 +114,8 @@ class Chan {
|
|||
}
|
||||
}
|
||||
}
|
||||
dereferencePreviews(messages) {
|
||||
|
||||
dereferencePreviews(messages: Msg[]) {
|
||||
if (!Config.values.prefetch || !Config.values.prefetchStorage) {
|
||||
return;
|
||||
}
|
||||
|
@ -160,6 +131,7 @@ class Chan {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
getSortedUsers(irc?: Network["irc"]) {
|
||||
const users = Array.from(this.users.values());
|
||||
|
||||
|
@ -182,21 +154,27 @@ class Chan {
|
|||
return userModeSortPriority[a.mode] - userModeSortPriority[b.mode];
|
||||
});
|
||||
}
|
||||
|
||||
findMessage(msgId: number) {
|
||||
return this.messages.find((message) => message.id === msgId);
|
||||
}
|
||||
|
||||
findUser(nick: string) {
|
||||
return this.users.get(nick.toLowerCase());
|
||||
}
|
||||
|
||||
getUser(nick: string) {
|
||||
return this.findUser(nick) || new User({nick}, new Prefix([]));
|
||||
}
|
||||
|
||||
setUser(user: User) {
|
||||
this.users.set(user.nick.toLowerCase(), user);
|
||||
}
|
||||
|
||||
removeUser(user: User) {
|
||||
this.users.delete(user.nick.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a clean clone of this channel that will be sent to the client.
|
||||
* This function performs manual cloning of channel object for
|
||||
|
@ -206,38 +184,54 @@ class Chan {
|
|||
* If true, channel is assumed active.
|
||||
* @param {int} lastMessage - Last message id seen by active client to avoid sending duplicates.
|
||||
*/
|
||||
getFilteredClone(lastActiveChannel?: number | boolean, lastMessage?: number): FilteredChannel {
|
||||
return Object.keys(this).reduce((newChannel, prop) => {
|
||||
if (Chan.optionalProperties.includes(prop)) {
|
||||
if (this[prop] !== undefined || (Array.isArray(this[prop]) && this[prop].length)) {
|
||||
newChannel[prop] = this[prop];
|
||||
}
|
||||
} else if (prop === "users") {
|
||||
// Do not send users, client requests updated user list whenever needed
|
||||
newChannel[prop] = [];
|
||||
} else if (prop === "messages") {
|
||||
// If client is reconnecting, only send new messages that client has not seen yet
|
||||
if (lastMessage && lastMessage > -1) {
|
||||
// When reconnecting, always send up to 100 messages to prevent message gaps on the client
|
||||
// See https://github.com/thelounge/thelounge/issues/1883
|
||||
newChannel[prop] = this[prop].filter((m) => m.id > lastMessage).slice(-100);
|
||||
} else {
|
||||
// If channel is active, send up to 100 last messages, for all others send just 1
|
||||
// Client will automatically load more messages whenever needed based on last seen messages
|
||||
const messagesToSend =
|
||||
lastActiveChannel === true || this.id === lastActiveChannel ? 100 : 1;
|
||||
getFilteredClone(
|
||||
lastActiveChannel?: number | boolean,
|
||||
lastMessage?: number
|
||||
): SharedNetworkChan {
|
||||
let msgs: SharedMsg[];
|
||||
|
||||
newChannel[prop] = this[prop].slice(-messagesToSend);
|
||||
}
|
||||
// If client is reconnecting, only send new messages that client has not seen yet
|
||||
if (lastMessage && lastMessage > -1) {
|
||||
// When reconnecting, always send up to 100 messages to prevent message gaps on the client
|
||||
// See https://github.com/thelounge/thelounge/issues/1883
|
||||
msgs = this.messages.filter((m) => m.id > lastMessage).slice(-100);
|
||||
} else {
|
||||
// If channel is active, send up to 100 last messages, for all others send just 1
|
||||
// Client will automatically load more messages whenever needed based on last seen messages
|
||||
const messagesToSend =
|
||||
lastActiveChannel === true || this.id === lastActiveChannel ? 100 : 1;
|
||||
msgs = this.messages.slice(-messagesToSend);
|
||||
}
|
||||
|
||||
(newChannel as FilteredChannel).totalMessages = this[prop].length;
|
||||
} else {
|
||||
newChannel[prop] = this[prop];
|
||||
}
|
||||
return {
|
||||
id: this.id,
|
||||
messages: msgs,
|
||||
totalMessages: this.messages.length,
|
||||
name: this.name,
|
||||
key: this.key,
|
||||
topic: this.topic,
|
||||
firstUnread: this.firstUnread,
|
||||
unread: this.unread,
|
||||
highlight: this.highlight,
|
||||
muted: this.muted,
|
||||
type: this.type,
|
||||
state: this.state,
|
||||
|
||||
return newChannel;
|
||||
}, {}) as FilteredChannel;
|
||||
special: this.special,
|
||||
data: this.data,
|
||||
closed: this.closed,
|
||||
num_users: this.num_users,
|
||||
};
|
||||
// TODO: funny array mutation below might need to be reproduced
|
||||
// static optionalProperties = ["userAway", "special", "data", "closed", "num_users"];
|
||||
// return Object.keys(this).reduce((newChannel, prop) => {
|
||||
// if (Chan.optionalProperties.includes(prop)) {
|
||||
// if (this[prop] !== undefined || (Array.isArray(this[prop]) && this[prop].length)) {
|
||||
// newChannel[prop] = this[prop];
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
writeUserLog(client: Client, msg: Msg) {
|
||||
this.messages.push(msg);
|
||||
|
||||
|
@ -270,6 +264,7 @@ class Chan {
|
|||
messageStorage.index(target.network, targetChannel, msg).catch((e) => log.error(e));
|
||||
}
|
||||
}
|
||||
|
||||
loadMessages(client: Client, network: Network) {
|
||||
if (!this.isLoggable()) {
|
||||
return;
|
||||
|
@ -326,15 +321,23 @@ class Chan {
|
|||
log.error(`Failed to load messages for ${client.name}: ${err.toString()}`)
|
||||
);
|
||||
}
|
||||
|
||||
isLoggable() {
|
||||
return this.type === ChanType.CHANNEL || this.type === ChanType.QUERY;
|
||||
}
|
||||
|
||||
setMuteStatus(muted: boolean) {
|
||||
this.muted = !!muted;
|
||||
}
|
||||
}
|
||||
|
||||
function requestZncPlayback(channel, network, from) {
|
||||
function requestZncPlayback(channel: Chan, network: Network, from: number) {
|
||||
if (!network.irc) {
|
||||
throw new Error(
|
||||
`requestZncPlayback: no irc field on network "${network.name}", this is a bug`
|
||||
);
|
||||
}
|
||||
|
||||
network.irc.raw("ZNC", "*playback", "PLAY", channel.name, from.toString());
|
||||
}
|
||||
|
||||
|
|
|
@ -1,41 +1,5 @@
|
|||
import _ from "lodash";
|
||||
import {LinkPreview} from "../plugins/irc-events/link";
|
||||
import User from "./user";
|
||||
|
||||
export type UserInMessage = Partial<User> & {
|
||||
mode: string;
|
||||
};
|
||||
|
||||
export enum MessageType {
|
||||
UNHANDLED = "unhandled",
|
||||
ACTION = "action",
|
||||
AWAY = "away",
|
||||
BACK = "back",
|
||||
ERROR = "error",
|
||||
INVITE = "invite",
|
||||
JOIN = "join",
|
||||
KICK = "kick",
|
||||
LOGIN = "login",
|
||||
LOGOUT = "logout",
|
||||
MESSAGE = "message",
|
||||
MODE = "mode",
|
||||
MODE_CHANNEL = "mode_channel",
|
||||
MODE_USER = "mode_user", // RPL_UMODEIS
|
||||
MONOSPACE_BLOCK = "monospace_block",
|
||||
NICK = "nick",
|
||||
NOTICE = "notice",
|
||||
PART = "part",
|
||||
QUIT = "quit",
|
||||
CTCP = "ctcp",
|
||||
CTCP_REQUEST = "ctcp_request",
|
||||
CHGHOST = "chghost",
|
||||
TOPIC = "topic",
|
||||
TOPIC_SET_BY = "topic_set_by",
|
||||
WHOIS = "whois",
|
||||
RAW = "raw",
|
||||
PLUGIN = "plugin",
|
||||
WALLOPS = "wallops",
|
||||
}
|
||||
import {MessageType, LinkPreview, UserInMessage} from "../../shared/types/msg";
|
||||
|
||||
class Msg {
|
||||
from!: UserInMessage;
|
||||
|
@ -70,7 +34,7 @@ class Msg {
|
|||
raw_modes!: any;
|
||||
when!: Date;
|
||||
whois!: any;
|
||||
users!: UserInMessage[] | string[];
|
||||
users!: string[];
|
||||
statusmsgGroup!: string;
|
||||
params!: string[];
|
||||
|
||||
|
|
|
@ -1,24 +1,17 @@
|
|||
import _ from "lodash";
|
||||
import {v4 as uuidv4} from "uuid";
|
||||
import IrcFramework, {Client as IRCClient} from "irc-framework";
|
||||
import Chan, {ChanConfig, Channel, ChanType} from "./chan";
|
||||
import Msg, {MessageType} from "./msg";
|
||||
import Chan, {ChanConfig, Channel} from "./chan";
|
||||
import Msg from "./msg";
|
||||
import Prefix from "./prefix";
|
||||
import Helper, {Hostmask} from "../helper";
|
||||
import Config, {WebIRC} from "../config";
|
||||
import STSPolicies from "../plugins/sts";
|
||||
import ClientCertificate, {ClientCertificateType} from "../plugins/clientCertificate";
|
||||
import Client from "../client";
|
||||
|
||||
/**
|
||||
* List of keys which should be sent to the client by default.
|
||||
*/
|
||||
const fieldsForClient = {
|
||||
uuid: true,
|
||||
name: true,
|
||||
nick: true,
|
||||
serverOptions: true,
|
||||
};
|
||||
import {MessageType} from "../../shared/types/msg";
|
||||
import {ChanType} from "../../shared/types/chan";
|
||||
import {SharedNetwork} from "../../shared/types/network";
|
||||
|
||||
type NetworkIrcOptions = {
|
||||
host: string;
|
||||
|
@ -52,7 +45,7 @@ type NetworkStatus = {
|
|||
};
|
||||
|
||||
export type IgnoreListItem = Hostmask & {
|
||||
when?: number;
|
||||
when: number;
|
||||
};
|
||||
|
||||
type IgnoreList = IgnoreListItem[];
|
||||
|
@ -505,24 +498,17 @@ class Network {
|
|||
}
|
||||
}
|
||||
|
||||
getFilteredClone(lastActiveChannel?: number, lastMessage?: number) {
|
||||
const filteredNetwork = Object.keys(this).reduce((newNetwork, prop) => {
|
||||
if (prop === "channels") {
|
||||
// Channels objects perform their own cloning
|
||||
newNetwork[prop] = this[prop].map((channel) =>
|
||||
channel.getFilteredClone(lastActiveChannel, lastMessage)
|
||||
);
|
||||
} else if (fieldsForClient[prop]) {
|
||||
// Some properties that are not useful for the client are skipped
|
||||
newNetwork[prop] = this[prop];
|
||||
}
|
||||
|
||||
return newNetwork;
|
||||
}, {}) as Network;
|
||||
|
||||
filteredNetwork.status = this.getNetworkStatus();
|
||||
|
||||
return filteredNetwork;
|
||||
getFilteredClone(lastActiveChannel?: number, lastMessage?: number): SharedNetwork {
|
||||
return {
|
||||
uuid: this.uuid,
|
||||
name: this.name,
|
||||
nick: this.nick,
|
||||
serverOptions: this.serverOptions,
|
||||
status: this.getNetworkStatus(),
|
||||
channels: this.channels.map((channel) =>
|
||||
channel.getFilteredClone(lastActiveChannel, lastMessage)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
getNetworkStatus() {
|
||||
|
|
|
@ -67,11 +67,11 @@ function advancedLdapAuth(user: string, password: string, callback: (success: bo
|
|||
});
|
||||
|
||||
const base = config.ldap.searchDN.base;
|
||||
const searchOptions = {
|
||||
const searchOptions: SearchOptions = {
|
||||
scope: config.ldap.searchDN.scope,
|
||||
filter: `(&(${config.ldap.primaryKey}=${userDN})${config.ldap.searchDN.filter})`,
|
||||
attributes: ["dn"],
|
||||
} as SearchOptions;
|
||||
};
|
||||
|
||||
ldapclient.on("error", function (err: Error) {
|
||||
log.error(`Unable to connect to LDAP server: ${err.toString()}`);
|
||||
|
@ -178,12 +178,12 @@ function advancedLdapLoadUsers(users: string[], callbackLoadUser) {
|
|||
|
||||
const remainingUsers = new Set(users);
|
||||
|
||||
const searchOptions = {
|
||||
const searchOptions: SearchOptions = {
|
||||
scope: config.ldap.searchDN.scope,
|
||||
filter: `${config.ldap.searchDN.filter}`,
|
||||
attributes: [config.ldap.primaryKey],
|
||||
paged: true,
|
||||
} as SearchOptions;
|
||||
};
|
||||
|
||||
ldapclient.search(base, searchOptions, function (err2, res) {
|
||||
if (err2) {
|
||||
|
|
|
@ -3,7 +3,7 @@ import log from "../../log";
|
|||
import Helper from "../../helper";
|
||||
import type {AuthHandler} from "../auth";
|
||||
|
||||
const localAuth: AuthHandler = (manager, client, user, password, callback) => {
|
||||
const localAuth: AuthHandler = (_manager, client, user, password, callback) => {
|
||||
// If no user is found, or if the client has not provided a password,
|
||||
// fail the authentication straight away
|
||||
if (!client || !password) {
|
||||
|
@ -40,7 +40,6 @@ const localAuth: AuthHandler = (manager, client, user, password, callback) => {
|
|||
callback(matching);
|
||||
})
|
||||
.catch((error) => {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
log.error(`Error while checking users password. Error: ${error}`);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@ import log from "../log";
|
|||
import pkg from "../../package.json";
|
||||
import ClientManager from "../clientManager";
|
||||
import Config from "../config";
|
||||
import {SharedChangelogData} from "../../shared/types/changelog";
|
||||
|
||||
const TIME_TO_LIVE = 15 * 60 * 1000; // 15 minutes, in milliseconds
|
||||
|
||||
|
@ -12,31 +13,17 @@ export default {
|
|||
fetch,
|
||||
checkForUpdates,
|
||||
};
|
||||
export type ChangelogData = {
|
||||
current: {
|
||||
prerelease: boolean;
|
||||
version: string;
|
||||
changelog?: string;
|
||||
url: string;
|
||||
};
|
||||
expiresAt: number;
|
||||
latest?: {
|
||||
prerelease: boolean;
|
||||
version: string;
|
||||
url: string;
|
||||
};
|
||||
packages?: boolean;
|
||||
};
|
||||
|
||||
const versions = {
|
||||
const versions: SharedChangelogData = {
|
||||
current: {
|
||||
prerelease: false,
|
||||
version: `v${pkg.version}`,
|
||||
changelog: undefined,
|
||||
url: "", // TODO: properly init
|
||||
},
|
||||
expiresAt: -1,
|
||||
latest: undefined,
|
||||
packages: undefined,
|
||||
} as ChangelogData;
|
||||
};
|
||||
|
||||
async function fetch() {
|
||||
const time = Date.now();
|
||||
|
|
|
@ -31,7 +31,7 @@ function get(uuid: string): ClientCertificateType | null {
|
|||
return {
|
||||
private_key: fs.readFileSync(paths.privateKeyPath, "utf-8"),
|
||||
certificate: fs.readFileSync(paths.certificatePath, "utf-8"),
|
||||
} as ClientCertificateType;
|
||||
};
|
||||
} catch (e: any) {
|
||||
log.error("Unable to get certificate", e);
|
||||
}
|
||||
|
@ -95,7 +95,6 @@ function generate() {
|
|||
// Set notAfter 100 years into the future just in case
|
||||
// the server actually validates this field
|
||||
cert.validity.notAfter = new Date();
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
||||
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 100);
|
||||
|
||||
const attrs = [
|
||||
|
@ -122,10 +121,10 @@ function generate() {
|
|||
// Sign this certificate with a SHA256 signature
|
||||
cert.sign(keys.privateKey, md.sha256.create());
|
||||
|
||||
const pem = {
|
||||
const pem: ClientCertificateType = {
|
||||
private_key: pki.privateKeyToPem(keys.privateKey),
|
||||
certificate: pki.certificateToPem(cert),
|
||||
} as ClientCertificateType;
|
||||
};
|
||||
|
||||
return pem;
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ export default (app: express.Application) => {
|
|||
const compiler = webpack(webpackConfig);
|
||||
|
||||
app.use(
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
webpackDevMiddleware(compiler, {
|
||||
index: "/",
|
||||
publicPath: webpackConfig.output?.publicPath,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import {ChanType} from "../../models/chan";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["slap", "me"];
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {ChanType} from "../../models/chan";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {PluginInputHandler} from "./index";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["ban", "unban", "banlist", "kickban"];
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {PluginInputHandler} from "./index";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
const commands = ["connect", "server"];
|
||||
const allowDisconnected = true;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {PluginInputHandler} from "./index";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
const commands = ["ctcp"];
|
||||
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import Helper from "../../helper";
|
||||
import {PluginInputHandler} from "./index";
|
||||
import {IgnoreListItem} from "../../models/network";
|
||||
import {ChanType, SpecialChanType} from "../../models/chan";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
const commands = ["ignore", "unignore", "ignorelist"];
|
||||
const commands = ["ignore", "unignore"];
|
||||
|
||||
const input: PluginInputHandler = function (network, chan, cmd, args) {
|
||||
const client = this;
|
||||
let target: string;
|
||||
// let hostmask: cmd === "ignoreList" ? string : undefined;
|
||||
let hostmask: IgnoreListItem | undefined;
|
||||
|
||||
if (cmd !== "ignorelist" && (args.length === 0 || args[0].trim().length === 0)) {
|
||||
if (args.length === 0 || args[0].trim().length === 0) {
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
|
@ -24,16 +20,13 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (cmd !== "ignorelist") {
|
||||
// Trim to remove any spaces from the hostmask
|
||||
target = args[0].trim();
|
||||
hostmask = Helper.parseHostmask(target) as IgnoreListItem;
|
||||
}
|
||||
const target = args[0].trim();
|
||||
const hostmask = Helper.parseHostmask(target);
|
||||
|
||||
switch (cmd) {
|
||||
case "ignore": {
|
||||
// IRC nicks are case insensitive
|
||||
if (hostmask!.nick.toLowerCase() === network.nick.toLowerCase()) {
|
||||
if (hostmask.nick.toLowerCase() === network.nick.toLowerCase()) {
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
|
@ -41,25 +34,14 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
|||
text: "You can't ignore yourself",
|
||||
})
|
||||
);
|
||||
} else if (
|
||||
!network.ignoreList.some(function (entry) {
|
||||
return Helper.compareHostmask(entry, hostmask!);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
network.ignoreList.some(function (entry) {
|
||||
return Helper.compareHostmask(entry, hostmask);
|
||||
})
|
||||
) {
|
||||
hostmask!.when = Date.now();
|
||||
network.ignoreList.push(hostmask!);
|
||||
|
||||
client.save();
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: MessageType.ERROR,
|
||||
text: `\u0002${hostmask!.nick}!${hostmask!.ident}@${
|
||||
hostmask!.hostname
|
||||
}\u000f added to ignorelist`,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
|
@ -67,32 +49,31 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
|||
text: "The specified user/hostmask is already ignored",
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
network.ignoreList.push({
|
||||
...hostmask,
|
||||
when: Date.now(),
|
||||
});
|
||||
|
||||
client.save();
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: MessageType.ERROR, // TODO: Successfully added via type.Error 🤔 ?
|
||||
text: `\u0002${hostmask.nick}!${hostmask.ident}@${hostmask.hostname}\u000f added to ignorelist`,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
case "unignore": {
|
||||
const idx = network.ignoreList.findIndex(function (entry) {
|
||||
return Helper.compareHostmask(entry, hostmask!);
|
||||
return Helper.compareHostmask(entry, hostmask);
|
||||
});
|
||||
|
||||
// Check if the entry exists before removing it, otherwise
|
||||
// let the user know.
|
||||
if (idx !== -1) {
|
||||
network.ignoreList.splice(idx, 1);
|
||||
client.save();
|
||||
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: MessageType.ERROR,
|
||||
text: `Successfully removed \u0002${hostmask!.nick}!${hostmask!.ident}@${
|
||||
hostmask!.hostname
|
||||
}\u000f from ignorelist`,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
if (idx === -1) {
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
|
@ -100,52 +81,20 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
|||
text: "The specified user/hostmask is not ignored",
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
network.ignoreList.splice(idx, 1);
|
||||
client.save();
|
||||
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: MessageType.ERROR, // TODO: Successfully removed via type.Error 🤔 ?
|
||||
text: `Successfully removed \u0002${hostmask.nick}!${hostmask.ident}@${hostmask.hostname}\u000f from ignorelist`,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
case "ignorelist":
|
||||
if (network.ignoreList.length === 0) {
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: MessageType.ERROR,
|
||||
text: "Ignorelist is empty",
|
||||
})
|
||||
);
|
||||
} else {
|
||||
const chanName = "Ignored users";
|
||||
const ignored = network.ignoreList.map((data) => ({
|
||||
hostmask: `${data.nick}!${data.ident}@${data.hostname}`,
|
||||
when: data.when,
|
||||
}));
|
||||
let newChan = network.getChannel(chanName);
|
||||
|
||||
if (typeof newChan === "undefined") {
|
||||
newChan = client.createChannel({
|
||||
type: ChanType.SPECIAL,
|
||||
special: SpecialChanType.IGNORELIST,
|
||||
name: chanName,
|
||||
data: ignored,
|
||||
});
|
||||
client.emit("join", {
|
||||
network: network.uuid,
|
||||
chan: newChan.getFilteredClone(true),
|
||||
index: network.addChannel(newChan),
|
||||
});
|
||||
} else {
|
||||
// TODO: add type for this chan/event
|
||||
newChan.data = ignored;
|
||||
|
||||
client.emit("msg:special", {
|
||||
chan: newChan.id,
|
||||
data: ignored,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
57
server/plugins/inputs/ignorelist.ts
Normal file
57
server/plugins/inputs/ignorelist.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
import Msg from "../../models/msg";
|
||||
import {ChanType, SpecialChanType} from "../../../shared/types/chan";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
const commands = ["ignorelist"];
|
||||
|
||||
const input: PluginInputHandler = function (network, chan, _cmd, _args) {
|
||||
const client = this;
|
||||
|
||||
if (network.ignoreList.length === 0) {
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: MessageType.ERROR,
|
||||
text: "Ignorelist is empty",
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const chanName = "Ignored users";
|
||||
const ignored = network.ignoreList.map((data) => ({
|
||||
hostmask: `${data.nick}!${data.ident}@${data.hostname}`,
|
||||
when: data.when,
|
||||
}));
|
||||
let newChan = network.getChannel(chanName);
|
||||
|
||||
if (typeof newChan === "undefined") {
|
||||
newChan = client.createChannel({
|
||||
type: ChanType.SPECIAL,
|
||||
special: SpecialChanType.IGNORELIST,
|
||||
name: chanName,
|
||||
data: ignored,
|
||||
});
|
||||
client.emit("join", {
|
||||
network: network.uuid,
|
||||
chan: newChan.getFilteredClone(true),
|
||||
shouldOpen: false,
|
||||
index: network.addChannel(newChan),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: add type for this chan/event
|
||||
newChan.data = ignored;
|
||||
|
||||
client.emit("msg:special", {
|
||||
chan: newChan.id,
|
||||
data: ignored,
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
commands,
|
||||
input,
|
||||
};
|
|
@ -54,6 +54,7 @@ const builtInInputs = [
|
|||
"ctcp",
|
||||
"disconnect",
|
||||
"ignore",
|
||||
"ignorelist",
|
||||
"invite",
|
||||
"kick",
|
||||
"kill",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import {ChanType} from "../../models/chan";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["invite", "invitelist"];
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import {ChanType} from "../../models/chan";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["kick"];
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import {ChanType} from "../../models/chan";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["mode", "umode", "op", "deop", "hop", "dehop", "voice", "devoice"];
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Chan, {ChanType} from "../../models/chan";
|
||||
import Msg from "../../models/msg";
|
||||
import Chan from "../../models/chan";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["query", "msg", "say"];
|
||||
|
||||
|
@ -97,10 +99,10 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
|||
// being sent back to us.
|
||||
if (!network.irc.network.cap.isEnabled("echo-message")) {
|
||||
const parsedTarget = network.irc.network.extractTargetGroup(targetName);
|
||||
let targetGroup;
|
||||
let targetGroup: string | undefined = undefined;
|
||||
|
||||
if (parsedTarget) {
|
||||
targetName = parsedTarget.target as string;
|
||||
targetName = parsedTarget.target;
|
||||
targetGroup = parsedTarget.target_group;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,9 +2,11 @@ import Chan from "../../models/chan";
|
|||
import Network from "../../models/network";
|
||||
import {PluginInputHandler} from "./index";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
|
||||
import Client from "../../client";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["mute", "unmute"];
|
||||
const allowDisconnected = true;
|
||||
|
@ -24,7 +26,7 @@ function args_to_channels(network: Network, args: string[]) {
|
|||
}
|
||||
|
||||
function change_mute_state(client: Client, target: Chan, valueToSet: boolean) {
|
||||
if (target.type === "special") {
|
||||
if (target.type === ChanType.SPECIAL) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
const commands = ["nick"];
|
||||
const allowDisconnected = true;
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import Config from "../../config";
|
||||
import {ChanType, ChanState} from "../../models/chan";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType, ChanState} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["close", "leave", "part"];
|
||||
const allowDisconnected = true;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import {ChanType} from "../../models/chan";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["cycle", "rejoin"];
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import {ChanType} from "../../models/chan";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["topic"];
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
import {ChanType} from "../../models/chan";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -14,7 +14,7 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
handleSTS(data, false);
|
||||
});
|
||||
|
||||
function handleSTS(data, shouldReconnect) {
|
||||
function handleSTS(data, shouldReconnect: boolean) {
|
||||
if (!Object.prototype.hasOwnProperty.call(data.capabilities, "sts")) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
/* eslint-disable @typescript-eslint/restrict-plus-operands */
|
||||
import _ from "lodash";
|
||||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import log from "../../log";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import Helper from "../../helper";
|
||||
import Config from "../../config";
|
||||
import {ChanType, ChanState} from "../../models/chan";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType, ChanState} from "../../../shared/types/chan";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import _ from "lodash";
|
||||
import {IrcEventHandler} from "../../client";
|
||||
import Helper from "../../helper";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import User from "../../models/user";
|
||||
import pkg from "../../../package.json";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
const ctcpResponses = {
|
||||
CLIENTINFO: () =>
|
||||
|
@ -78,7 +79,6 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
type: MessageType.CTCP_REQUEST,
|
||||
time: data.time,
|
||||
from: new User({nick: target}),
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
||||
hostmask: data.ident + "@" + data.hostname,
|
||||
ctcpMessage: data.message,
|
||||
});
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import Config from "../../config";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
@ -57,7 +58,6 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
if (irc.connection.registered === false) {
|
||||
const nickLen = parseInt(network.irc.network.options.NICKLEN, 10) || 16;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
||||
const random = (data.nick || irc.user.nick) + Math.floor(Math.random() * 10);
|
||||
|
||||
// Safeguard nick changes up to allowed length
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {IrcEventHandler} from "../../client";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {IrcEventHandler} from "../../client";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import User from "../../models/user";
|
||||
import type {IrcEventHandler} from "../../client";
|
||||
import {ChanState} from "../../models/chan";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanState} from "../../../shared/types/chan";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
@ -18,6 +19,7 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
client.emit("join", {
|
||||
network: network.uuid,
|
||||
chan: chan.getFilteredClone(true),
|
||||
shouldOpen: false,
|
||||
index: network.addChannel(chan),
|
||||
});
|
||||
client.save();
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
import {ChanState} from "../../models/chan";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import User from "../../models/user";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanState} from "../../../shared/types/chan";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
@ -14,11 +14,12 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
return;
|
||||
}
|
||||
|
||||
const user = chan.getUser(data.kicked!);
|
||||
const msg = new Msg({
|
||||
type: MessageType.KICK,
|
||||
time: data.time,
|
||||
from: chan.getUser(data.nick),
|
||||
target: chan.getUser(data.kicked!),
|
||||
target: user,
|
||||
text: data.message || "",
|
||||
highlight: data.kicked === irc.user.nick,
|
||||
self: data.nick === irc.user.nick,
|
||||
|
@ -34,7 +35,7 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
state: chan.state,
|
||||
});
|
||||
} else {
|
||||
chan.removeUser(msg.target as User);
|
||||
chan.removeUser(user);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@ import mime from "mime-types";
|
|||
import log from "../../log";
|
||||
import Config from "../../config";
|
||||
import {findLinksWithSchema} from "../../../shared/linkify";
|
||||
import {LinkPreview} from "../../../shared/types/msg";
|
||||
import storage from "../storage";
|
||||
import Client from "../../client";
|
||||
import Chan from "../../models/chan";
|
||||
|
@ -20,23 +21,6 @@ const currentFetchPromises = new Map<string, Promise<FetchRequest>>();
|
|||
const imageTypeRegex = /^image\/.+/;
|
||||
const mediaTypeRegex = /^(audio|video)\/.+/;
|
||||
|
||||
export type LinkPreview = {
|
||||
type: string;
|
||||
head: string;
|
||||
body: string;
|
||||
thumb: string;
|
||||
size: number;
|
||||
link: string; // Send original matched link to the client
|
||||
shown?: boolean | null;
|
||||
error?: string;
|
||||
message?: string;
|
||||
|
||||
media?: string;
|
||||
mediaType?: string;
|
||||
maxSize?: number;
|
||||
thumbActualUrl?: string;
|
||||
};
|
||||
|
||||
export default function (client: Client, chan: Chan, msg: Msg, cleanText: string) {
|
||||
if (!Config.values.prefetch) {
|
||||
return;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Chan, {ChanType, SpecialChanType} from "../../models/chan";
|
||||
import Chan from "../../models/chan";
|
||||
import {ChanType, SpecialChanType} from "../../../shared/types/chan";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
@ -50,6 +51,7 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
client.emit("join", {
|
||||
network: network.uuid,
|
||||
chan: chan.getFilteredClone(true),
|
||||
shouldOpen: false,
|
||||
index: network.addChannel(chan),
|
||||
});
|
||||
} else {
|
||||
|
|
|
@ -1,54 +1,54 @@
|
|||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import LinkPrefetch from "./link";
|
||||
import {cleanIrcMessage} from "../../../shared/irc";
|
||||
import Helper from "../../helper";
|
||||
import {IrcEventHandler} from "../../client";
|
||||
import Chan, {ChanType} from "../../models/chan";
|
||||
import Chan from "../../models/chan";
|
||||
import User from "../../models/user";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
import {MessageEventArgs} from "irc-framework";
|
||||
|
||||
const nickRegExp = /(?:\x03[0-9]{1,2}(?:,[0-9]{1,2})?)?([\w[\]\\`^{|}-]+)/g;
|
||||
|
||||
type HandleInput = {
|
||||
nick: string;
|
||||
hostname: string;
|
||||
ident: string;
|
||||
target: string;
|
||||
type: MessageType;
|
||||
time?: number;
|
||||
text?: string;
|
||||
from_server?: boolean;
|
||||
message: string;
|
||||
group?: string;
|
||||
};
|
||||
|
||||
function convertForHandle(type: MessageType, data: MessageEventArgs): HandleInput {
|
||||
return {...data, type: type};
|
||||
}
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
||||
irc.on("notice", function (data) {
|
||||
data.type = MessageType.NOTICE;
|
||||
|
||||
type ModifiedData = typeof data & {
|
||||
type: MessageType.NOTICE;
|
||||
};
|
||||
|
||||
handleMessage(data as ModifiedData);
|
||||
handleMessage(convertForHandle(MessageType.NOTICE, data));
|
||||
});
|
||||
|
||||
irc.on("action", function (data) {
|
||||
data.type = MessageType.ACTION;
|
||||
handleMessage(data);
|
||||
handleMessage(convertForHandle(MessageType.ACTION, data));
|
||||
});
|
||||
|
||||
irc.on("privmsg", function (data) {
|
||||
data.type = MessageType.MESSAGE;
|
||||
handleMessage(data);
|
||||
handleMessage(convertForHandle(MessageType.MESSAGE, data));
|
||||
});
|
||||
|
||||
irc.on("wallops", function (data) {
|
||||
data.from_server = true;
|
||||
data.type = MessageType.WALLOPS;
|
||||
handleMessage(data);
|
||||
handleMessage(convertForHandle(MessageType.WALLOPS, data));
|
||||
});
|
||||
|
||||
function handleMessage(data: {
|
||||
nick: string;
|
||||
hostname: string;
|
||||
ident: string;
|
||||
target: string;
|
||||
type: MessageType;
|
||||
time: number;
|
||||
text?: string;
|
||||
from_server?: boolean;
|
||||
message: string;
|
||||
group?: string;
|
||||
}) {
|
||||
function handleMessage(data: HandleInput) {
|
||||
let chan: Chan | undefined;
|
||||
let from: User;
|
||||
let highlight = false;
|
||||
|
@ -105,6 +105,7 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
client.emit("join", {
|
||||
network: network.uuid,
|
||||
chan: chan.getFilteredClone(true),
|
||||
shouldOpen: false,
|
||||
index: network.addChannel(chan),
|
||||
});
|
||||
client.save();
|
||||
|
@ -125,7 +126,7 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
// msg is constructed down here because `from` is being copied in the constructor
|
||||
const msg = new Msg({
|
||||
type: data.type,
|
||||
time: data.time as any,
|
||||
time: data.time ? new Date(data.time) : undefined,
|
||||
text: data.message,
|
||||
self: self,
|
||||
from: from,
|
||||
|
@ -164,7 +165,6 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
|
||||
while ((match = nickRegExp.exec(data.message))) {
|
||||
if (chan.findUser(match[1])) {
|
||||
// @ts-expect-error Type 'string' is not assignable to type '{ mode: string; }'.ts(2345)
|
||||
msg.users.push(match[1]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import _ from "lodash";
|
||||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
@ -34,7 +35,6 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
|
||||
const msg = new Msg({
|
||||
type: MessageType.MODE_CHANNEL,
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
text: `${data.raw_modes} ${data.raw_params.join(" ")}`,
|
||||
});
|
||||
targetChan.pushMessage(client, msg);
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
import {SpecialChanType, ChanType} from "../../models/chan";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {SpecialChanType, ChanType} from "../../../shared/types/chan";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
@ -68,6 +69,7 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
client.emit("join", {
|
||||
network: network.uuid,
|
||||
chan: chan.getFilteredClone(true),
|
||||
shouldOpen: false,
|
||||
index: network.addChannel(chan),
|
||||
});
|
||||
} else {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
@ -13,7 +14,6 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
|
||||
const lobby = network.getLobby();
|
||||
const msg = new Msg({
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
text: `You're now known as ${data.new_nick}`,
|
||||
});
|
||||
lobby.pushMessage(client, msg, true);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
@ -10,7 +11,6 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
|
||||
const msg = new Msg({
|
||||
type: MessageType.LOGIN,
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
||||
text: "Logged in as: " + data.account,
|
||||
});
|
||||
lobby.pushMessage(client, msg, true);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
import {ChanType} from "../../models/chan";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
@ -28,9 +29,9 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
});
|
||||
|
||||
client.emit("join", {
|
||||
shouldOpen: true,
|
||||
network: network.uuid,
|
||||
chan: chan.getFilteredClone(true),
|
||||
shouldOpen: true,
|
||||
index: network.addChannel(chan),
|
||||
});
|
||||
chan.loadMessages(client, network);
|
||||
|
@ -43,7 +44,6 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
if (data.error) {
|
||||
msg = new Msg({
|
||||
type: MessageType.ERROR,
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
||||
text: "No such nick: " + data.nick,
|
||||
});
|
||||
} else {
|
||||
|
|
|
@ -7,8 +7,9 @@ import Config from "../../config";
|
|||
import Msg, {Message} from "../../models/msg";
|
||||
import Chan, {Channel} from "../../models/chan";
|
||||
import Helper from "../../helper";
|
||||
import type {SearchResponse, SearchQuery, SearchableMessageStorage, DeletionRequest} from "./types";
|
||||
import type {SearchableMessageStorage, DeletionRequest} from "./types";
|
||||
import Network from "../../models/network";
|
||||
import {SearchQuery, SearchResponse} from "../../../shared/types/storage";
|
||||
|
||||
// TODO; type
|
||||
let sqlite3: any;
|
||||
|
|
|
@ -6,8 +6,9 @@ import filenamify from "filenamify";
|
|||
import Config from "../../config";
|
||||
import {MessageStorage} from "./types";
|
||||
import Channel from "../../models/chan";
|
||||
import {Message, MessageType} from "../../models/msg";
|
||||
import {Message} from "../../models/msg";
|
||||
import Network from "../../models/network";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
class TextFileMessageStorage implements MessageStorage {
|
||||
isEnabled: boolean;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue