Compare commits

...

129 commits

Author SHA1 Message Date
Reto Brunner 0d9c184f19 server: fix time handling
The framework may emit messages which do not have a time stamp.
We tried to unconditionally convert the time field, fix that.

The Msg constructor replaces falsey time fields with the current
date so we can also remove the duplication from that codepath.
2024-05-16 22:15:39 +02:00
Max Leiter 4de413070d
Merge pull request #4873 from thelounge/types
client: remove setAppBadge and clearAppBadge
2024-05-12 23:08:52 -07:00
Reto Brunner 0955d9df06 Identd: fix various issues
There's a bunch of sub optimal behavior from our ident server.
For one, it allows user enumeration which we don't really want and it doesn't clean up connections that don't send any data.

Fix that
2024-05-12 11:51:18 +02:00
Reto Brunner cb4aaf6a97 client: remove setAppBadge and clearAppBadge
They already ship with typescript's lib.dom.d.ts
2024-05-10 13:21:18 +02:00
Reto Brunner 45c2fc87ee client: properly type Socket in window 2024-05-10 12:27:41 +02:00
Reto Brunner 29fcc2da05 ident: close connections if they don't send data 2024-05-09 19:01:10 +02:00
Reto Brunner 12679081c8 ident: fix write after connection end
We only respond once to data, then half-close the connection.
Hence, we should only listen to a single data event as well,
else if the remote doesn't stop sending data we keep trying to
write to the closed write end of the pipe.
2024-05-09 19:01:10 +02:00
Reto Brunner 0e48014d5a ident: only respond if the ip,port tuples match
Per RFC 1413, The uniquely identifying tuple includes not only the ports,
but also both addresses.

If multiple connections happen to use the same local port number
(which is possible if the addresses differ), the username of the first
is returned for all, resulting in the wrong ident for all but the
first.

By not checking the connection address, the information becomes
public. Because there is only relatively small number of local ports,
and the remote ports are likely to be either 6667 or 6697, it becomes
trivial to enumerate all the users.

Co-Authored-By: Juerd Waalboer <juerd@tnx.nl>
2024-05-09 19:01:10 +02:00
Reto Brunner 4819406af5 ident: order imports 2024-05-09 17:24:53 +02:00
Max Leiter 74563effa7
Merge pull request #4869 from thelounge/tsUpdate
Ts update
2024-05-04 21:49:58 -07:00
Max Leiter cbab10f416
Merge pull request #4870 from thelounge/dts
remove unused .d.ts file
2024-05-04 21:46:54 -07:00
Reto Brunner 4dfeb899b4 remove unused .d.ts file 2024-05-04 13:45:40 +02:00
Reto Brunner 3259ac596d client: fix all new linter errros 2024-05-04 12:29:57 +02:00
Reto Brunner 3fbbc39cd6 client/commands: statically import commands
Dynamic imports won't work very well with modules and we don't
really need them, it's just there to save us an import statement.

Let's flip this to a static version.
2024-05-04 12:29:57 +02:00
Reto Brunner 9ae9482223 bump all eslint related deps 2024-05-02 08:21:34 +02:00
Reto Brunner a3953405ed bump eslint to latest 8.* 2024-04-27 13:19:39 +02:00
Reto Brunner 9086bc648d bump typescript to v5 2024-04-27 13:06:30 +02:00
Reto Brunner da2572fe25 Merge remote-tracking branch 'origin/renovate/read-0.x' 2024-04-27 12:50:11 +02:00
Reto Brunner d9977df315 Merge remote-tracking branch 'origin/renovate/sqlite3-3.x' 2024-04-27 12:48:20 +02:00
Reto Brunner cc0aa5e8e5 Merge remote-tracking branch 'origin/renovate/npm-webpack-dev-middleware-vulnerability' 2024-04-27 12:47:01 +02:00
Reto Brunner 02df78b0f2 Merge remote-tracking branch 'origin/renovate/npm-express-vulnerability' 2024-04-27 12:46:42 +02:00
Reto Brunner 18b0e06855 Merge remote-tracking branch 'origin/renovate/express-4.x' 2024-04-27 12:46:27 +02:00
Reto Brunner d5db9c653b Merge remote-tracking branch 'origin/renovate/ua-parser-js-0.x' 2024-04-27 12:46:09 +02:00
Reto Brunner f7926267d9 untangle client and server
Our project was quite confused as to the boundaries between client and
server code.
This false sharing meant that it was quite hard to tell what was actually
sent to the client and what was uniquely scoped to either side.

Further, this meant that our compilation and build pipelines were very
confused and pulled in files they should not have.

This commit series tries to untangle the two. This also entails fixing
quite some typing issues.
It's hard to make this in sane, small, commits that still build at each
step (it's impossible, as fixing one type error / any type immediately lead
to further errors in a game of whack a mole).
So you'll get my actual progress in small commits that can each be reviewed,
however the earlier ones are in fact sometimes wrong and get cleaned up later
once the picture is a bit clearer.
2024-04-26 09:39:15 +02:00
Reto Brunner 8eb398c5cc server: don't throw in async callback from index requests
This was flagged as an issue by codeQL

> Server crash [High]
> The server of this route handler will terminate when an
> uncaught exception from this location escapes an
> asynchronous callback.
2024-04-21 15:49:51 +02:00
Reto Brunner 36cb75ee99 NetworkForm: disable unsafe return lints for now
The NetworkForm type is wrong, hence the compiler can't infer the type.
This needs quite some changes, so for now we just turn the linter off
for the 2 watch functions.
The whole component is too dynamic to fix easily.
2024-04-21 15:11:52 +02:00
Reto Brunner 1ec67a6605 test/sqlite: remove unused eslint directive 2024-04-21 15:11:52 +02:00
Reto Brunner 8372c5a57e test: token in init event payload is undefined not null
Doesn't matter which, code happens to emit undefined.
Adapt test expectation over writing strange || null code.
The conditional just checks for a falsey value.
2024-04-21 15:11:52 +02:00
Reto Brunner 5567f07a7c test/chan: remove users field from test
The client side fetches the user list when needed, we don't send
it over from the server.
Hence modify the test expectation.
2024-04-21 15:11:52 +02:00
Reto Brunner a200bab8bd test/chan: getFilteredClone has more fields than the test thinks
The codebase shoves various things into channel objects to transmit them
for things like channel lists etc.

This however means that the type does contains the fields and needs
to export them.

We should clean up the events so that we can get rid of all that.
But for now, we adapt the test expectation to reality.
2024-04-21 15:11:52 +02:00
Reto Brunner 91ac363cc6 components/MessageTypes/errors: fix eslint errors
This makes the code somewhat ugly, but to properly fix we need
to enforce the needed fields
2024-04-21 15:11:52 +02:00
Reto Brunner 6c9d2c36a1 components/Message: fix eslint errors 2024-04-21 15:11:52 +02:00
Reto Brunner 6241eed8f4 client/ImageViewer: fix types and guard against undefined 2024-04-21 15:11:52 +02:00
Reto Brunner 03151e0ab1 test/plugins/sqlite: fix import path 2024-04-21 15:11:52 +02:00
Reto Brunner 7f5e0f3ebf test/plugins/link: fix import path 2024-04-21 15:11:52 +02:00
Reto Brunner 5e444be37b test/models/network: fix import path 2024-04-21 15:11:52 +02:00
Reto Brunner c8664301ba test/models/msg: fix linkpreview import 2024-04-21 15:11:52 +02:00
Reto Brunner 1edb5a72c1 test/models/chan: we do not send an empty user list anymore 2024-04-21 15:11:52 +02:00
Reto Brunner 31d987283a fix mode tests 2024-04-21 15:11:52 +02:00
Reto Brunner 4ceafb653f test/client: fix import path 2024-04-21 15:11:52 +02:00
Reto Brunner f25fee4c6c previews: fix possibly undefined 2024-04-21 15:11:52 +02:00
Reto Brunner 96848c1c1b msg_preview: fix possibly undefined error 2024-04-21 15:11:52 +02:00
Reto Brunner 4b07e05491 client: add missing import for SharedMsg 2024-04-21 15:11:52 +02:00
Reto Brunner fc9805545b sharedMsg: remove userAway
userAway is purely server side and we don't send it to the client
2024-04-21 15:11:52 +02:00
Reto Brunner 82e4150cc8 server: remove type cast from change pw 2024-04-21 15:11:52 +02:00
Reto Brunner e61e356f1e server: somewhat type fix auth related functions
The auth functions are a bloody mess and need to be cleaned up.
using various callback functions and using variables as pointers makes the logic
hard to follow and hence idiotic to type too, as multiple orthogonal logic paths
are mixed up into one function.

This really needs to be untangled
2024-04-21 15:11:52 +02:00
Reto Brunner 5001d607b1 server: mark req params as unused 2024-04-21 15:11:52 +02:00
Reto Brunner 8c41356ae9 publicClient: type fix 2024-04-21 15:11:52 +02:00
Reto Brunner e2b56cf16b irc-events/message: fix types 2024-04-21 15:11:52 +02:00
Reto Brunner 92a0affba1 kick: use the user object 2024-04-21 15:11:52 +02:00
Reto Brunner edb96f683b cap: type the boolean 2024-04-21 15:11:52 +02:00
Reto Brunner 5c8951ffc3 fix extractTargetGroup typing 2024-04-21 15:11:52 +02:00
Reto Brunner c3fc54e158 ignorelist: shut up the linter 2024-04-21 15:11:52 +02:00
Reto Brunner 917fdb2a0a ignore: remove dead import 2024-04-21 15:11:52 +02:00
Reto Brunner b8400a3a46 ignore: clean up the types and conditionals
Now that ignorelist doesn't muddy the waters, we can clean up
all the funny conditional types and enforce `when`
2024-04-21 15:11:52 +02:00
Reto Brunner 071a5afda6 ignore: move ignorelist to its own command
ignorelist shares no logic with /ignore or /unignore so it shouldn't
share a file. That just makes typing awkward.
2024-04-21 15:11:52 +02:00
Reto Brunner 5274fdc21a ignore: keep happy path on the left
It is much easier to follow the control flow if error checks
are done on the indented path, immediately returning.
2024-04-21 15:11:52 +02:00
Reto Brunner b8a9fe08ab clientCertificate: remove unsafe casts 2024-04-21 15:11:52 +02:00
Reto Brunner a4afa08add ldap: type SearchOptions scope 2024-04-21 15:11:52 +02:00
Reto Brunner 4614c35486 chan: type untyped method params 2024-04-21 15:11:52 +02:00
Reto Brunner 540144c417 chan: remove cast in pushMessage 2024-04-21 15:11:52 +02:00
Reto Brunner bb7c3925c6 type serverOptions for network:options 2024-04-21 15:11:52 +02:00
Reto Brunner 9898f38de6 add todo 2024-04-21 15:11:52 +02:00
Reto Brunner 9f2c82e152 fix mentions 2024-04-21 15:11:52 +02:00
Reto Brunner 17ba07db3b fix mentions import 2024-04-21 15:11:52 +02:00
Reto Brunner 0311e5f836 add socket-events import to entry point
socket-events aren't ever imported, if we don't do that however
webpack never actually sees any code that leads to it and skips
bundling it.

So for now, do an import that has the side effect of registering
all the events until we have a proper registration in place that's
a bit more sane to call
2024-04-21 15:11:52 +02:00
Reto Brunner 4d0474b897 store: don't duplicate import 2024-04-21 15:11:52 +02:00
Reto Brunner 14b9169899 store: fix import 2024-04-21 15:11:52 +02:00
Reto Brunner 50037644c0 socket-events: fix join 2024-04-21 15:11:52 +02:00
Reto Brunner 7287c6bcaa remove dead import 2024-04-21 15:11:52 +02:00
Reto Brunner bfca0ca612 fix more 2024-04-21 15:11:52 +02:00
Reto Brunner 300bd4c84c add timestamp to NotificationOptions 2024-04-21 15:11:52 +02:00
Reto Brunner 42ea66c343 socket-events/msg: fix errors 2024-04-21 15:11:51 +02:00
Reto Brunner 1565eb8d05 socket-events/msg: if/else chains are not a switch replacement
If we switch on a field, use switch for god's sake.
If/elif chains are for cases where you have multiple selectors.
2024-04-21 15:11:51 +02:00
Reto Brunner 29750a3e51 ClientChan: does need a user array after all 2024-04-21 15:11:51 +02:00
Reto Brunner 3ea5170e6a socket-events: fix network:status 2024-04-21 15:11:51 +02:00
Reto Brunner fe4f497fad fix socket-event: network 2024-04-21 15:11:51 +02:00
Reto Brunner c20cd6bda1 publicClient: add FIXME and ignore the type mismatch
The publicClient interface is utterly horrific.
It allows any client to inject arbitrary events into the socket.io
event stream.
This should get wrapped into a "plugin" event so that it can get properly
typed, better yet, this should get removed completely.
2024-04-21 15:11:51 +02:00
Reto Brunner 1c4ce5d4a5 fix sync_sort:channels emitter 2024-04-21 15:11:51 +02:00
Reto Brunner 9c4d24d1f7 fix join socket type 2024-04-21 15:11:51 +02:00
Reto Brunner 35e38d13c4 client: properly type the emit method
This breaks the world -.-
2024-04-21 15:11:51 +02:00
Reto Brunner bf7eb0e727 network event: remove unused array
All the network events only ever emit a single copy
There's no point in wrapping it into an array
2024-04-21 15:11:51 +02:00
Reto Brunner 5ee9c2b338 type Server 2024-04-21 15:11:51 +02:00
Reto Brunner e15b121080 remove obsolete error override 2024-04-21 15:11:51 +02:00
Reto Brunner 98452ccc18 remove obsolete import 2024-04-21 15:11:51 +02:00
Reto Brunner a8e7022d04 fix search event params 2024-04-21 15:11:51 +02:00
Reto Brunner 60486bf5e3 server: fix init client 2024-04-21 15:11:51 +02:00
Reto Brunner 46f3fd9682 server: fix push subscription 2024-04-21 15:11:51 +02:00
Reto Brunner 56215382a3 server: remove static props which are currently unused 2024-04-21 15:11:51 +02:00
Reto Brunner 9ab9ad0f56 socket-events: fix up init 2024-04-21 15:11:51 +02:00
Reto Brunner 0660a8772c server: fix getFilteredClone of chan 2024-04-21 15:11:51 +02:00
Reto Brunner f5c691f37b wip: unbork init progress 2024-04-21 15:11:51 +02:00
Reto Brunner 0067c30273 Split sort event
The sort event bundled networks and channels for no reason at all.
They share none of the actual logic, so combining them just makes
the typing poor but serves no benefit.
2024-04-21 15:11:51 +02:00
Reto Brunner 843db1727b server: actually type the socket 2024-04-21 15:11:51 +02:00
Reto Brunner e9ef59b641 fix bad typing
There were quite some errors, where the type was passed the wrong way
```
// This is invalid
"change-password": ({ old_password: string, new_password: string, verify_password: string})

// What was actually meant
"change-password": (data: { old_password: string, new_password: string, verify_password: string})
```

The whole callback function is also very verbose as is, with fluff we don't need.
It's always a function that returns void, so there's no real information to be gained
by spelling it out time and time again.

Let's use a helper type that just accepts the payload.
That should make the above error impossible to do.
2024-04-21 15:11:51 +02:00
Reto Brunner fceffd42b9 fix missing import 2024-04-21 15:11:51 +02:00
Reto Brunner b89b0cad53 client: id is always a string, not a number 2024-04-21 15:11:51 +02:00
Reto Brunner c869ea9a73 sharedchans does not have users 2024-04-21 15:11:51 +02:00
Reto Brunner 9aee3e3e98 Some whitespace is good mkey 2024-04-21 15:11:51 +02:00
Reto Brunner 636b5c5b04 models/network: unfuck client export 2024-04-21 15:11:51 +02:00
Reto Brunner 6984e8f25a unused import 2024-04-21 15:11:51 +02:00
Reto Brunner e43cbb139c remove all server files from compilation 2024-04-21 15:11:51 +02:00
Reto Brunner e57e547b74 further chan fixes 2024-04-21 15:11:51 +02:00
Reto Brunner 3217536245 searchresponse 2024-04-21 15:11:51 +02:00
Reto Brunner 194b4e1a2f import fix 2024-04-21 15:11:51 +02:00
Reto Brunner 88c8830a17 chatuserlist 2024-04-21 15:11:51 +02:00
Reto Brunner 7073584f1c fix msg event 2024-04-21 15:11:51 +02:00
Reto Brunner 8e6920af1d configuration 2024-04-21 15:11:51 +02:00
Reto Brunner 7bc184b252 changelog data type 2024-04-21 15:11:51 +02:00
Reto Brunner 4d237600d5 changelog: don't type assert to a broken type
The mandatory fields are unset, stop lying to the compiler
2024-04-21 15:11:51 +02:00
Reto Brunner 383907c2b8 Use SharedTypes 2024-04-21 15:10:45 +02:00
Reto Brunner f0ee3be6fb wip: config 2024-04-21 15:10:45 +02:00
Reto Brunner 12a0b0b6f9 network 2024-04-21 15:10:45 +02:00
Reto Brunner d716402da2 mention 2024-04-21 15:10:45 +02:00
Reto Brunner d0b71aba32 shared: extract chan + user 2024-04-21 15:10:45 +02:00
Reto Brunner 3f0ee6a961 move chan enums 2024-04-21 15:10:45 +02:00
Reto Brunner b67e4699f5 wip sharedmsg in client 2024-04-21 15:10:45 +02:00
Reto Brunner 68ba13ca12 wip: searchquery 2024-04-21 15:10:45 +02:00
Reto Brunner 3eb19135f5 wip: msg 2024-04-21 15:10:41 +02:00
Max Leiter 549c445853
Merge pull request #4856 from thelounge/generateConfigDoc
fix generate-config-doc.js
2024-04-07 19:38:13 -07:00
Reto Brunner 2466c1b1e4 fix generate-config-doc.js
It errored out with
> Error: Cannot find module '../server/log'
Which is expected, but we don't really need it, we can just open code
the log functions
2024-04-07 16:22:08 +02:00
Reto Brunner f5867c3643 v4.4.3
Bump version to kick CI/CD for the actual deployment
2024-04-06 13:48:04 +02:00
Reto Brunner 231c498def release workflow: fix broken npm update
The release workflow wants to use --provenance but the update fails:

Run npm install -g npm
npm ERR! code EACCES
npm ERR! syscall mkdir
npm ERR! path /usr/local/share/man/man7
npm ERR! errno -13
npm ERR! Error: EACCES: permission denied, mkdir '/usr/local/share/man/man7'
npm ERR!  [Error: EACCES: permission denied, mkdir '/usr/local/share/man/man7'] {
npm ERR!   errno: -13,
npm ERR!   code: 'EACCES',
npm ERR!   syscall: 'mkdir',
npm ERR!   path: '/usr/local/share/man/man7'
npm ERR! }
npm ERR!
npm ERR! The operation was rejected by your operating system.
npm ERR! It is likely you do not have the permissions to access this file as the current user
npm ERR!
npm ERR! If you believe this might be a permissions issue, please double-check the
npm ERR! permissions of the file and its containing directories, or try running
npm ERR! the command again as root/Administrator.

So we fix it by just telling the action what we want rather than monkey patching it.

Link: https://github.com/thelounge/thelounge/actions/runs/8580746748/job/23517165611
2024-04-06 13:31:48 +02:00
renovate[bot] e8f6ba5b08
chore(deps): update dependency @types/ua-parser-js to v0.7.39 2024-04-01 00:49:35 +00:00
renovate[bot] 07276bbde4
chore(deps): update dependency @types/express to v4.17.21 2024-04-01 00:49:23 +00:00
renovate[bot] 9ad92e1860
fix(deps): update dependency express to v4.19.2 [security] 2024-03-28 00:33:05 +00:00
renovate[bot] 7923d4a2cd
chore(deps): update dependency webpack-dev-middleware to v5.3.4 [security] 2024-03-23 16:07:59 +00:00
renovate[bot] 9248358169
chore(deps): update dependency @types/sqlite3 to v3.1.11 2024-03-01 02:16:09 +00:00
renovate[bot] 6ab52bc9a9
chore(deps): update dependency @types/read to v0.0.32 2024-03-01 02:15:57 +00:00
128 changed files with 1948 additions and 1564 deletions

View file

@ -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;

View file

@ -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

View file

@ -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

View file

@ -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;
}
};

View file

@ -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;
}
}

View file

@ -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] = [];

View file

@ -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);

View file

@ -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";
};

View file

@ -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]++;
}
}

View file

@ -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
);

View file

@ -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;

View file

@ -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];

View file

@ -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),
});
};

View file

@ -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> & {

View file

@ -48,7 +48,7 @@ export default defineComponent({
watch(
() => route.params.uuid,
(newValue) => {
() => {
setNetworkData();
}
);

View file

@ -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",

View file

@ -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
View 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;
}

View file

@ -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};

View file

@ -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};

View file

@ -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,
};

View file

@ -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;
}

View file

@ -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};

View file

@ -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",

View file

@ -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(

View file

@ -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",
{

View file

@ -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;
}
}

View file

@ -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(() => {});
}
}

View file

@ -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,

View file

@ -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);
}

View file

@ -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) {

View file

@ -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;
}

View file

@ -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;

View file

@ -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;
}
}
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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;
});
}
});

View file

@ -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));
});

View file

@ -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,

View file

@ -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
View file

@ -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;
};

View file

@ -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(() => {});
}
}
}

View file

@ -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": {

View file

@ -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"
}
}

View file

@ -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();
}

View file

@ -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"
) {

View file

@ -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}`);
}

View file

@ -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 = {

View file

@ -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
View file

@ -1 +0,0 @@
import "./types";

View file

@ -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());
}

View file

@ -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[];

View file

@ -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() {

View file

@ -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) {

View file

@ -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}`);
});
};

View file

@ -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();

View file

@ -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;
}

View file

@ -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,

View file

@ -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"];

View file

@ -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"];

View file

@ -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;

View file

@ -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"];

View file

@ -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;
}
};

View 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,
};

View file

@ -54,6 +54,7 @@ const builtInInputs = [
"ctcp",
"disconnect",
"ignore",
"ignorelist",
"invite",
"kick",
"kill",

View file

@ -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"];

View file

@ -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"];

View file

@ -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"];

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;

View file

@ -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;

View file

@ -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"];

View file

@ -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"];

View file

@ -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;

View file

@ -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;
}

View file

@ -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;

View file

@ -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;

View file

@ -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,
});

View file

@ -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

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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();

View file

@ -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);
}
});
};

View file

@ -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;

View file

@ -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 {

View file

@ -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]);
}
}

View file

@ -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);

View file

@ -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 {

View file

@ -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;

View file

@ -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);

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -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;

View file

@ -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;

View file

@ -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 {

View file

@ -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;

View file

@ -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