Compare commits

...

289 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
Reto Brunner eeaec413d6 v4.4.2 2024-04-03 08:20:36 +02:00
Reto Brunner 515f894c13 changelog: don't break if author is nil
The author field can somehow be null for whatever reason...
Guard the script against blowing up
2024-04-01 15:15:50 +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
Reto Brunner 48213955b9 v4.4.2-rc.1 2024-02-19 19:52:37 +01:00
Reto Brunner 682b3b91aa rc changelog 2024-02-19 19:46:46 +01:00
Reto Brunner be3e27aa19 Merge remote-tracking branch 'origin/pull/4231' 2024-02-19 13:50:00 +01:00
Reto Brunner c09f751552 Merge remote-tracking branch 'origin/pull/4834' 2024-02-19 13:45:53 +01:00
Reto Brunner fb5864ee00 Merge branch 'linkifyOverride' 2024-02-19 13:43:00 +01:00
Reto Brunner 3bd5b704c7 Merge remote-tracking branch 'origin/renovate/node-17.x' 2024-02-19 13:37:06 +01:00
Reto Brunner 139ce47b73 update @types/mousetrap 2024-02-19 13:34:46 +01:00
Zach Bloomquist 45563d9a59
server: remove version from CTCP response 2024-02-01 00:09:45 -05:00
renovate[bot] e2fda1fb84
chore(deps): update dependency @types/node to v17.0.45 2024-02-01 03:42:37 +00:00
renovate[bot] a77fbb894f
chore(deps): update dependency @types/mousetrap to v1.6.15 2024-02-01 03:42:30 +00:00
renovate[bot] fe50a90235
chore(deps): update dependency @types/lodash to v4.14.202 2024-02-01 01:15:55 +00:00
Reto Brunner a8be84028c Merge rm-node-16 2024-01-28 10:55:12 +01:00
Max Leiter 25e55ce75c
Merge pull request #4815 from thelounge/renovate/sqlite3-5.x
chore(deps): update dependency sqlite3 to v5.1.7
2024-01-27 15:56:29 -08:00
Max Leiter 113e9bd2fb Remove Node.js 16 from package.json and testing matrix
Node.js 16 entered EOL in September 2023 (https://nodejs.org/en/blog/announcements/nodejs16-eol)
2024-01-27 15:55:55 -08:00
Max Leiter 2b146ba3e6
Merge pull request #4825 from thelounge/testingSetup
Testing setup
2024-01-27 15:40:09 -08:00
Max Leiter f95dd29a0d
Merge pull request #4811 from thelounge/renovate/textcomplete-textarea-0.x
chore(deps): update dependency @textcomplete/textarea to v0.1.13
2024-01-27 15:40:01 -08:00
Max Leiter 91dc719c93
Merge branch 'master' into testingSetup 2024-01-27 15:34:07 -08:00
Max Leiter 5af893db3a
Merge pull request #4826 from thelounge/huskyDie
Remove husky, add githooks-install
2024-01-27 15:33:26 -08:00
Nachtalb daabb76781
Add shorcut to navigate between channels with undread msgs 2024-01-27 22:50:10 +01:00
Reto Brunner 393d0a63b7 Remove husky, add githooks-install
Fixes: https://github.com/thelounge/thelounge/issues/4452
2024-01-27 16:59:46 +01:00
Reto Brunner 037fc479b8 test: be specific as to which command we want to invoke 2024-01-27 15:41:46 +01:00
Reto Brunner 646bafab99 mocha: move spec to invocation
If we specify the spec in the config file, we can't manually
specify a specific test file from the cli.

This is annoying, as the alternative is copying out the full
package.json blurb into the shell.

Rather, give the spec in the invocation and add a helper
that makes testing a specific file simple.

With this `yarn test:nospec test/plugins/link.ts` will only run
tests within that file
2024-01-27 15:34:13 +01:00
Reto Brunner d4c77c74f6 test:mocha remove duplicate flags
We already specify color in the config file.
ts-node is already required in the config file
2024-01-27 15:26:37 +01:00
Reto Brunner eeefeb229c mocharc: Remove interactive
Interactive isn't a thing according to the help output or the
config docs
2024-01-27 15:24:09 +01:00
Reto Brunner 29c5323bfd test:mocha: webpack doesn't switch on NODE_ENV=test
So we might as well not complicate the cli for no reason
2024-01-27 14:11:30 +01:00
Reto Brunner a12ddc75d8 test:mocha don't run coverage report
Tests should run the tests, not the coverage.
Frequently one is debugging a test, the coverage won't change
between runs but it delays the cycle considerably.

Rather, if one wants to look at the coverage, one should use
the "coverage" command
2024-01-27 14:11:30 +01:00
Reto Brunner dd24cb1300 linkify: simplify noscheme detection logic
Overriding the built in is poor form, as this prevents adding
a new type handler with its own normalize handler.

We only ever want to override protocol-less URLs to http, so
we just do so explicitly in the "//" schema normalizer.

This also means that we don't need all that type conversion dance,
we simply set the schema to null when we patch it and filter on the
schema directly
2024-01-21 21:18:09 +01:00
SoniEx2 ae6bae69ac linkify: Add web+ schema support
Co-Authored-By: Reto Brunner <reto@slightlybroken.com>
2024-01-21 17:47:32 +01:00
renovate[bot] b5372e3ed7
chore(deps): update dependency sqlite3 to v5.1.7 2024-01-06 10:39:32 +00:00
Reto Brunner d15998d919 Merge renovate/content-disposition-0.x 2024-01-04 19:04:00 +01:00
Reto Brunner 436bf6a180 Merge renovate/linkify-it-3.x 2024-01-04 19:03:40 +01:00
renovate[bot] 1d2fdd95b0
chore(deps): update dependency @types/linkify-it to v3.0.5 2024-01-01 03:42:56 +00:00
renovate[bot] eaa70caad7
chore(deps): update dependency @types/is-utf8 to v0.2.3 2024-01-01 03:42:48 +00:00
renovate[bot] aa95032760
chore(deps): update dependency @types/content-disposition to v0.5.8 2024-01-01 01:41:48 +00:00
renovate[bot] e636121d7a
chore(deps): update dependency @textcomplete/textarea to v0.1.13 2024-01-01 01:41:39 +00:00
Max Leiter 083abae750
Merge pull request #4783 from thelounge/router-api
router: don't use next() in router guards
2023-12-26 16:49:37 -08:00
Max Leiter 01cfe3d19d
Merge pull request #4807 from flotwig/fixup-generate-config-docs-script
scripts: fix generate-config-doc, handle usage errors
2023-12-26 16:49:20 -08:00
Reto Brunner 7f0b721790 add storage cleaner
Introduce the ability to clean up old messages from the sqlite db.
The StoragePolicy can be chosen by the user. Currently there's
two versions, delete everything based on age is the obvious.

The other is for the data hoarders among us. It'll only delete
message types which can be considered low value... Types with
a time aspect like away / back... joins / parts etc.

It tries to do that in a sensible way, so that we don't block
all other db writers that are ongoing.
The "periodically" interval is by design not exposed to the user.
2023-12-26 12:00:53 +01:00
Reto Brunner edb1226b47 sqlite: add msg type index to speed up cleaner 2023-12-24 16:55:45 +01:00
Reto Brunner b0ca8e51fb wire up storage cleaner upon server start 2023-12-24 16:55:45 +01:00
Reto Brunner 21b1152f53 cleaner: expose cli task to do cleaning + vacuum
Make the cleaner available to users by exposing it as a subcommand
to thelounge storage.

This is recommended to be run whenever the storage policy significantly
changes in a way that makes many messages eligible for deletion.
The cleaner would cope, but it'll be inefficient and can take many hours.
Due to how storage works in sqlite, the space would not actually be
given back to the OS, just marked for future writes.
Hence this also runs a vacuum to compact the DB as much as it can.
2023-12-24 16:55:45 +01:00
Reto Brunner 74aff7ee5a introduce storage cleaner
Once this is getting hooked up, it'll periodically delete old
messages.

The StoragePolicy can be chosen by the user, currently there's
two versions, delete everything based on age is the obvious.

The other is for the data hoarders among us. It'll only delete
message types which can be considered low value... Types with
a time aspect like away / back... joins / parts etc.

It tries to do that in a sensible way, so that we don't block
all other db writers that are ongoing.
The "periodically" interval is by design not exposed to the user.
2023-12-24 16:55:45 +01:00
Reto Brunner 14d9ff247d sqlite: implement deleteMessages
This is laying the foundation to build a cleaning task that's
sort of database agnostic.
All calls are done by acting on a "DeletionRequest" so interpretation
of the config will go through a single point
2023-12-23 21:08:07 +01:00
Reto Brunner aec8d0b033 sqlite: accept db connection string
This allows us to inject a memory db during testing
2023-12-23 21:08:07 +01:00
Reto Brunner 60ddf17124 sqlite: use variadic function for serialize_run
This makes the usage of the function a bit nicer
2023-12-23 21:08:07 +01:00
dependabot[bot] 20227b174c
build(deps): bump @babel/traverse from 7.18.9 to 7.23.6
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.18.9 to 7.23.6.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.6/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-23 10:07:23 +00:00
Reto Brunner d18182da8b Merge 'renovate/cheerio-0.x' 2023-12-20 07:40:09 +01:00
Reto Brunner ea35040b42 Merge renovate/bcryptjs-2.x 2023-12-20 07:38:59 +01:00
Reto Brunner 97f553eea8 cli: don't fail if stderr is not in json format
A user reported in the IRC chan that installing packages fails with

```
2023-12-13 20:02:34 [INFO] Installing thelounge-theme-solarized v1.1.9...
undefined:1
(node:3329) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
^
SyntaxError: Unexpected token '(', "(node:3329"... is not valid JSON
```

Now, this happens as yarn helpfully prints a deprecation warning
that is shown in the stack trace.

Let's assume that we may get non json messages and log them at debug, as we
don't know their severity.
2023-12-13 22:55:53 +01:00
Zach Bloomquist 6603c1a6e6
scripts: fix generate-config-doc, handle usage errors 2023-12-12 11:52:45 -05:00
renovate[bot] 73a529acea
chore(deps): update dependency @types/cheerio to v0.22.35 2023-12-01 01:11:42 +00:00
renovate[bot] 2f40d9dbcc
chore(deps): update dependency @types/bcryptjs to v2.4.6 2023-12-01 01:11:29 +00:00
Reto Brunner d1561f8ebc sqlite: return new version in downgrade()
We want to give the caller the current version, not the last
version we rolled back, fix that
2023-11-06 07:52:59 +01:00
Reto Brunner ec75ff00cb sqlite: don't modify global array during tests 2023-11-06 07:52:59 +01:00
Reto Brunner 884a92c74b sqlite: fix typo fetch_rollbacks 2023-11-06 07:52:59 +01:00
Reto Brunner 77b64c546b dont' crash on rDNS failure 2023-11-06 07:36:52 +01:00
Reto Brunner cc59e6b578 Merge renovate/is-utf8-0.x 2023-11-04 12:22:05 +01:00
Reto Brunner fb1d79f5fa Merge renovate/bcryptjs-2.x 2023-11-04 12:21:46 +01:00
Reto Brunner 100ff3c198 Merge renovate/actions-setup-node-4.x 2023-11-04 12:19:04 +01:00
Reto Brunner d893feff1c Merge renovate/mousetrap-1.x 2023-11-04 12:18:47 +01:00
Reto Brunner 88a5fef4ea Merge renovate/lodash-4.x 2023-11-04 12:17:54 +01:00
Reto Brunner 5b64ecbe68 Merge renovate/content-disposition-0.x 2023-11-04 12:11:33 +01:00
Reto Brunner 5024acd7dc Merge renovate/cheerio-0.x 2023-11-04 12:10:14 +01:00
Reto Brunner bbfada251c Merge renovate/npm-postcss-vulnerability 2023-11-04 12:09:49 +01:00
Reto Brunner 8cec292f2c Merge dependabot/npm_and_yarn/get-func-name-2.0.2 2023-11-04 12:08:42 +01:00
Reto Brunner 22ae594cc3 bump caniuse-lite 2023-11-04 12:04:51 +01:00
Reto Brunner 1c6bec2323 Merge branch 'cliMigrations' 2023-11-04 11:59:41 +01:00
Reto Brunner 9105fbc23a Merge branch 'emoji' 2023-11-04 11:53:30 +01:00
Reto Brunner 8c54cd50d8 don't crash on rDNS failure
Node apparently throws even on valid ipv6 input in certain environments,
probably due to the DNS server returning SERVFAIL.
Guard against it and fallback with the plain IP

Fixes: https://github.com/thelounge/thelounge/issues/4768
2023-11-04 11:45:11 +01:00
renovate[bot] 59de6afd3f
chore(deps): update dependency @types/is-utf8 to v0.2.2 2023-11-01 00:33:46 +00:00
renovate[bot] b506966b08
chore(deps): update dependency @types/bcryptjs to v2.4.5 2023-11-01 00:33:33 +00:00
renovate[bot] 785ec0a0e2
chore(deps): update actions/setup-node action to v4 2023-10-24 16:50:41 +00:00
renovate[bot] 250433c875
chore(deps): update dependency @types/mousetrap to v1.6.13 2023-10-19 10:52:51 +00:00
renovate[bot] d4d5a8e386
chore(deps): update dependency @types/lodash to v4.14.200 2023-10-19 08:04:02 +00:00
renovate[bot] bcca111a4d
chore(deps): update dependency @types/content-disposition to v0.5.7 2023-10-19 02:00:59 +00:00
renovate[bot] b686059c6b
chore(deps): update dependency @types/cheerio to v0.22.33 2023-10-19 02:00:46 +00:00
renovate[bot] ff77a33663
chore(deps): update dependency postcss to v8.4.31 [security] 2023-10-08 01:28:22 +00:00
dependabot[bot] d308e74183
build(deps): bump get-func-name from 2.0.0 to 2.0.2
Bumps [get-func-name](https://github.com/chaijs/get-func-name) from 2.0.0 to 2.0.2.
- [Release notes](https://github.com/chaijs/get-func-name/releases)
- [Commits](https://github.com/chaijs/get-func-name/commits/v2.0.2)

---
updated-dependencies:
- dependency-name: get-func-name
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-28 05:21:42 +00:00
Reto Brunner f999db99c7 Merge branch 'renovate/linkify-it-3.x' 2023-09-18 21:03:10 +02:00
Reto Brunner 76c896aea2 Merge branch 'renovate/bcryptjs-2.x' 2023-09-18 20:22:50 +02:00
dependabot[bot] 08413c7b6b
build(deps): bump word-wrap from 1.2.3 to 1.2.5
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.5.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.5)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-18 18:13:29 +00:00
renovate[bot] 48301b1ca3
chore(deps): update dependency @types/bcryptjs to v2.4.4 2023-09-16 19:28:31 +00:00
Reto Brunner 03795a2718 router: don't use next() in router guards
Vue wants to get rid of the next call.
https://router.vuejs.org/guide/advanced/navigation-guards.html#Optional-third-argument-next

For one of the router guards, it's easy enough to do so let's do
that.
2023-09-16 13:17:06 +02:00
renovate[bot] 2985727996
chore(deps): update dependency @types/linkify-it to v3.0.3 2023-09-01 01:55:02 +00:00
Max Leiter 9f05a75c39
Merge pull request #4770 from thelounge/smallTScleanup 2023-07-31 11:04:08 -07:00
Reto Brunner c0b38d4762 store: use return type over a type cast 2023-07-31 10:50:48 +02:00
Reto Brunner 2878f87879 Respect bind setting for all outgoing requests
So far the bind config only impacted the IRC connections.
However, nothing in our doc comment says that this is intentional.

bind
Set the local IP to bind to for outgoing connections.

This commit fixes the leak and uses it for all outgoing requests
as described by the docstring.
2023-07-16 12:02:22 +02:00
dependabot[bot] 447a237fc6
build(deps): bump semver from 7.3.5 to 7.5.2
Bumps [semver](https://github.com/npm/node-semver) from 7.3.5 to 7.5.2.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.3.5...v7.5.2)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-16 09:56:00 +00:00
Reto Brunner 430a865e9f update dependency postcss to v8.4.26 2023-07-16 11:52:16 +02:00
Reto Brunner 816b7686e3 update dependency @types/chai to v4.3.5 2023-07-16 11:50:19 +02:00
Reto Brunner 2e019a2fdb update dependency @types/lodash to v4.14.195 2023-07-16 11:49:15 +02:00
Reto Brunner 4f9ca3e192 update dependency @vue/test-utils to v2.4.0 2023-07-16 11:45:47 +02:00
Reto Brunner 57c4d5513c update dependency webpack-hot-middleware to v2.25.4 2023-07-16 11:42:58 +02:00
Reto Brunner 3e21bfcbea update dependency cheerio to v1.0.0-rc.12 2023-07-16 11:35:49 +02:00
Reto Brunner 607b9fc96a update emoji 2023-07-15 10:41:43 +02:00
Reto Brunner 1a1153aed6 use shebang for generate-emoji script 2023-07-15 10:40:44 +02:00
renovate[bot] 54ff563247
chore(deps): update dependency postcss to v8.4.26 2023-07-14 20:02:42 +00:00
Reto Brunner ed0a47fe2c bump emoji-regex to latest
Fixes: https://github.com/thelounge/thelounge/issues/4761
2023-07-12 08:24:58 +02:00
Reto Brunner 3af4ad1076 Respect bind setting for all outgoing requests
So far the bind config only impacted the IRC connections.
However, nothing in our doc comment says that this is intentional.

> ### bind
> Set the local IP to bind to for outgoing connections.

This commit fixes the leak and uses it for all outgoing requests
as described by the docstring.
2023-06-25 19:30:52 +02:00
Reto Brunner 79fae26f39 test/storage: use helper for url creation
We keep repeating ourselves, let's move that into a helper instead.
In order to get a sane host, we fix the listener to 127.0.0.1
else we get the unspecified :: ipv6 addr (on suitable hosts),
which isn't useful.
2023-06-25 19:30:52 +02:00
Reto Brunner c6b1913b91 test/link: use helper for url creation
We keep repeating ourselves, let's move that into a helper instead.
In order to get a sane host, we fix the listener to 127.0.0.1
else we get the unspecified :: ipv6 addr (on suitable hosts),
which isn't useful.
2023-06-25 19:08:23 +02:00
Reto Brunner 071ad96d9b Merge branch 'signin' 2023-06-25 10:15:21 +02:00
Reto Brunner 2ef8b37009 sqlite: add migrations support and introduce primary key
Add the ability to migrate our db in the upwards direction.
Use the facility to add primary keys to our messages table.
This should allow work like jumping to messages and the likes.

This also introduces the framework for rollback, without actually
hooking it up.
This should be easy enough to do when the need arises.
2023-06-24 14:50:39 +02:00
Reto Brunner 8aa5e33b1d Fix semver for prerelease versions #4744 2023-06-24 14:35:41 +02:00
Reto Brunner 43a2b397a2 Add comments explaining behavior when echo-message is not available 2023-06-24 14:32:33 +02:00
Reto Brunner c43a47afc1 Merge branch 'applePush' 2023-06-24 14:31:23 +02:00
Reto Brunner 14575c94cf Merge branch 'frameworkInternals' 2023-06-24 14:28:48 +02:00
renovate[bot] 303f53fe72
chore(deps): update dependency @vue/test-utils to v2.4.0 2023-06-23 00:52:37 +00:00
renovate[bot] 06f1387f7b
chore(deps): update dependency webpack-hot-middleware to v2.25.4 2023-06-21 23:04:16 +00:00
Reto Brunner c5326e8795 Sign in: use v-model
There's no need to mess with DOM elements, we can use the normal
v-model approach for both username and password
2023-06-18 15:20:08 +02:00
Reto Brunner 355c5d6fa4 v4.4.1 2023-06-13 08:24:37 +02:00
Kufat 7ac2a6fd77 Fix semver for prerelease versions
Noticed this breakage while trying to install a plugin on 4.4.1-rc2.

```
> semver.default.satisfies("4.4.1-rc2", ">=4.3.0")
false
> semver.default.satisfies("4.4.1-rc2", ">=4.3.0", {includePrerelease: true})
true
```
2023-06-01 08:03:07 -04:00
renovate[bot] c4879fdbba
fix(deps): update dependency cheerio to v1.0.0-rc.12 2023-06-01 02:09:05 +00:00
Val Lorentz 4255c1cdec Add comments explaining behavior when echo-message is not available 2023-05-30 22:09:39 +02:00
Reto Brunner ae9d312b2a v4.4.1-rc.2 2023-05-27 08:51:42 +02:00
renovate[bot] f7c6ba5eb1
chore(deps): update dependency @types/lodash to v4.14.195 2023-05-26 23:27:55 +00:00
Reto Brunner 4d60d9c282 bump socket.io-parser from 4.2.1 to 4.2.3 2023-05-25 07:36:54 +02:00
dependabot[bot] af49ef21ea
build(deps): bump socket.io-parser from 4.2.1 to 4.2.3
Bumps [socket.io-parser](https://github.com/socketio/socket.io-parser) from 4.2.1 to 4.2.3.
- [Release notes](https://github.com/socketio/socket.io-parser/releases)
- [Changelog](https://github.com/socketio/socket.io-parser/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io-parser/compare/4.2.1...4.2.3)

---
updated-dependencies:
- dependency-name: socket.io-parser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-25 05:33:59 +00:00
Reto Brunner 7a9ddc01e1 settings: make missing_field msg descriptive
The "missing_fields" error triggers on any missing field (duh).
"Please enter a new password" is not a sensible string for that.
2023-05-23 08:24:20 +02:00
Reto Brunner 8f08cf3d0b client: fix password change input
The TS rewrite dropped the form that was expected to be passed
as props.
That lead to the password change being borked, as the fields
were always set to "null".
We don't need a form, can just use refs here.
2023-05-23 08:18:20 +02:00
Reto Brunner e05871fd2f 4.4.1-rc.1
Co-Authored-By: Max Leiter <maxwell.leiter@gmail.com>
2023-05-21 14:42:30 +02:00
Reto Brunner ede48ab034 Merge branch 'previewFix' 2023-05-20 10:10:50 +02:00
Reto Brunner 4c6fa550aa Merge branch 'nodeEOL' 2023-05-20 10:09:48 +02:00
Reto Brunner 9388960497 linkPreview: Pass channel prop
Else the update logic doesn't work and we don't show
the next / prev buttons
2023-05-15 09:38:09 +02:00
Reto Brunner 7bce779254 Remove unused code 2023-05-15 09:37:40 +02:00
Reto Brunner a7b85db990 v4.4.0 2023-05-13 13:21:12 +02:00
Reto Brunner f4ef11de3f v4.4.0-rc.1 2023-05-06 10:39:34 +02:00
Reto Brunner 3a63484762 Merge branch 'changelog' 2023-05-06 10:36:58 +02:00
Reto Brunner 04b2bf036b bump socket.io to 4.6.1 2023-05-04 23:39:07 +02:00
Max Leiter 3066f48a69
Merge pull request #4728 from thelounge/settingsFormProp 2023-05-01 11:43:07 -07:00
renovate[bot] ed40c83a2b
Update dependency @types/chai to v4.3.5 2023-05-01 02:39:13 +00:00
Reto Brunner f21f665384 Add changelog for 4.4.0
Co-Authored-by: Max Leiter <maxwell.leiter@gmail.com>
2023-04-29 13:52:18 +02:00
Reto Brunner 12d9ef34f0 Client/Settings: Remove bogus settings-form prop
As is this has no effect, other than looking weird in html:
<div settings-form="[object HTMLFormElement]"><div><h2>Native app</h2>
2023-04-29 11:52:34 +02:00
Reto Brunner 9ee1cf13a8 Publish to npm with provenance 2023-04-25 22:36:42 +02:00
Lenore ba1a4206a6 fix motd display to match settings 2023-04-25 00:49:57 +02:00
Reto Brunner 59cf29ef4a workflows: bump Windows + Mac node versions to 18 2023-04-23 21:48:39 +02:00
Reto Brunner 8e43d8083d node: remove v14, add v20 to build matrix
EOL date reached.
2023-04-23 21:46:53 +02:00
Pavel Djundik 3cd0a75ac2 Publish to npm with provenance
Ref: https://github.blog/changelog/2023-04-19-npm-provenance-public-beta/
2023-04-23 12:11:08 +03:00
Reto Brunner 21d1dbaad6 Unbreak nick colors for existing themes
https://github.com/thelounge/thelounge/pull/4649 broke existing
themes by removing the colored-nicks class from chat.

Considering that we don't bump the major version, keep backwards
compatibility for now
2023-04-17 01:11:35 +02:00
Reto Brunner 90ad06a29a Fix load of channels from user config
While the commits that caused the problem have been reverted,
this still adds test cases to it and make the loading more robust.
2023-04-08 13:46:13 +02:00
Val Lorentz 0c7cc85184 Fix load of channels from user config
Network.export() only writes the "type" key if it's a ChanType.QUERY;
so the config on disk has no "type".

This causes it to be undefined when loading, which breaks various other
checks, and then drops it the next time the config is saved.
2023-04-08 12:38:06 +02:00
Reto Brunner 3be805bd38 sqlite: Add rollback support
This enables db migrations to be undone, or "down migrated".
The down migration shouldn't be done automatically
as it could lead to severe data loss if that were done.
Hence, we still hard fail if we encounter a version lower than what
we have in the DB.

A CLI will be added in a later commit that allows users to explicitly
do that.
2023-03-31 11:34:26 +02:00
Reto Brunner e25c296901 push: remove iOS warning
iOS 16.4 introduced webpush, we can get rid of the special case
in our settings panel.
2023-03-28 10:00:56 +02:00
Max Leiter 4babd17383
Merge pull request #4715 from progval/test-server-teardown
tests/server: Tear down test fixtures in the order they were setup
2023-03-20 13:54:06 -07:00
Max Leiter b408843ff1
Merge pull request #4717 from thelounge/changelog
Inline logger into changelog script
2023-03-20 13:53:41 -07:00
Reto Brunner 0f3487c533 Inline logger into changelog script
We can't really import easily from our build without it being
brittle. TL isn't meant to be used as a library.

Instead, just inline the logger as it is trivial enough.
2023-03-19 23:49:42 +01:00
Val Lorentz edc6f77c64
add setup 2023-03-18 07:50:35 +01:00
Val Lorentz 0dd74a93bf tests/server: Tear down test fixtures in the order they were setup
if for whatever reason before() fails to import the server, it causes after()
to fail on the first line, so it doesn't restore stubs; causing other errors
to be printed in other tests ("TypeError: Attempted to wrap warn which is
already wrapped")
2023-03-17 17:12:29 +01:00
Reto Brunner 7f6059d5b7 input/raw: use the irc-framework api
We are not allowed to mess with the connection object directly
according to the public api surface of the framework
2023-03-04 18:17:17 +01:00
Reto Brunner 8ca9ee873b use the irc connected helper function
We should not mess with irc-framework internals.
Technically we shouldn't even access the connection object,
it's not part of the documented API surface
2023-03-04 18:16:28 +01:00
Reto Brunner dfed1dd757 skip migrations if the user has disabled logging 2023-02-27 14:33:34 +01:00
Reto Brunner d67277d996 clientManager: Expose user config 2023-02-27 14:33:34 +01:00
Reto Brunner 95aaba43fa cli: Implement storage migrate subcommand
This introduces the ability to run the migration offline, while
TL is not running as the migrations can take a long time.

The migrate command is added as a `thelounge storage` subcommand.
Reason being that it is expected that more subcommands will follow,
say `thelounge storage clean` to remove partial data from the db.
2023-02-27 14:33:34 +01:00
Reto Brunner 3e7255ff20 sqlite: Add primary keys to the messages table
We want primary keys to never get re-used to so that we
can implement jump to messages / context fetching etc
in the future.

This isn't hooked up yet at all to the rest of the code, only
the schema is changed
2023-02-27 14:20:31 +01:00
Reto Brunner 86e376fc03 sqlite: run migrations on startup 2023-02-27 14:20:31 +01:00
Reto Brunner 899762cddd sqlite: Add infrastructure for migration tests
This sets up the testing infrastructure to test migrations we are
doing.
It's done on a in memory database directly, we are only interested
in the statements themselves and it's easier than to try and
inject a prepared db into the store.

We do add some dummy data though to make sure we actually execute
the things as we expect.
2023-02-27 14:20:29 +01:00
Reto Brunner 063aca948c sqlite: don't hardcode version test 2023-02-27 14:17:04 +01:00
Reto Brunner 25642fbe98 sqlite: delete table creation test
This just repeats the hard coded values from the code, which
is not helping.
We need to touch that test whenever we modify the sql which is
undesired and it doesn't test any useful functionality.

Any error that may ensue would hopefully be tracked by the other
test.
2023-02-27 14:11:47 +01:00
152 changed files with 4508 additions and 2395 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

@ -1,5 +1,8 @@
name: Build
permissions:
contents: read
on: [push, pull_request]
jobs:
@ -9,21 +12,19 @@ jobs:
strategy:
matrix:
include:
# EOL: April 2023
- os: ubuntu-latest
node_version: 14.x
# EOL: April 2024
- os: ubuntu-latest
node_version: 16.x
- os: macOS-latest
node_version: 16.x
- os: windows-latest
node_version: 16.x
# EOL: April 2025
- os: macOS-latest
node_version: 18.x
- os: windows-latest
node_version: 18.x
- os: ubuntu-latest
node_version: 18.x
# EOL: April 2026
- os: ubuntu-latest
node_version: 20.x
# EOL: April June 2024
- os: ubuntu-latest
node_version: 21.x
runs-on: ${{ matrix.os }}
@ -31,7 +32,7 @@ jobs:
- uses: actions/checkout@master
- name: Setup Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node_version }}

View file

@ -1,5 +1,9 @@
name: Release
permissions:
contents: read
id-token: write
on:
push:
tags: v*
@ -14,8 +18,9 @@ jobs:
- uses: actions/checkout@master
- name: Setup Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: "latest"
registry-url: "https://registry.npmjs.org/"
- name: Install
@ -31,13 +36,13 @@ jobs:
- name: Publish latest
if: "!contains(github.ref, '-')"
run: npm publish --tag latest
run: npm publish --tag latest --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
- name: Publish next
if: contains(github.ref, '-')
run: npm publish --tag next
run: npm publish --tag next --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}

View file

@ -4,6 +4,386 @@ All notable changes to this project will be documented in this file.
<!-- New entries go after this line -->
## 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
and running a database `VACUUM` will use up ~2x the current DB disc space for a short period.
If you enable the storagePolicy, stop the running instance and run `thelounge storage clean`.
This will force a full cleanup once, rather than doing so incrementally and will release all the
disc space back to the OS.
As usual, we follow the Node.js release schedule, so the minimum Node.js version required is now 18.
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.3)
### Added
- Sign in: use v-model ([`c5326e8`](https://github.com/thelounge/thelounge/commit/c5326e87958b1e99ca9405da5c8d17e3f45c983c) by [@brunnre8](https://github.com/brunnre8))
- Add comments explaining behavior when echo-message is not available ([`43a2b39`](https://github.com/thelounge/thelounge/commit/43a2b397a2efc65c7214893846831376bb880138) by [@brunnre8](https://github.com/brunnre8))
- Fix semver for prerelease versions #4744 ([`8aa5e33`](https://github.com/thelounge/thelounge/commit/8aa5e33b1d9e0a56e51481c227bf7d61fdd7b21f) by [@brunnre8](https://github.com/brunnre8))
- sqlite: add migrations support and introduce primary key ([`2ef8b37`](https://github.com/thelounge/thelounge/commit/2ef8b3700945deb9a113ddf4e3010ad36556deef) by [@brunnre8](https://github.com/brunnre8))
- test/link: use helper for url creation ([`c6b1913`](https://github.com/thelounge/thelounge/commit/c6b1913b919421ab2b70093218422a390d822c75) by [@brunnre8](https://github.com/brunnre8))
- test/storage: use helper for url creation ([`79fae26`](https://github.com/thelounge/thelounge/commit/79fae26f396081b6f557ae7b4f0c8fd4649b6a74) by [@brunnre8](https://github.com/brunnre8))
- Respect bind setting for all outgoing requests ([`3af4ad1`](https://github.com/thelounge/thelounge/commit/3af4ad1076330428da41f4205bb069d714b2a4e2) by [@brunnre8](https://github.com/brunnre8))
- bump emoji-regex to latest ([`ed0a47f`](https://github.com/thelounge/thelounge/commit/ed0a47fe2c10a2512832c9365a863967f9fc1ee0) by [@brunnre8](https://github.com/brunnre8))
- use shebang for generate-emoji script ([`1a1153a`](https://github.com/thelounge/thelounge/commit/1a1153aed638de0e5e2ca4089cb7656bbfa4394a) by [@brunnre8](https://github.com/brunnre8))
- Respect bind setting for all outgoing requests ([`2878f87`](https://github.com/thelounge/thelounge/commit/2878f87879cab30eabedbe2376507dae33295f22) by [@brunnre8](https://github.com/brunnre8))
- store: use return type over a type cast ([#4770](https://github.com/thelounge/thelounge/pull/4770) by [@brunnre8](https://github.com/brunnre8))
- don't crash on rDNS failure ([`8c54cd5`](https://github.com/thelounge/thelounge/commit/8c54cd50d8431481a70dec26a66a5343f2bbbd2c) by [@brunnre8](https://github.com/brunnre8))
- sqlite: fix typo fetch_rollbacks ([`884a92c`](https://github.com/thelounge/thelounge/commit/884a92c74bb669ff9a94c5a1c164912a9bd9891b) by [@brunnre8](https://github.com/brunnre8))
- sqlite: don't modify global array during tests ([`ec75ff0`](https://github.com/thelounge/thelounge/commit/ec75ff00cb8fdcef1857749ce6d033860e1ca157) by [@brunnre8](https://github.com/brunnre8))
- sqlite: return new version in downgrade() ([`d1561f8`](https://github.com/thelounge/thelounge/commit/d1561f8ebccacd0277d185626f3737bfd23bc99e) by [@brunnre8](https://github.com/brunnre8))
- cli: don't fail if stderr is not in json format ([`97f553e`](https://github.com/thelounge/thelounge/commit/97f553eea8ed4a57f6d760a767425159f6451e08) by [@brunnre8](https://github.com/brunnre8))
- sqlite: use variadic function for serialize_run ([`60ddf17`](https://github.com/thelounge/thelounge/commit/60ddf17124af8e451412b14a11910ded894979d8) by [@brunnre8](https://github.com/brunnre8))
- sqlite: accept db connection string ([`aec8d0b`](https://github.com/thelounge/thelounge/commit/aec8d0b03341691a0211d172538afc61560a919c) by [@brunnre8](https://github.com/brunnre8))
- sqlite: implement deleteMessages ([`14d9ff2`](https://github.com/thelounge/thelounge/commit/14d9ff247d51e77640bc0f37464804eadc822dd7) by [@brunnre8](https://github.com/brunnre8))
- introduce storage cleaner ([`74aff7e`](https://github.com/thelounge/thelounge/commit/74aff7ee5a9440a653859879390191031f81153e) by [@brunnre8](https://github.com/brunnre8))
- cleaner: expose cli task to do cleaning + vacuum ([`21b1152`](https://github.com/thelounge/thelounge/commit/21b1152f5357f47586456949cadfb9876a0613da) by [@brunnre8](https://github.com/brunnre8))
- wire up storage cleaner upon server start ([`b0ca8e5`](https://github.com/thelounge/thelounge/commit/b0ca8e51fb21b23859f95406f41dfe1ce273f419) by [@brunnre8](https://github.com/brunnre8))
- sqlite: add msg type index to speed up cleaner ([`edb1226`](https://github.com/thelounge/thelounge/commit/edb1226b474e9dc74d096201220d8e675821ac21) by [@brunnre8](https://github.com/brunnre8))
- add storage cleaner ([`7f0b721`](https://github.com/thelounge/thelounge/commit/7f0b7217906abf90343f5b91dc7ceaa650dd058f) by [@brunnre8](https://github.com/brunnre8))
- scripts: fix generate-config-doc, handle usage errors ([#4807](https://github.com/thelounge/thelounge/pull/4807) by [@flotwig](https://github.com/flotwig))
- router: don't use next() in router guards ([#4783](https://github.com/thelounge/thelounge/pull/4783) by [@brunnre8](https://github.com/brunnre8))
- linkify: Add web+ schema support ([`ae6bae6`](https://github.com/thelounge/thelounge/commit/ae6bae69ac2c915c3dcac4262168da46f8eddf39) by [@SoniEx2](https://github.com/SoniEx2))
- linkify: simplify noscheme detection logic ([`dd24cb1`](https://github.com/thelounge/thelounge/commit/dd24cb13002b76ba0a67abfa11faedaa455df828) by [@brunnre8](https://github.com/brunnre8))
- Add shortcut to navigate between channels with undread msgs ([`daabb76`](https://github.com/thelounge/thelounge/commit/daabb7678172fc6b6d7c6eebc6fad40b6f84ea39) by [@Nachtalb](https://github.com/Nachtalb))
- Remove husky, add githooks-install ([#4826](https://github.com/thelounge/thelounge/pull/4826) by [@brunnre8](https://github.com/brunnre8))
- Testing setup ([#4825](https://github.com/thelounge/thelounge/pull/4825) by [@brunnre8](https://github.com/brunnre8))
- Remove Node.js 16 from package.json and testing matrix ([`113e9bd`](https://github.com/thelounge/thelounge/commit/113e9bd2fb9a5154c048234d8ebbd8c0a61070d1) by [@MaxLeiter](https://github.com/MaxLeiter))
- server: remove version from CTCP response ([`45563d9`](https://github.com/thelounge/thelounge/commit/45563d9a5938ae4fa46da8a2d6c51fc829ebb910) by [@flotwig](https://github.com/flotwig))
### Documentation
On the [website repository](https://github.com/thelounge/thelounge.github.io):
- Merge branch 'localInstall' ([`8c0d5a5`](https://github.com/thelounge/thelounge.github.io/commit/8c0d5a58075fc1035f5c71675847823751e1f98d) by [@brunnre8](https://github.com/brunnre8))
- docs: update docker image to point to the new ghcr.io repository ([`5d7c993`](https://github.com/thelounge/thelounge.github.io/commit/5d7c993b9e26050b482550cb3f16aa11e0b99d9e) by [@brunnre8](https://github.com/brunnre8))
- Add "Hide all chat messages containing a link in a specific channel" … ([`993cf8b`](https://github.com/thelounge/thelounge.github.io/commit/993cf8b00e35ffeff1c20d122defc32d09e236b3) by [@zDEFz](https://github.com/zDEFz))
- ctcp: remove stale link to code (#273) ([`379c34d`](https://github.com/thelounge/thelounge.github.io/commit/379c34d88aa73dd86078af7757a4536bb9958e02) by [@brunnre8](https://github.com/brunnre8))
- docs: sync config.js.md (add prefetchTimeout, update ldap) (#275) ([`51dfc80`](https://github.com/thelounge/thelounge.github.io/commit/51dfc803415946e985c36317ea362ba625c67a3c) by [@flotwig](https://github.com/flotwig))
- Removing #thelounge-scandinavia due to inactivity (#278) ([`403cc6a`](https://github.com/thelounge/thelounge.github.io/commit/403cc6aa05cd30a0f9a86b81369ec0c9f1ffd24f) by [@fnutt](https://github.com/fnutt))
- Nodejs documentation link update (#277) ([`06e4725`](https://github.com/thelounge/thelounge.github.io/commit/06e47254cc6b98eabe4d527b1ce6be6f7ea7b9eb) by [@xfisbest](https://github.com/xfisbest))
- Add installation instructions for Gentoo (#276) ([`52be432`](https://github.com/thelounge/thelounge.github.io/commit/52be432b36cabc7a9d393a07e7702e3aebff8075) by [@rahilarious](https://github.com/rahilarious))
### Dependency updates
- chore(deps): update dependency webpack-hot-middleware to v2.25.4 ([`06f1387`](https://github.com/thelounge/thelounge/commit/06f1387f7b5ff374b52bc4aeac06d6e936bc00f4) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @vue/test-utils to v2.4.0 ([`303f53f`](https://github.com/thelounge/thelounge/commit/303f53fe72a6cde53410821b2d59c81db90d308a) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency postcss to v8.4.26 ([`54ff563`](https://github.com/thelounge/thelounge/commit/54ff56324714bd5c6221250d02491f20b7ede6df) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @types/linkify-it to v3.0.3 ([`2985727`](https://github.com/thelounge/thelounge/commit/2985727996c1e84fefce06e5c2a0da02a8b6ccb6) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @types/bcryptjs to v2.4.4 ([`48301b1`](https://github.com/thelounge/thelounge/commit/48301b1ca31f0eb145695f320c81d0047e6883e6) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- build(deps): bump word-wrap from 1.2.3 to 1.2.5 ([`08413c7`](https://github.com/thelounge/thelounge/commit/08413c7b6b78f460bdee31239a87e6f86e14dda2) by [@dependabot[bot]](https://github.com/dependabot%5Bbot%5D))
- chore(deps): update dependency postcss to v8.4.31 [security] ([`ff77a33`](https://github.com/thelounge/thelounge/commit/ff77a3366305c23180e6e509f5f39d285edca8d1) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @types/cheerio to v0.22.33 ([`b686059`](https://github.com/thelounge/thelounge/commit/b686059c6bf2f2014497d7dceb093422c5fb8fc2) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @types/content-disposition to v0.5.7 ([`bcca111`](https://github.com/thelounge/thelounge/commit/bcca111a4dd42e8b648acee1da9548a0c677d056) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @types/lodash to v4.14.200 ([`d4d5a8e`](https://github.com/thelounge/thelounge/commit/d4d5a8e386df60c69826fb9b1c63c138a1503640) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @types/mousetrap to v1.6.13 ([`250433c`](https://github.com/thelounge/thelounge/commit/250433c87549b59f34cd4d3933364a3766cf587e) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update actions/setup-node action to v4 ([`785ec0a`](https://github.com/thelounge/thelounge/commit/785ec0a0e26f2233ddea6f51ef16cd5cc5e14e40) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @types/bcryptjs to v2.4.5 ([`b506966`](https://github.com/thelounge/thelounge/commit/b506966b08fba11ab9b8b88268c9371dac78c314) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @types/is-utf8 to v0.2.2 ([`59de6af`](https://github.com/thelounge/thelounge/commit/59de6afd3fdbeb894e8cf39321c786220bbcf66b) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @types/bcryptjs to v2.4.6 ([`2f40d9d`](https://github.com/thelounge/thelounge/commit/2f40d9dbcca6fff43f1a66a2e0efb826e22cd4b4) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @types/cheerio to v0.22.35 ([`73a529a`](https://github.com/thelounge/thelounge/commit/73a529acea765705c1903762106d8f8f3221e6fc) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @types/content-disposition to v0.5.8 ([`aa95032`](https://github.com/thelounge/thelounge/commit/aa95032760761cc7e28d802ed9bec93d4a807335) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @types/is-utf8 to v0.2.3 ([`eaa70ca`](https://github.com/thelounge/thelounge/commit/eaa70caad7e578af4bf5f1603c5008b9159a04e6) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @types/linkify-it to v3.0.5 ([`1d2fdd9`](https://github.com/thelounge/thelounge/commit/1d2fdd95b0ee698bbdc85eb70fd02f47d46e86da) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @types/lodash to v4.14.202 ([`fe50a90`](https://github.com/thelounge/thelounge/commit/fe50a9023509412b8c6d981053b469e27b5a49c0) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @types/mousetrap to v1.6.15 ([`a77fbb8`](https://github.com/thelounge/thelounge/commit/a77fbb894ff550cabf7d6f54e06296babdeb2b67) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @types/node to v17.0.45 ([`e2fda1f`](https://github.com/thelounge/thelounge/commit/e2fda1fb84da9cdbb445d6ebfe0f9795cb83633d) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- build(deps): bump semver from 7.3.5 to 7.5.2 ([`447a237`](https://github.com/thelounge/thelounge/commit/447a237fc6d54e59e563e982a406e16011c57b7a) by [@dependabot[bot]](https://github.com/dependabot%5Bbot%5D))
- build(deps): bump get-func-name from 2.0.0 to 2.0.2 ([`d308e74`](https://github.com/thelounge/thelounge/commit/d308e7418367e880f1b5454ade8267f5996bd035) by [@dependabot[bot]](https://github.com/dependabot%5Bbot%5D))
- build(deps): bump @babel/traverse from 7.18.9 to 7.23.6 ([`20227b1`](https://github.com/thelounge/thelounge/commit/20227b174c4bf375af1168c60ef57e6124c199f4) by [@dependabot[bot]](https://github.com/dependabot%5Bbot%5D))
- update emoji ([`607b9fc`](https://github.com/thelounge/thelounge/commit/607b9fc96a9ca933154dcc082fb2bb6dd545a2db) by [@brunnre8](https://github.com/brunnre8))
- update dependency cheerio to v1.0.0-rc.12 ([`3e21bfc`](https://github.com/thelounge/thelounge/commit/3e21bfcbea579c08f0c02d692e59242653b553b3) by [@brunnre8](https://github.com/brunnre8))
- update dependency webpack-hot-middleware to v2.25.4 ([`57c4d55`](https://github.com/thelounge/thelounge/commit/57c4d5513cfe6f0770a89330932dc07623c35e26) by [@brunnre8](https://github.com/brunnre8))
- update dependency @vue/test-utils to v2.4.0 ([`4f9ca3e`](https://github.com/thelounge/thelounge/commit/4f9ca3e1923837f2886a58df4605255229b200b2) by [@brunnre8](https://github.com/brunnre8))
- update dependency @types/lodash to v4.14.195 ([`2e019a2`](https://github.com/thelounge/thelounge/commit/2e019a2fdba684ad4cef15f55e514ae7a1bc8edf) by [@brunnre8](https://github.com/brunnre8))
- update dependency @types/chai to v4.3.5 ([`816b768`](https://github.com/thelounge/thelounge/commit/816b7686e36aaac36371a5bfbcd2648443bc4e48) by [@brunnre8](https://github.com/brunnre8))
- update dependency postcss to v8.4.26 ([`430a865`](https://github.com/thelounge/thelounge/commit/430a865e9fd7218ac8b0deaa6fc0841341b823ab) by [@brunnre8](https://github.com/brunnre8))
- update @types/mousetrap ([`139ce47`](https://github.com/thelounge/thelounge/commit/139ce47b73a4907da0e2737dbb245bc686330ec1) by [@brunnre8](https://github.com/brunnre8))
- bump caniuse-lite ([`22ae594`](https://github.com/thelounge/thelounge/commit/22ae594cc3d6905c82aa2238f4cd68506acf79a3) by [@brunnre8](https://github.com/brunnre8))
## v4.4.2-rc.1 - 2024-02-19 [Pre-release]
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
and running a database `VACUUM` will use up ~2x the current DB disc space for a short period.
If you enable the storagePolicy, stop the running instance and run `thelounge storage clean`.
This will force a full cleanup once, rather than doing so incrementally and will release all the
disc space back to the OS.
As usual, we follow the Node.js release schedule, so the minimum Node.js version required is now 18.
[See the full changelog](https://github.com/thelounge/thelounge/compare/v4.4.1...v4.4.2-rc.1)
This is a release candidate (RC) for v4.4.2 to ensure maximum stability for public release.
Bugs may be fixed, but no further features will be added until the next stable version.
Please refer to the commit list given above for a complete list of changes, or wait for the stable release to get a thoroughly prepared change log entry.
As with all pre-releases, this version requires explicit use of the `next` tag to be installed:
```sh
yarn global add thelounge@next
```
## v4.4.1 - 2023-06-13
Small bug fix release that addresses the bugs reported since v4.4.0
- fixes the image preview buttons disappearing.
- Restores the ability to change the password via the user interface.
Following the [Node.js maintenance schedule](https://nodejs.dev/en/about/releases/), The Lounge now needs at least Node.js 16 to run.
For more details, [see the full changelog](https://github.com/thelounge/thelounge/compare/v4.4.0...v4.4.1) and [milestone](https://github.com/thelounge/thelounge/milestone/43?closed=1).
### Fixed
- linkPreview: Pass channel prop ([`9388960`](https://github.com/thelounge/thelounge/commit/93889604973eeefb3a875e3ad5c9de737638888c) by [@brunnre8](https://github.com/brunnre8))
- client: fix password change input ([`8f08cf3`](https://github.com/thelounge/thelounge/commit/8f08cf3d0bd5b839016000afca1c700c74193f39) by [@brunnre8](https://github.com/brunnre8))
### Documentation
On the [website repository](https://github.com/thelounge/thelounge.github.io):
- Document local installation of packages ([`c72092e`](https://github.com/thelounge/thelounge.github.io/commit/c72092e2f8feab66f912b2c63c5a0572b123ea29) by [@brunnre8](https://github.com/brunnre8))
- docs: update docker image to point to the new ghcr.io repository ([`b43d002`](https://github.com/thelounge/thelounge.github.io/commit/b43d002584757709fff19dfdcf558c9d378f3d61) by [@williamboman](https://github.com/williamboman))
- Fix deb link ([`485570d`](https://github.com/thelounge/thelounge.github.io/commit/485570d4c4027296c546c2773272e4b44b0db06a) by [@brunnre8](https://github.com/brunnre8))
- deb: directly link to latest ([`c9a8ad9`](https://github.com/thelounge/thelounge.github.io/commit/c9a8ad95bbfc62f9ef704581fc742b069ff605fe) by [@brunnre8](https://github.com/brunnre8))
### Internals
- Remove unused code ([`7bce779`](https://github.com/thelounge/thelounge/commit/7bce77925449e2bcfa2db5d66dc5f808e04058c7) by [@brunnre8](https://github.com/brunnre8))
- settings: make missing_field msg descriptive ([`7a9ddc0`](https://github.com/thelounge/thelounge/commit/7a9ddc01e1819da8d28860548a82736f35283ab0) by [@brunnre8](https://github.com/brunnre8))
### Dependency updates
- build(deps): bump socket.io-parser from 4.2.1 to 4.2.3 ([`af49ef2`](https://github.com/thelounge/thelounge/commit/af49ef21ea3fed54c0807a4d87f9c0f9f70017c3) by [@dependabot[bot]](https://github.com/dependabot%5Bbot%5D))
- bump socket.io-parser from 4.2.1 to 4.2.3 ([`4d60d9c`](https://github.com/thelounge/thelounge/commit/4d60d9c282490ad63a1ff61e57e9a6c7a5fb9684) by [@brunnre8](https://github.com/brunnre8))
## v4.4.1-rc.2 - 2023-05-27 [Pre-release]
Restore the ability to change the password via the user interface.
[See the full changelog](https://github.com/thelounge/thelounge/compare/v4.4.0-rc.1...v4.4.1-rc.2)
This is a release candidate (RC) for v4.4.1 to ensure maximum stability for public release.
Bugs may be fixed, but no further features will be added until the next stable version.
Please refer to the commit list given above for a complete list of changes, or wait for the stable release to get a thoroughly prepared change log entry.
## v4.4.1-rc.1 - 2023-05-20 [Pre-release]
Small bug fix release that addresses the image preview buttons disappearing.
Following the [Node.js maintenance schedule](https://nodejs.dev/en/about/releases/), The Lounge now needs at least Node.js 16 to run.
[See the full changelog](https://github.com/thelounge/thelounge/compare/v4.4.0...v4.4.1-rc.1)
This is a release candidate (RC) for v4.4.1 to ensure maximum stability for public release.
Bugs may be fixed, but no further features will be added until the next stable version.
Please refer to the commit list given above for a complete list of changes, or wait for the stable release to get a thoroughly prepared change log entry.
As with all pre-releases, this version requires explicit use of the `next` tag to be installed:
```sh
yarn global add thelounge@next
```
## v4.4.0 - 2023-04-22
For more details, [see the full changelog](https://github.com/thelounge/thelounge/compare/v4.3.1...v4.4.0) and [milestone](https://github.com/thelounge/thelounge/milestone/42?closed=1).
This is mostly a developer focused release. Max, Eric and others rewrote the whole thing in TypeScript / Vue 3,
which should make it much easier to add features and find bugs in the future. So huge kudos from the rest of the team!
Additionally, there's the obvious grab bag of fixes, dependency updates and improvements.
Settings are now grouped and easier to navigate for new users.
Following the Node.js maintenance schedule, The Lounge now needs at least Node.js 14 to run.
A big thanks to everyone who contributed in any way to this release, your help is much appreciated.
Considering that a bunch of our dependencies had security issues assigned to them, all users are advised to update to the new version.
Packagers: Considering the switch to TypeScript, the server build now emits to the dist/ folder.
You might need to adapt your build scripts.
### Added
- Add prefetchTimeout ([`aa7db1e`](https://github.com/thelounge/thelounge/commit/aa7db1e7f787350f4102f98b85a2e8173173f92a) by [@brunnre8](https://github.com/brunnre8))
### Changed
- Refactor settings to their own tabs and routes ([#4489](https://github.com/thelounge/thelounge/pull/4489) by [@MaxLeiter](https://github.com/MaxLeiter))
- allow away and back to be collapsed ([#4669](https://github.com/thelounge/thelounge/pull/4669) by [@brunnre8](https://github.com/brunnre8))
- Kill TL when ident can't start up (#4512) ([`37d7de7`](https://github.com/thelounge/thelounge/commit/37d7de7671cf07f8a7fb3a8b3ea32122a738b646) by [@brunnre8](https://github.com/brunnre8))
- enable znc/playback even without message storage ([`c8115e2`](https://github.com/thelounge/thelounge/commit/c8115e22acf4a6e34a1546fd2fc273c76cbb7e86) by [@brunnre8](https://github.com/brunnre8))
- Remove node 12, add node 18. Bump minimum node version 14 (#4552) ([`9dbb6e5`](https://github.com/thelounge/thelounge/commit/9dbb6e5e1923dc1a2d3d69b0eac2778ff8cf5d3b) by [@xPaw](https://github.com/xPaw))
- linkPreviews: Enforce TLS validity ([`621fa92`](https://github.com/thelounge/thelounge/commit/621fa92036d59aa6558df828a1ff48136eed19ce) by [@brunnre8](https://github.com/brunnre8))
- Use nick as a realname fallback ([`30e9f45`](https://github.com/thelounge/thelounge/commit/30e9f45fac5b675ddadf5f904f0d0f05a7cdb5f9) by [@brunnre8](https://github.com/brunnre8))
- Plugins: include pre-releases in compatibility lookup (#4506) ([`e4840b4`](https://github.com/thelounge/thelounge/commit/e4840b4d75ff4dc79083955ebd9dfbdd7dd7ea8a) by [@brunnre8](https://github.com/brunnre8))
- install: expand ~ for local paths ([`e221e70`](https://github.com/thelounge/thelounge/commit/e221e708c1237eaa3088d97aebf8bf4869843dc6) by [@brunnre8](https://github.com/brunnre8))
### Fixed
- Fix the alignment of the header buttons ([#4539](https://github.com/thelounge/thelounge/pull/4539) by [@ronilaukkarinen](https://github.com/ronilaukkarinen))
- Fix user commands not working ([#4594](https://github.com/thelounge/thelounge/pull/4594) by [@xPaw](https://github.com/xPaw))
- Don't crash on oidentd socket race condition ([#4695](https://github.com/thelounge/thelounge/pull/4695) by [@maxpoulin64](https://github.com/maxpoulin64))
- cli: don't error if the user folder doesn't exist (#4508) ([`8153198`](https://github.com/thelounge/thelounge/commit/815319810c28ffe17119a5dc62f7eac33eba12f5) by [@brunnre8](https://github.com/brunnre8))
- Fix user file permissions on create (#4507) ([`d7bba32`](https://github.com/thelounge/thelounge/commit/d7bba325a73b1898edfa4299c4525749e174bbac) by [@brunnre8](https://github.com/brunnre8))
- sqlite: Escape '%' and '\_' in search queries. (#4487) ([`20ed3e6`](https://github.com/thelounge/thelounge/commit/20ed3e6dc5cf482e38d537444163e98b2bae0879) by [@progval](https://github.com/progval))
- set 'video/quicktime' to 'video/mp4' (#4495) ([`57b1e51`](https://github.com/thelounge/thelounge/commit/57b1e51e9f0f65e0866f5a809b12efaaf277536a) by [@xnaas](https://github.com/xnaas))
- Preserve client certificate ([`c9c8cad`](https://github.com/thelounge/thelounge/commit/c9c8cadb1a00f01d00920792cc129077aa6934fd) by [@brunnre8](https://github.com/brunnre8))
- Remove uploading event listeners on ChatInput unmount (#4600) ([`80f65c5`](https://github.com/thelounge/thelounge/commit/80f65c5b7276c466d2032fb3a7822fa39df3c685) by [@MaxLeiter](https://github.com/MaxLeiter))
- Potentially fix saving new networks (#4599) ([`d72d869`](https://github.com/thelounge/thelounge/commit/d72d8694bbea9fde7bf86275fb77b4c4c8a168ec) by [@MaxLeiter](https://github.com/MaxLeiter))
- Fix regex escape for prefix patterns ([`d6e1af0`](https://github.com/thelounge/thelounge/commit/d6e1af0e7dedb34dcd9932105ee4f2ddbe98e221) by [@brunnre8](https://github.com/brunnre8))
- Fix ctcp request message (#4603) ([`c8cd405`](https://github.com/thelounge/thelounge/commit/c8cd4057bc4ef19271720fc6b893b9c74e690457) by [@brunnre8](https://github.com/brunnre8))
- connect: Trim white space from user input fields (#4623) ([`0fa2035`](https://github.com/thelounge/thelounge/commit/0fa203569a62ee6bc6062b781729c7d801ccb8ba) by [@brunnre8](https://github.com/brunnre8))
- Search: Clear earlier searches when a new one is executed ([`83e11b0`](https://github.com/thelounge/thelounge/commit/83e11b0143e599a40924cab856636beeca6df27c) by [@brunnre8](https://github.com/brunnre8))
- Fix previous-source calculation (#4656) ([`073a38e`](https://github.com/thelounge/thelounge/commit/073a38ef1ef3c46740a028d4cbe7ebe4c7a08526) by [@brunnre8](https://github.com/brunnre8))
- Fix sidebar swipe flicker after letting go ([`502780c`](https://github.com/thelounge/thelounge/commit/502780c5a3e3455d977d8873506f1be51946fa68) by [@xPaw](https://github.com/xPaw))
- search: ignore searchResults if it isn't the active query ([`0ebc3a5`](https://github.com/thelounge/thelounge/commit/0ebc3a574c42185c818ca8795a56d8eb58a20f4e) by [@brunnre8](https://github.com/brunnre8))
- fix motd display to match settings ([#4726])(https://github.com/thelounge/thelounge/pull/4726) by [@SpaceLenore](https://github.com/SpaceLenore))
### Documentation
- Fix misleading LDAP filiter in default config ([`f785acb`](https://github.com/thelounge/thelounge/commit/f785acb07d78ae791a24a39821a93afb81616934) by [@goodspeed34](https://github.com/goodspeed34))
- Use correct option name (filter instead of ldapFilter) in config.js c… ([`4af5fc6`](https://github.com/thelounge/thelounge/commit/4af5fc6f33b43d64adcebcbf5aa8c4dceaad493f) by [@murph](https://github.com/murph))
- Add password param to /join docs ([`8b1a4f7`](https://github.com/thelounge/thelounge/commit/8b1a4f72fa79e12b43ff3073f0d48b13d93008e7) by [@aab12345](https://github.com/aab12345))
- install: Document file: prefix in cli help ([`31739b8`](https://github.com/thelounge/thelounge/commit/31739b8ac9ff95a03c374b32cc9bce2163d05d1e) by [@brunnre8](https://github.com/brunnre8))
On the [website repository](https://github.com/thelounge/thelounge.github.io):
- Link directly to themes on npm (#261) ([`410f5d0`](https://github.com/thelounge/thelounge.github.io/commit/410f5d077676cf597397b01acdc81414cc3dbc01) by [@jeremiah-rs](https://github.com/jeremiah-rs))
- Don't use yarn link for source installs ([#262](https://github.com/thelounge/thelounge.github.io/pull/262) by [@brunnre8](https://github.com/brunnre8))
- Add Insecure Warning CSS (#264) ([`95efa48`](https://github.com/thelounge/thelounge.github.io/commit/95efa482668af7997c7058cf01dff611efdea644) by [@aab12345](https://github.com/aab12345))
- Add custom nick colors section to custom css guide (#265) ([`63847c3`](https://github.com/thelounge/thelounge.github.io/commit/63847c346b6e49ddcdb34f5b733b57e3db8cc2df) by [@xnaas](https://github.com/xnaas))
- Fix Apache configuration syntax ([`41cb84e`](https://github.com/thelounge/thelounge.github.io/commit/41cb84ee70f5dc4a6920dfd1916fdf5eb00f190c) by [@lucaswerkmeister](https://github.com/lucaswerkmeister))
- Be more explicit about needing Yarn 1 (Classic) (#268) ([`1eff267`](https://github.com/thelounge/thelounge.github.io/commit/1eff26768a437e2bac1b62982da5ae02fdbda950) by [@SyntaxColoring](https://github.com/SyntaxColoring))
- Don't mention `npm` command for installation ([`7e936c2`](https://github.com/thelounge/thelounge.github.io/commit/7e936c2814b2902855570e928e0f13a40e17fce7) by [@SyntaxColoring](https://github.com/SyntaxColoring))
- Update reverse-proxies.md ([`afc7e29`](https://github.com/thelounge/thelounge.github.io/commit/afc7e2957211f0fa9a4f986fb4a0a03547384a6d) by [@PeGaSuS-Coder](https://github.com/PeGaSuS-Coder))
### Internals
- Decouple server ([#4686](https://github.com/thelounge/thelounge/pull/4686) by [@brunnre8](https://github.com/brunnre8))
- Tests/server: Tear down test fixtures in the order they were setup ([#4715](https://github.com/thelounge/thelounge/pull/4715) by [@progval](https://github.com/progval))
- Refactor config out of Helper (#4558) ([`d4cc2dd`](https://github.com/thelounge/thelounge/commit/d4cc2dd361bd2f166924dd18efdc57634d67bc19) by [@brunnre8](https://github.com/brunnre8))
- Convert configs to cjs, move babel to own file, combine webpack confi… ([`c205b89`](https://github.com/thelounge/thelounge/commit/c205b895233f5d7c58ef44bad31ccee777f3b95d) by [@nemchik](https://github.com/nemchik))
- Fix yarn dev (#4574) ([`2e3d9a6`](https://github.com/thelounge/thelounge/commit/2e3d9a6265d4c0d0168729a60b319bea236e098b) by [@nemchik](https://github.com/nemchik))
- TypeScript and Vue 3 (#4559) ([`dd05ee3`](https://github.com/thelounge/thelounge/commit/dd05ee3a656cb5eb5d0ab7620dbc7a1cfa4102ab) by [@MaxLeiter](https://github.com/MaxLeiter))
- Added client type checking to webpack (#4619) ([`117c5fa`](https://github.com/thelounge/thelounge/commit/117c5fa3fdbd2787bc1df521627b7b07fc1522c6) by [@antoniomika](https://github.com/antoniomika))
- don't call search on a disabled msg provider ([`bea4545`](https://github.com/thelounge/thelounge/commit/bea4545abffe738dfeb025b36817490c1b5fa61d) by [@brunnre8](https://github.com/brunnre8))
- extract migrations ([`f04a066`](https://github.com/thelounge/thelounge/commit/f04a06682d3690b571dc0b9720baa79b687b9465) by [@brunnre8](https://github.com/brunnre8))
- sqlite: error if sqlite isn't enabled but search() is called ([`cebc6d0`](https://github.com/thelounge/thelounge/commit/cebc6d069fa609de918881854414768fadc87fed) by [@brunnre8](https://github.com/brunnre8))
- sqlite: move export to bottom of the file ([`f6b2921`](https://github.com/thelounge/thelounge/commit/f6b292107ee4e627562d170babcb272cfa102a1e) by [@brunnre8](https://github.com/brunnre8))
- sqlite: fix docstring ([`e62b169`](https://github.com/thelounge/thelounge/commit/e62b169a6abab4b2a0df34a5da21c92136ba3790) by [@brunnre8](https://github.com/brunnre8))
- sqlite: add run helper function ([`89ee537`](https://github.com/thelounge/thelounge/commit/89ee5373643d1c5cb664401de745109bf7bcb77c) by [@brunnre8](https://github.com/brunnre8))
- sqlite: create serialize_fetchall helper function ([`cc3302e`](https://github.com/thelounge/thelounge/commit/cc3302e8743633b3b87e15fb54a964510b2466d1) by [@brunnre8](https://github.com/brunnre8))
- sqlite: use serialize_fetchall in getMessages ([`ee8223c`](https://github.com/thelounge/thelounge/commit/ee8223c2006ad31fc746824b495125b321da4bf8) by [@brunnre8](https://github.com/brunnre8))
- sqlite: use serialize_fetchall in search ([`5e1cbe3`](https://github.com/thelounge/thelounge/commit/5e1cbe32f95aca776fe4dff550a0c8c369460417) by [@brunnre8](https://github.com/brunnre8))
- sqlite: add serialize_get ([`bbe81bb`](https://github.com/thelounge/thelounge/commit/bbe81bb2fa9001762df90c1a267afa0239ebb7c7) by [@brunnre8](https://github.com/brunnre8))
- sqlite: convert migrations to async ([`f068fd4`](https://github.com/thelounge/thelounge/commit/f068fd429012c47648faf8c4d751f972062709bd) by [@brunnre8](https://github.com/brunnre8))
- messageStorage: convert to async ([`d62dd3e`](https://github.com/thelounge/thelounge/commit/d62dd3e62d106009cbded2fd9af13fe9fae35ae5) by [@brunnre8](https://github.com/brunnre8))
- SearchResults: remove computed search prop ([`6b617f8`](https://github.com/thelounge/thelounge/commit/6b617f893d73fb9e8304d228336cf574c29992a3) by [@brunnre8](https://github.com/brunnre8))
- SearchResults: Fix search progess upon search ([`dca2024`](https://github.com/thelounge/thelounge/commit/dca202427aa543d43d18fb72ae10ffa51b3b6c60) by [@brunnre8](https://github.com/brunnre8))
- SearchResults: remove dead code (#4639) ([`53f6041`](https://github.com/thelounge/thelounge/commit/53f6041f42ac36b5d69fc05cc66618ea0fe67a88) by [@brunnre8](https://github.com/brunnre8))
- SearchQuery: offset is always a number ([`8095d9e`](https://github.com/thelounge/thelounge/commit/8095d9e88a0018d2ac559ab01488d2736b4fe5e6) by [@brunnre8](https://github.com/brunnre8))
- Search: fix off by one offset error ([`51c9ce0`](https://github.com/thelounge/thelounge/commit/51c9ce078d15efafd677cff525b681dcec51fdd5) by [@brunnre8](https://github.com/brunnre8))
- keybinds: Fix invalid return ([`0765d20`](https://github.com/thelounge/thelounge/commit/0765d209f2ce204e2a3e86c56a7c2108a0487a6f) by [@brunnre8](https://github.com/brunnre8))
- server: the http{,s} server can't be null ([`1597c2c`](https://github.com/thelounge/thelounge/commit/1597c2c56ec932859ebc77e31eda8c164f196388) by [@brunnre8](https://github.com/brunnre8))
- make getClientConfiguration type safe ([`fd14b4a`](https://github.com/thelounge/thelounge/commit/fd14b4a17203bc043b8c9c1f371c2c5ced96eef7) by [@brunnre8](https://github.com/brunnre8))
- remove VueApp from router ([`dfb4217`](https://github.com/thelounge/thelounge/commit/dfb4217167bd20232bf2bdc443454a7ea9cc1094) by [@brunnre8](https://github.com/brunnre8))
- search: fix order of result merging ([`8204c34`](https://github.com/thelounge/thelounge/commit/8204c3481ad1e5eb3f59cabdb5c3c52936094b48) by [@brunnre8](https://github.com/brunnre8))
- store: addMessageSearchResults shouldn't accept null ([`982816f`](https://github.com/thelounge/thelounge/commit/982816ff2015077fe2903180df6420005c73b33e) by [@brunnre8](https://github.com/brunnre8))
- sqlite: synchronize enable() internally ([`2d4143b`](https://github.com/thelounge/thelounge/commit/2d4143b7798c9cf0600280a5a79cb9061585be0e) by [@brunnre8](https://github.com/brunnre8))
- messagestorage: remove implementation details from interface ([`661d5cb`](https://github.com/thelounge/thelounge/commit/661d5cb5b0d6c3aebb9a83ac4c5115d0411b3f39) by [@brunnre8](https://github.com/brunnre8))
- textStorage: rip out client instance ([`52b8a2a`](https://github.com/thelounge/thelounge/commit/52b8a2a78e62dfdcdd2313e8c7e81a7b07f383e2) by [@brunnre8](https://github.com/brunnre8))
- sqlite: Remove client from sqlitestorage ([`958a948`](https://github.com/thelounge/thelounge/commit/958a948456d1a0c3c97bb60e8759e8f9f5578ac8) by [@brunnre8](https://github.com/brunnre8))
- Fix uploader mount/unmount lifecycle ([`2ce374f`](https://github.com/thelounge/thelounge/commit/2ce374fe858992c5c930b0c49bf40cba2928f839) by [@maxpoulin64](https://github.com/maxpoulin64))
- Fix git commit not being available in dist build ([`2f04150`](https://github.com/thelounge/thelounge/commit/2f04150461fbd538b09e58d8c1beb33ee0db18ce) by [@xPaw](https://github.com/xPaw))
- network: add getLobby accessor ([`fade6a8`](https://github.com/thelounge/thelounge/commit/fade6a8d2ec5d621d761e2f6a716c5e59f4a9770) by [@brunnre8](https://github.com/brunnre8))
- pluginCommand: type it and guard against bad input ([`4023323`](https://github.com/thelounge/thelounge/commit/402332340b727d7f4087b1f24dcd4eecf16b0891) by [@brunnre8](https://github.com/brunnre8))
- packaging: Use an include list in package.json ([`efd24fd`](https://github.com/thelounge/thelounge/commit/efd24fd12cad9192d6f333c5a3c01c33ad23b0c6) by [@brunnre8](https://github.com/brunnre8))
- Fix incorrect typing of dehydrated networks and channels ([`76098d7`](https://github.com/thelounge/thelounge/commit/76098d7e766ad074eb6278ee487410f1f02817c3) [@progval](https://github.com/progval))
- Client: move socket connection out of the constructor ([`a049a01`](https://github.com/thelounge/thelounge/commit/a049a01aeb2b09edaaf46411bb764c14a607b343) [@progval](https://github.com/progval))
- Fix test wording ([`d58fb84`](https://github.com/thelounge/thelounge/commit/d58fb845651fe2859313c05a80cdcdebc27a8c68) [@progval](https://github.com/progval))
- Remove override of UserConfig ([`320075e`](https://github.com/thelounge/thelounge/commit/320075e376eecc0843f57b2f9b3207f8f245930e) [@progval](https://github.com/progval))
- Fix sqlite query invocation in test ([`845daba`](https://github.com/thelounge/thelounge/commit/845dabad53c4a47b6c39f7529ad02ec810c5ed48) by [@brunnre8](https://github.com/brunnre8))
- Fix config typing and make Client easier to test ([`eb509f7`](https://github.com/thelounge/thelounge/commit/eb509f7100869427d3f8b4dbd54692bf12630e67) by [@brunnre8](https://github.com/brunnre8))
- server/client: refactor command input ([`4e954b9`](https://github.com/thelounge/thelounge/commit/4e954b919c86ad17f6c7f934de4aa8d6fe5b9b1d) by [@brunnre8](https://github.com/brunnre8))
- Clean up command input code ([`e8b6434`](https://github.com/thelounge/thelounge/commit/e8b6434144998693532ce2853c049e878f158d63) by [@brunnre8](https://github.com/brunnre8))
- Inline logger into changelog script ([#4717](https://github.com/thelounge/thelounge/pull/4717) by [@brunnre8](https://github.com/brunnre8))
- Fix load of channels from user config ([`0c7cc85`](https://github.com/thelounge/thelounge/commit/0c7cc85184d9f90987000ffcddfa2b9581bb96cb) Val Lorentz)
- style: Put user colors into the smallest possible scope ([`f55f772`](https://github.com/thelounge/thelounge/commit/f55f772659a505ceb8751d8728c22c810afed018) by [@brunnre8](https://github.com/brunnre8))
- Fix Morning theme nick colors ([#4690](https://github.com/thelounge/thelounge/pull/4690) by [@maxpoulin64](https://github.com/maxpoulin64))
- Publish to npm with provenance ([#4724])(https://github.com/thelounge/thelounge/pull/4724) by [@xPaw](https://github.com/xPaw))
### Dependency updates
_Aka the boring bits... It's the last section too, so feel free to gloss over it_
- fix(deps): update dependency got to v11.8.5 [security] ([#4596](https://github.com/thelounge/thelounge/pull/4596) by [@renovate](https://github.com/apps/renovate))
- `sqlite3` ([#4541](https://github.com/thelounge/thelounge/pull/4541))
- chore(deps): update dependency sqlite3 to v5.0.6 ([`da02350`](https://github.com/thelounge/thelounge/commit/da02350725291be79c0d6c5d15261a2e0ef72313) by [@renovate-bot](https://github.com/renovate-bot))
- chore(deps): update dependency @textcomplete/core to v0.1.11 (#4555) ([`99c48db`](https://github.com/thelounge/thelounge/commit/99c48dbcea2ebe08d64a38946d81301fbfe66ee2) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update babel monorepo (#4554) ([`38f1352`](https://github.com/thelounge/thelounge/commit/38f13525e6104ee332c64d2df20bfe2694bc7fe5) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency mocha to v9.2.2 (#4581) ([`194b85b`](https://github.com/thelounge/thelounge/commit/194b85be4d93813f763b06264124d5545ba8aa27) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency sqlite3 to v5.0.8 (#4564) ([`ddcee53`](https://github.com/thelounge/thelounge/commit/ddcee5371acfe960c53e85e97405d005953dec3c) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @textcomplete/textarea to v0.1.12 ([`e972165`](https://github.com/thelounge/thelounge/commit/e97216518adb9ac7d6ef458c362a591a0f56ed14) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @types/content-disposition to v0.5.5 ([`740618c`](https://github.com/thelounge/thelounge/commit/740618ca499aeb2efb8ffd4f0363b5cf841a49dc) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @textcomplete/core to v0.1.12 ([`0cb4791`](https://github.com/thelounge/thelounge/commit/0cb4791cd02c0fd2e578edc1366124117529ac10) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency sqlite3 to v5.0.10 ([`520646a`](https://github.com/thelounge/thelounge/commit/520646a212e08f971c870e6f464712a90e198d66) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- fix(deps): update dependency file-type to v16.5.4 [security] ([`0495761`](https://github.com/thelounge/thelounge/commit/0495761c4485ac86b43ced638a361b905e7ddc60) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): lock file maintenance ([`57ed37c`](https://github.com/thelounge/thelounge/commit/57ed37c1fda4024ae655de2defdf4af68ade69fe) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- Revert "chore(deps): update dependency @textcomplete/core to v0.1.12" ([`3240997`](https://github.com/thelounge/thelounge/commit/32409973478ecb88290447faa7f2639a6d5c4d1f) by [@brunnre8](https://github.com/brunnre8))
- chore(deps): update dependency sqlite3 to v5.1.2 ([`5a803cc`](https://github.com/thelounge/thelounge/commit/5a803ccd239e42fe8853b4c615e82ef2c64bbc14) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @vue/test-utils to v2.2.1 ([`cb17f8d`](https://github.com/thelounge/thelounge/commit/cb17f8d87f9eac3b3449455d47c5ddaec09c0c5d) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency postcss to v8.4.18 ([`5a4a39b`](https://github.com/thelounge/thelounge/commit/5a4a39b9d1f4a49ddc2f9c5551f9fd28d0307a4b) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency chai to v4.3.7 ([`0ad033f`](https://github.com/thelounge/thelounge/commit/0ad033fe0aac01e0f4512428fda0e93ddefdcfb6) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @types/is-utf8 to v0.2.1 ([`b5ea7cc`](https://github.com/thelounge/thelounge/commit/b5ea7cceb3ff6a13f0ee20f4ed1c017b983d7d8c) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @types/lodash to v4.14.188 ([`dfe288e`](https://github.com/thelounge/thelounge/commit/dfe288ef166a0ac07f538ee5a07c2f7b65ee15f9) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @types/chai to v4.3.4 ([`19307d0`](https://github.com/thelounge/thelounge/commit/19307d05e70f8b7ed9ab3d6177c7c9ae6c93a438) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency postcss to v8.4.19 ([`2218841`](https://github.com/thelounge/thelounge/commit/221884166df61feb43513205c982b271b299f074) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @types/lodash to v4.14.191 ([`d61ab7e`](https://github.com/thelounge/thelounge/commit/d61ab7e7a084018d68444c4b0ef8d14702142d84) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency sqlite3 to v5.1.4 ([`c854d27`](https://github.com/thelounge/thelounge/commit/c854d27d3d8451ea25051dc356dc8f101542f9a1) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @types/ws to v8.5.4 ([`502fb7a`](https://github.com/thelounge/thelounge/commit/502fb7a7050edbecd8e34b6c30664e0bdcfc4a6c) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @vue/test-utils to v2.2.7 ([`6b23b87`](https://github.com/thelounge/thelounge/commit/6b23b87063c893ce588321929598e579401e16ee) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency sinon to v13.0.2 ([`90d17ca`](https://github.com/thelounge/thelounge/commit/90d17cacc155a3a6bafd76411b2e00997347a24b) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency vue-loader to v17.0.1 ([`2f8dc01`](https://github.com/thelounge/thelounge/commit/2f8dc01930f921f4de23dff29abfc703fdbefdbc) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency webpack-dev-middleware to v5.3.3 ([`4742a07`](https://github.com/thelounge/thelounge/commit/4742a077211229191867033320c0efc876a9404c) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @vue/test-utils to v2.3.1 ([`50e8d2a`](https://github.com/thelounge/thelounge/commit/50e8d2a8903b1c1c826208850f46a5d98dbf6458) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency postcss to v8.4.21 ([`8e249d4`](https://github.com/thelounge/thelounge/commit/8e249d46afb234a4a1def2cbcc0204c4edd52bdc) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency sqlite3 to v5.1.5 [security] ([`bc4c308`](https://github.com/thelounge/thelounge/commit/bc4c3082b852e175e55003c8b91b2a69a7d8283f) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency webpack to v5.76.0 [security] ([`a67cee1`](https://github.com/thelounge/thelounge/commit/a67cee1ee43da01afd8c7584b44d46e6e8dc990d) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency sqlite3 to v5.1.6 ([`34a01c2`](https://github.com/thelounge/thelounge/commit/34a01c2dd164b60d7470b588f7c0e0ed3d3b7647) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- chore(deps): update dependency @types/mousetrap to v1.6.11 ([`5037383`](https://github.com/thelounge/thelounge/commit/5037383c4c9a87a53eaa358ffbe7492ab6ad6365) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- Autocomplete: update to @textcomplete package and close on blur (#4493) ([`bdd6e71`](https://github.com/thelounge/thelounge/commit/bdd6e71049a4ddc65eca8d6acc52ce5c7eb3f6fd) by [@MaxLeiter](https://github.com/MaxLeiter))
- Update sqlite3 to 5.0.3 ([`7db0d46`](https://github.com/thelounge/thelounge/commit/7db0d4619d98ad473eff7a1dbdf41c8b0167d0dd) by [@xPaw](https://github.com/xPaw))
- Merge sqlite3 upgrade to v5.0.6 ([`abf8906`](https://github.com/thelounge/thelounge/commit/abf89067575810339fa3c723af54a7ea670fe4e5) by [@brunnre8](https://github.com/brunnre8))
- bump socket.io to 4.5.2 ([`d4bbd91`](https://github.com/thelounge/thelounge/commit/d4bbd9191cd78f065386fe25c7e8e90b1171a159) by [@brunnre8](https://github.com/brunnre8))
- bump socket.io-client to 4.5.0 ([`4c7337b`](https://github.com/thelounge/thelounge/commit/4c7337b6257af2428e6e9f8af570126da094d266) by [@brunnre8](https://github.com/brunnre8))
- Bump engine.io from 6.2.0 to 6.2.1 ([`f8eb0eb`](https://github.com/thelounge/thelounge/commit/f8eb0ebafdf8824bfe316fd2ad8adb3b8beda2d2) by [@dependabot[bot]](https://github.com/dependabot%5Bbot%5D))
- Bump loader-utils from 2.0.2 to 2.0.4 ([`8924545`](https://github.com/thelounge/thelounge/commit/89245455ceceba157821437a3f8f4e80f3b03268) by [@dependabot[bot]](https://github.com/dependabot%5Bbot%5D))
- Bump loader-utils from 2.0.2 to 2.0.4 ([`21c8b0d`](https://github.com/thelounge/thelounge/commit/21c8b0d17fc7e09d1cad77990fa833fdcad62927) by [@brunnre8](https://github.com/brunnre8))
- update dependency @types/mousetrap to v1.6.11 ([`7ee4b80`](https://github.com/thelounge/thelounge/commit/7ee4b80a6e744b09385fc686cdca1fbf0e7784ac) by [@brunnre8](https://github.com/brunnre8))
- update dependency @types/lodash to v4.14.191 ([`c67df36`](https://github.com/thelounge/thelounge/commit/c67df36a29a04bacc9e3197a32368493ae0a2ae9) by [@brunnre8](https://github.com/brunnre8))
- caniuse-lite: update db ([`efd3b64`](https://github.com/thelounge/thelounge/commit/efd3b645642ff75639ecb27a8ff9d6f6e1c0ccab) by [@brunnre8](https://github.com/brunnre8))
- build(deps): bump json5 from 2.2.1 to 2.2.3 ([`ce3ad56`](https://github.com/thelounge/thelounge/commit/ce3ad56ced3b498def5bb65065b4185a46a20995) by [@dependabot[bot]](https://github.com/dependabot%5Bbot%5D))
- fix(deps): update dependency ua-parser-js to v1.0.33 [security] ([`bde5c3d`](https://github.com/thelounge/thelounge/commit/bde5c3d443dc1e965bdd2641abb94b526600ddec) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
- build(deps): bump http-cache-semantics from 4.1.0 to 4.1.1 ([`7304acd`](https://github.com/thelounge/thelounge/commit/7304acd8e072af33dfdd1ea2f108b91a6e449f65) by [@dependabot[bot]](https://github.com/dependabot%5Bbot%5D))
- update dependency postcss to v8.4.21 ([`95e5630`](https://github.com/thelounge/thelounge/commit/95e56300db48bbb75b3463267eb0809ee9739686) by [@brunnre8](https://github.com/brunnre8))
- update dependency sinon to v13.0.2 ([`0183d89`](https://github.com/thelounge/thelounge/commit/0183d89384405ad944863ecffd783c99f0c36517) by [@brunnre8](https://github.com/brunnre8))
- update dependency vue-loader to v17.0.1 ([`eddcbcc`](https://github.com/thelounge/thelounge/commit/eddcbcc7660e5f51d9b794ab0302abb9790c6b3c) by [@brunnre8](https://github.com/brunnre8))
- update dependency webpack-dev-middleware to v5.3.3 ([`4831c20`](https://github.com/thelounge/thelounge/commit/4831c2080415a72492e97d55be8512c86c4324b3) by [@brunnre8](https://github.com/brunnre8))
- update dependency webpack to v5.76.0 ([`6b00ccf`](https://github.com/thelounge/thelounge/commit/6b00ccf82b60503b31e4fee1e32f2765c234d8cc) by [@brunnre8](https://github.com/brunnre8))
## v4.4.0-pre.2 - 2023-03-19 [Pre-release]
[See the full changelog](https://github.com/thelounge/thelounge/compare/v4.4.0-pre.1...v4.4.0-pre.2)

View file

@ -90,3 +90,6 @@ Before submitting any change, make sure to:
- Run `yarn build:server` if you change anything in `server/`
- The built files will be output to `dist/` by tsc
- `yarn dev` can be used to start The Lounge with hot module reloading
To ensure that you don't commit files that fail the linting, you can install a pre-commit git hook.
Execute `yarn githooks-install` to do so.

View file

@ -38,7 +38,6 @@ import {
ref,
Ref,
InjectionKey,
inject,
} from "vue";
import {useStore} from "../js/store";
import type {DebouncedFunc} from "lodash";
@ -47,10 +46,6 @@ export const imageViewerKey = Symbol() as InjectionKey<Ref<typeof ImageViewer |
const contextMenuKey = Symbol() as InjectionKey<Ref<typeof ContextMenu | null>>;
const confirmDialogKey = Symbol() as InjectionKey<Ref<typeof ConfirmDialog | null>>;
export const useImageViewer = () => {
return inject(imageViewerKey) as Ref<typeof ImageViewer | null>;
};
export default defineComponent({
name: "App",
components: {

View file

@ -3,9 +3,10 @@
<div
id="chat"
:class="{
'hide-motd': store.state.settings.motd,
'hide-motd': !store.state.settings.motd,
'time-seconds': store.state.settings.showSeconds,
'time-12h': store.state.settings.use12hClock,
'colored-nicks': true, // TODO temporarily fixes themes, to be removed in next major version
}"
>
<div
@ -135,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",
@ -160,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;
}
@ -193,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);
@ -456,6 +456,7 @@ export default defineComponent({
return {
link,
channel,
image,
transform,
closeViewer,

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

@ -15,6 +15,7 @@
<RevealPassword v-slot:default="slotProps">
<input
id="current-password"
v-model="old_password"
autocomplete="current-password"
:type="slotProps.isVisible ? 'text' : 'password'"
name="old_password"
@ -28,6 +29,7 @@
<RevealPassword v-slot:default="slotProps">
<input
id="new-password"
v-model="new_password"
:type="slotProps.isVisible ? 'text' : 'password'"
name="new_password"
autocomplete="new-password"
@ -41,6 +43,7 @@
<RevealPassword v-slot:default="slotProps">
<input
id="new-password-verify"
v-model="verify_password"
:type="slotProps.isVisible ? 'text' : 'password'"
name="verify_password"
autocomplete="new-password"
@ -111,17 +114,11 @@ export default defineComponent({
RevealPassword,
Session,
},
props: {
settingsForm: {
type: Object as PropType<HTMLFormElement>,
required: true,
},
},
setup(props) {
setup() {
const store = useStore();
const passwordErrors = {
missing_fields: "Please enter a new password",
missing_fields: "Please fill in all fields",
password_mismatch: "Both new password fields must match",
password_incorrect: "The current password field does not match your account password",
update_failed: "Failed to update your password",
@ -132,6 +129,10 @@ export default defineComponent({
error: keyof typeof passwordErrors;
}>();
const old_password = ref("");
const new_password = ref("");
const verify_password = ref("");
const currentSession = computed(() => {
return store.state.sessions.find((item) => item.current);
});
@ -149,12 +150,10 @@ export default defineComponent({
});
const changePassword = () => {
const allFields = new FormData(props.settingsForm);
const data = {
old_password: allFields.get("old_password"),
new_password: allFields.get("new_password"),
verify_password: allFields.get("verify_password"),
old_password: old_password.value,
new_password: new_password.value,
verify_password: verify_password.value,
};
if (!data.old_password || !data.new_password || !data.verify_password) {
@ -189,6 +188,9 @@ export default defineComponent({
activeSessions,
otherSessions,
changePassword,
old_password,
new_password,
verify_password,
};
},
});

View file

@ -28,17 +28,6 @@
<div v-if="store.state.pushNotificationState === 'unsupported'" class="error">
<strong>Warning</strong>:
<span>Push notifications are not supported by your browser.</span>
<div v-if="isIOS" class="apple-push-unsupported">
Safari does
<a
href="https://bugs.webkit.org/show_bug.cgi?id=182566"
target="_blank"
rel="noopener"
>not support the web push notification specification</a
>, and because all browsers on iOS use Safari under the hood, The Lounge is
unable to provide push notifications on iOS devices.
</div>
</div>
</div>
</template>

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

@ -179,6 +179,26 @@
</div>
</div>
<div class="help-item">
<div class="subject">
<span v-if="!isApple"><kbd>Alt</kbd> <kbd>Ctrl</kbd> <kbd></kbd></span>
<span v-else><kbd></kbd> <kbd></kbd> <kbd></kbd></span>
</div>
<div class="description">
<p>Switch to the next window with unread messages in the channel list.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<span v-if="!isApple"><kbd>Alt</kbd> <kbd>Ctrl</kbd> <kbd></kbd></span>
<span v-else><kbd></kbd> <kbd></kbd> <kbd></kbd></span>
</div>
<div class="description">
<p>Switch to the previous window with unread messages in the channel list.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<span v-if="!isApple"><kbd>Alt</kbd> <kbd>A</kbd></span>

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

@ -7,14 +7,14 @@
<div class="container">
<form ref="settingsForm" autocomplete="off" @change="onChange" @submit.prevent>
<router-view :settings-form="settingsForm"></router-view>
<router-view></router-view>
</form>
</div>
</div>
</template>
<script lang="ts">
import {defineComponent, ref} from "vue";
import {defineComponent} from "vue";
import SidebarToggle from "../SidebarToggle.vue";
import Navigation from "../Settings/Navigation.vue";
import {useStore} from "../../js/store";
@ -27,7 +27,6 @@ export default defineComponent({
},
setup() {
const store = useStore();
const settingsForm = ref<HTMLFormElement>();
const onChange = (event: Event) => {
const ignore = ["old_password", "new_password", "verify_password"];
@ -51,7 +50,6 @@ export default defineComponent({
return {
onChange,
settingsForm,
};
},
});

View file

@ -19,14 +19,13 @@
<label for="signin-username">Username</label>
<input
id="signin-username"
ref="username"
v-model="username"
class="input"
type="text"
name="username"
autocapitalize="none"
autocorrect="off"
autocomplete="username"
:value="getStoredUser()"
required
autofocus
/>
@ -36,9 +35,8 @@
<RevealPassword v-slot:default="slotProps">
<input
id="signin-password"
ref="password"
v-model="password"
:type="slotProps.isVisible ? 'text' : 'password'"
name="password"
class="input"
autocapitalize="none"
autocorrect="off"
@ -70,8 +68,8 @@ export default defineComponent({
const inFlight = ref(false);
const errorShown = ref(false);
const username = ref<HTMLInputElement | null>(null);
const password = ref<HTMLInputElement | null>(null);
const username = ref(storage.get("user") || "");
const password = ref("");
const onAuthFailed = () => {
inFlight.value = false;
@ -89,8 +87,8 @@ export default defineComponent({
errorShown.value = false;
const values = {
user: username.value?.value,
password: password.value?.value,
user: username.value,
password: password.value,
};
storage.set("user", values.user);
@ -98,10 +96,6 @@ export default defineComponent({
socket.emit("auth:perform", values);
};
const getStoredUser = () => {
return storage.get("user");
};
onMounted(() => {
socket.on("auth:failed", onAuthFailed);
});
@ -116,7 +110,6 @@ export default defineComponent({
username,
password,
onSubmit,
getStoredUser,
};
},
});

View file

@ -1928,7 +1928,6 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
width: 100%;
}
#settings .apple-push-unsupported,
#settings .settings-sync-panel {
padding: 10px;
margin-bottom: 16px;
@ -1958,11 +1957,6 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
box-shadow: 0 0 0 3px rgb(0 123 255 / 50%);
}
#settings .apple-push-unsupported a {
color: inherit;
text-decoration: underline;
}
#settings .opt {
display: block;
padding: 5px 0 5px 1px;

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

@ -9,6 +9,7 @@
"😂": "face with tears of joy",
"🙂": "slightly smiling face",
"🙃": "upside-down face",
"🫠": "melting face",
"😉": "winking face",
"😊": "smiling face with smiling eyes",
"😇": "smiling face with halo",
@ -27,20 +28,27 @@
"🤪": "zany face",
"😝": "squinting face with tongue",
"🤑": "money-mouth face",
"🤗": "hugging face",
"🤗": "smiling face with open hands",
"🤭": "face with hand over mouth",
"🫢": "face with open eyes and hand over mouth",
"🫣": "face with peeking eye",
"🤫": "shushing face",
"🤔": "thinking face",
"🫡": "saluting face",
"🤐": "zipper-mouth face",
"🤨": "face with raised eyebrow",
"😐": "neutral face",
"😑": "expressionless face",
"😶": "face without mouth",
"🫥": "dotted line face",
"😶‍🌫": "face in clouds",
"😏": "smirking face",
"😒": "unamused face",
"🙄": "face with rolling eyes",
"😬": "grimacing face",
"😮‍💨": "face exhaling",
"🤥": "lying face",
"🫨": "shaking face",
"😌": "relieved face",
"😔": "pensive face",
"😪": "sleepy face",
@ -55,7 +63,8 @@
"🥵": "hot face",
"🥶": "cold face",
"🥴": "woozy face",
"😵": "dizzy face",
"😵": "face with crossed-out eyes",
"😵‍💫": "face with spiral eyes",
"🤯": "exploding head",
"🤠": "cowboy hat face",
"🥳": "partying face",
@ -64,6 +73,7 @@
"🤓": "nerd face",
"🧐": "face with monocle",
"😕": "confused face",
"🫤": "face with diagonal mouth",
"😟": "worried face",
"🙁": "slightly frowning face",
"☹": "frowning face",
@ -72,6 +82,7 @@
"😲": "astonished face",
"😳": "flushed face",
"🥺": "pleading face",
"🥹": "face holding back tears",
"😦": "frowning face with open mouth",
"😧": "anguished face",
"😨": "fearful face",
@ -88,7 +99,7 @@
"😫": "tired face",
"🥱": "yawning face",
"😤": "face with steam from nose",
"😡": "pouting face",
"😡": "enraged face",
"😠": "angry face",
"🤬": "face with symbols on mouth",
"😈": "smiling face with horns",
@ -115,7 +126,6 @@
"🙈": "see-no-evil monkey",
"🙉": "hear-no-evil monkey",
"🙊": "speak-no-evil monkey",
"💋": "kiss mark",
"💌": "love letter",
"💘": "heart with arrow",
"💝": "heart with ribbon",
@ -127,15 +137,21 @@
"💟": "heart decoration",
"❣": "heart exclamation",
"💔": "broken heart",
"❤‍🔥": "heart on fire",
"❤‍🩹": "mending heart",
"❤": "red heart",
"🩷": "pink heart",
"🧡": "orange heart",
"💛": "yellow heart",
"💚": "green heart",
"💙": "blue heart",
"🩵": "light blue heart",
"💜": "purple heart",
"🤎": "brown heart",
"🖤": "black heart",
"🩶": "grey heart",
"🤍": "white heart",
"💋": "kiss mark",
"💯": "hundred points",
"💢": "anger symbol",
"💥": "collision",
@ -143,23 +159,29 @@
"💦": "sweat droplets",
"💨": "dashing away",
"🕳": "hole",
"💣": "bomb",
"💬": "speech balloon",
"👁‍🗨": "eye in speech bubble",
"🗨": "left speech bubble",
"🗯": "right anger bubble",
"💭": "thought balloon",
"💤": "zzz",
"💤": "ZZZ",
"👋": "waving hand",
"🤚": "raised back of hand",
"🖐": "hand with fingers splayed",
"✋": "raised hand",
"🖖": "vulcan salute",
"🫱": "rightwards hand",
"🫲": "leftwards hand",
"🫳": "palm down hand",
"🫴": "palm up hand",
"🫷": "leftwards pushing hand",
"🫸": "rightwards pushing hand",
"👌": "OK hand",
"🤌": "pinched fingers",
"🤏": "pinching hand",
"✌": "victory hand",
"🤞": "crossed fingers",
"🫰": "hand with index finger and thumb crossed",
"🤟": "love-you gesture",
"🤘": "sign of the horns",
"🤙": "call me hand",
@ -169,6 +191,7 @@
"🖕": "middle finger",
"👇": "backhand index pointing down",
"☝": "index pointing up",
"🫵": "index pointing at the viewer",
"👍": "thumbs up",
"👎": "thumbs down",
"✊": "raised fist",
@ -177,6 +200,7 @@
"🤜": "right-facing fist",
"👏": "clapping hands",
"🙌": "raising hands",
"🫶": "heart hands",
"👐": "open hands",
"🤲": "palms up together",
"🤝": "handshake",
@ -201,6 +225,7 @@
"👁": "eye",
"👅": "tongue",
"👄": "mouth",
"🫦": "biting lip",
"👶": "baby",
"🧒": "child",
"👦": "boy",
@ -208,7 +233,9 @@
"🧑": "person",
"👱": "person: blond hair",
"👨": "man",
"🧔": "man: beard",
"🧔": "person: beard",
"🧔‍♂": "man: beard",
"🧔‍♀": "woman: beard",
"👨‍🦰": "man: red hair",
"👨‍🦱": "man: curly hair",
"👨‍🦳": "man: white hair",
@ -318,6 +345,7 @@
"👷": "construction worker",
"👷‍♂": "man construction worker",
"👷‍♀": "woman construction worker",
"🫅": "person with crown",
"🤴": "prince",
"👸": "princess",
"👳": "person wearing turban",
@ -332,6 +360,8 @@
"👰‍♂": "man with veil",
"👰‍♀": "woman with veil",
"🤰": "pregnant woman",
"🫃": "pregnant man",
"🫄": "pregnant person",
"🤱": "breast-feeding",
"👩‍🍼": "woman feeding baby",
"👨‍🍼": "man feeding baby",
@ -367,6 +397,7 @@
"🧟": "zombie",
"🧟‍♂": "man zombie",
"🧟‍♀": "woman zombie",
"🧌": "troll",
"💆": "person getting massage",
"💆‍♂": "man getting massage",
"💆‍♀": "woman getting massage",
@ -517,6 +548,8 @@
"🐅": "tiger",
"🐆": "leopard",
"🐴": "horse face",
"🫎": "moose",
"🫏": "donkey",
"🐎": "horse",
"🦄": "unicorn",
"🦓": "zebra",
@ -579,6 +612,9 @@
"🦩": "flamingo",
"🦚": "peacock",
"🦜": "parrot",
"🪽": "wing",
"🐦‍⬛": "black bird",
"🪿": "goose",
"🐸": "frog",
"🐊": "crocodile",
"🐢": "turtle",
@ -598,6 +634,8 @@
"🦈": "shark",
"🐙": "octopus",
"🐚": "spiral shell",
"🪸": "coral",
"🪼": "jellyfish",
"🐌": "snail",
"🦋": "butterfly",
"🐛": "bug",
@ -617,6 +655,7 @@
"💐": "bouquet",
"🌸": "cherry blossom",
"💮": "white flower",
"🪷": "lotus",
"🏵": "rosette",
"🌹": "rose",
"🥀": "wilted flower",
@ -624,6 +663,7 @@
"🌻": "sunflower",
"🌼": "blossom",
"🌷": "tulip",
"🪻": "hyacinth",
"🌱": "seedling",
"🪴": "potted plant",
"🌲": "evergreen tree",
@ -637,6 +677,9 @@
"🍁": "maple leaf",
"🍂": "fallen leaf",
"🍃": "leaf fluttering in wind",
"🪹": "empty nest",
"🪺": "nest with eggs",
"🍄": "mushroom",
"🍇": "grapes",
"🍈": "melon",
"🍉": "watermelon",
@ -668,9 +711,11 @@
"🥦": "broccoli",
"🧄": "garlic",
"🧅": "onion",
"🍄": "mushroom",
"🥜": "peanuts",
"🫘": "beans",
"🌰": "chestnut",
"🫚": "ginger root",
"🫛": "pea pod",
"🍞": "bread",
"🥐": "croissant",
"🥖": "baguette bread",
@ -755,6 +800,7 @@
"🍻": "clinking beer mugs",
"🥂": "clinking glasses",
"🥃": "tumbler glass",
"🫗": "pouring liquid",
"🥤": "cup with straw",
"🧋": "bubble tea",
"🧃": "beverage box",
@ -765,6 +811,7 @@
"🍴": "fork and knife",
"🥄": "spoon",
"🔪": "kitchen knife",
"🫙": "jar",
"🏺": "amphora",
"🌍": "globe showing Europe-Africa",
"🌎": "globe showing Americas",
@ -827,6 +874,7 @@
"🌉": "bridge at night",
"♨": "hot springs",
"🎠": "carousel horse",
"🛝": "playground slide",
"🎡": "ferris wheel",
"🎢": "roller coaster",
"💈": "barber pole",
@ -875,12 +923,14 @@
"🛤": "railway track",
"🛢": "oil drum",
"⛽": "fuel pump",
"🛞": "wheel",
"🚨": "police car light",
"🚥": "horizontal traffic light",
"🚦": "vertical traffic light",
"🛑": "stop sign",
"🚧": "construction",
"⚓": "anchor",
"🛟": "ring buoy",
"⛵": "sailboat",
"🛶": "canoe",
"🚤": "speedboat",
@ -1035,13 +1085,13 @@
"🎿": "skis",
"🛷": "sled",
"🥌": "curling stone",
"🎯": "direct hit",
"🎯": "bullseye",
"🪀": "yo-yo",
"🪁": "kite",
"🔫": "water pistol",
"🎱": "pool 8 ball",
"🔮": "crystal ball",
"🪄": "magic wand",
"🧿": "nazar amulet",
"🎮": "video game",
"🕹": "joystick",
"🎰": "slot machine",
@ -1049,6 +1099,7 @@
"🧩": "puzzle piece",
"🧸": "teddy bear",
"🪅": "piñata",
"🪩": "mirror ball",
"🪆": "nesting dolls",
"♠": "spade suit",
"♥": "heart suit",
@ -1085,6 +1136,7 @@
"🩳": "shorts",
"👙": "bikini",
"👚": "womans clothes",
"🪭": "folding hand fan",
"👛": "purse",
"👜": "handbag",
"👝": "clutch bag",
@ -1099,6 +1151,7 @@
"👡": "womans sandal",
"🩰": "ballet shoes",
"👢": "womans boot",
"🪮": "hair pick",
"👑": "crown",
"👒": "womans hat",
"🎩": "top hat",
@ -1137,6 +1190,8 @@
"🪕": "banjo",
"🥁": "drum",
"🪘": "long drum",
"🪇": "maracas",
"🪈": "flute",
"📱": "mobile phone",
"📲": "mobile phone with arrow",
"☎": "telephone",
@ -1144,6 +1199,7 @@
"📟": "pager",
"📠": "fax machine",
"🔋": "battery",
"🪫": "low battery",
"🔌": "electric plug",
"💻": "laptop",
"🖥": "desktop computer",
@ -1255,7 +1311,7 @@
"🛠": "hammer and wrench",
"🗡": "dagger",
"⚔": "crossed swords",
"🔫": "pistol",
"💣": "bomb",
"🪃": "boomerang",
"🏹": "bow and arrow",
"🛡": "shield",
@ -1284,7 +1340,9 @@
"🩸": "drop of blood",
"💊": "pill",
"🩹": "adhesive bandage",
"🩼": "crutch",
"🩺": "stethoscope",
"🩻": "x-ray",
"🚪": "door",
"🛗": "elevator",
"🪞": "mirror",
@ -1305,6 +1363,7 @@
"🧻": "roll of paper",
"🪣": "bucket",
"🧼": "soap",
"🫧": "bubbles",
"🪥": "toothbrush",
"🧽": "sponge",
"🧯": "fire extinguisher",
@ -1313,8 +1372,11 @@
"⚰": "coffin",
"🪦": "headstone",
"⚱": "funeral urn",
"🧿": "nazar amulet",
"🪬": "hamsa",
"🗿": "moai",
"🪧": "placard",
"🪪": "identification card",
"🏧": "ATM sign",
"🚮": "litter in bin sign",
"🚰": "potable water",
@ -1374,6 +1436,7 @@
"☮": "peace symbol",
"🕎": "menorah",
"🔯": "dotted six-pointed star",
"🪯": "khanda",
"♈": "Aries",
"♉": "Taurus",
"♊": "Gemini",
@ -1409,6 +1472,7 @@
"🔅": "dim button",
"🔆": "bright button",
"📶": "antenna bars",
"🛜": "wireless",
"📳": "vibration mode",
"📴": "mobile phone off",
"♀": "female sign",
@ -1418,13 +1482,14 @@
"": "plus",
"": "minus",
"➗": "divide",
"🟰": "heavy equals sign",
"♾": "infinity",
"‼": "double exclamation mark",
"⁉": "exclamation question mark",
"❓": "question mark",
"❓": "red question mark",
"❔": "white question mark",
"❕": "white exclamation mark",
"❗": "exclamation mark",
"❗": "red exclamation mark",
"〰": "wavy dash",
"💱": "currency exchange",
"💲": "heavy dollar sign",

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

@ -12,6 +12,7 @@
"joy": "😂",
"slightly_smiling_face": "🙂",
"upside_down_face": "🙃",
"melting_face": "🫠",
"wink": "😉",
"blush": "😊",
"innocent": "😇",
@ -32,18 +33,25 @@
"money_mouth_face": "🤑",
"hugs": "🤗",
"hand_over_mouth": "🤭",
"face_with_open_eyes_and_hand_over_mouth": "🫢",
"face_with_peeking_eye": "🫣",
"shushing_face": "🤫",
"thinking": "🤔",
"saluting_face": "🫡",
"zipper_mouth_face": "🤐",
"raised_eyebrow": "🤨",
"neutral_face": "😐",
"expressionless": "😑",
"no_mouth": "😶",
"dotted_line_face": "🫥",
"face_in_clouds": "😶‍🌫️",
"smirk": "😏",
"unamused": "😒",
"roll_eyes": "🙄",
"grimacing": "😬",
"face_exhaling": "😮‍💨",
"lying_face": "🤥",
"shaking_face": "🫨",
"relieved": "😌",
"pensive": "😔",
"sleepy": "😪",
@ -59,6 +67,7 @@
"cold_face": "🥶",
"woozy_face": "🥴",
"dizzy_face": "😵",
"face_with_spiral_eyes": "😵‍💫",
"exploding_head": "🤯",
"cowboy_hat_face": "🤠",
"partying_face": "🥳",
@ -67,6 +76,7 @@
"nerd_face": "🤓",
"monocle_face": "🧐",
"confused": "😕",
"face_with_diagonal_mouth": "🫤",
"worried": "😟",
"slightly_frowning_face": "🙁",
"frowning_face": "☹️",
@ -75,6 +85,7 @@
"astonished": "😲",
"flushed": "😳",
"pleading_face": "🥺",
"face_holding_back_tears": "🥹",
"frowning": "😦",
"anguished": "😧",
"fearful": "😨",
@ -121,7 +132,6 @@
"see_no_evil": "🙈",
"hear_no_evil": "🙉",
"speak_no_evil": "🙊",
"kiss": "💋",
"love_letter": "💌",
"cupid": "💘",
"gift_heart": "💝",
@ -133,15 +143,21 @@
"heart_decoration": "💟",
"heavy_heart_exclamation": "❣️",
"broken_heart": "💔",
"heart_on_fire": "❤️‍🔥",
"mending_heart": "❤️‍🩹",
"heart": "❤️",
"pink_heart": "🩷",
"orange_heart": "🧡",
"yellow_heart": "💛",
"green_heart": "💚",
"blue_heart": "💙",
"light_blue_heart": "🩵",
"purple_heart": "💜",
"brown_heart": "🤎",
"black_heart": "🖤",
"grey_heart": "🩶",
"white_heart": "🤍",
"kiss": "💋",
"anger": "💢",
"boom": "💥",
"collision": "💥",
@ -149,7 +165,6 @@
"sweat_drops": "💦",
"dash": "💨",
"hole": "🕳️",
"bomb": "💣",
"speech_balloon": "💬",
"eye_speech_bubble": "👁️‍🗨️",
"left_speech_bubble": "🗨️",
@ -162,11 +177,18 @@
"hand": "✋",
"raised_hand": "✋",
"vulcan_salute": "🖖",
"rightwards_hand": "🫱",
"leftwards_hand": "🫲",
"palm_down_hand": "🫳",
"palm_up_hand": "🫴",
"leftwards_pushing_hand": "🫷",
"rightwards_pushing_hand": "🫸",
"ok_hand": "👌",
"pinched_fingers": "🤌",
"pinching_hand": "🤏",
"v": "✌️",
"crossed_fingers": "🤞",
"hand_with_index_finger_and_thumb_crossed": "🫰",
"love_you_gesture": "🤟",
"metal": "🤘",
"call_me_hand": "🤙",
@ -177,6 +199,7 @@
"fu": "🖕",
"point_down": "👇",
"point_up": "☝️",
"index_pointing_at_the_viewer": "🫵",
"+1": "👍",
"thumbsup": "👍",
"-1": "👎",
@ -190,6 +213,7 @@
"fist_right": "🤜",
"clap": "👏",
"raised_hands": "🙌",
"heart_hands": "🫶",
"open_hands": "👐",
"palms_up_together": "🤲",
"handshake": "🤝",
@ -214,6 +238,7 @@
"eye": "👁️",
"tongue": "👅",
"lips": "👄",
"biting_lip": "🫦",
"baby": "👶",
"child": "🧒",
"boy": "👦",
@ -222,6 +247,8 @@
"blond_haired_person": "👱",
"man": "👨",
"bearded_person": "🧔",
"man_beard": "🧔‍♂️",
"woman_beard": "🧔‍♀️",
"red_haired_man": "👨‍🦰",
"curly_haired_man": "👨‍🦱",
"white_haired_man": "👨‍🦳",
@ -338,6 +365,7 @@
"construction_worker": "👷",
"construction_worker_man": "👷‍♂️",
"construction_worker_woman": "👷‍♀️",
"person_with_crown": "🫅",
"prince": "🤴",
"princess": "👸",
"person_with_turban": "👳",
@ -353,6 +381,8 @@
"woman_with_veil": "👰‍♀️",
"bride_with_veil": "👰‍♀️",
"pregnant_woman": "🤰",
"pregnant_man": "🫃",
"pregnant_person": "🫄",
"breast_feeding": "🤱",
"woman_feeding_baby": "👩‍🍼",
"man_feeding_baby": "👨‍🍼",
@ -388,6 +418,7 @@
"zombie": "🧟",
"zombie_man": "🧟‍♂️",
"zombie_woman": "🧟‍♀️",
"troll": "🧌",
"massage": "💆",
"massage_man": "💆‍♂️",
"massage_woman": "💆‍♀️",
@ -542,6 +573,8 @@
"tiger2": "🐅",
"leopard": "🐆",
"horse": "🐴",
"moose": "🫎",
"donkey": "🫏",
"racehorse": "🐎",
"unicorn": "🦄",
"zebra": "🦓",
@ -605,6 +638,9 @@
"flamingo": "🦩",
"peacock": "🦚",
"parrot": "🦜",
"wing": "🪽",
"black_bird": "🐦‍⬛",
"goose": "🪿",
"frog": "🐸",
"crocodile": "🐊",
"turtle": "🐢",
@ -625,6 +661,8 @@
"shark": "🦈",
"octopus": "🐙",
"shell": "🐚",
"coral": "🪸",
"jellyfish": "🪼",
"snail": "🐌",
"butterfly": "🦋",
"bug": "🐛",
@ -645,6 +683,7 @@
"bouquet": "💐",
"cherry_blossom": "🌸",
"white_flower": "💮",
"lotus": "🪷",
"rosette": "🏵️",
"rose": "🌹",
"wilted_flower": "🥀",
@ -652,6 +691,7 @@
"sunflower": "🌻",
"blossom": "🌼",
"tulip": "🌷",
"hyacinth": "🪻",
"seedling": "🌱",
"potted_plant": "🪴",
"evergreen_tree": "🌲",
@ -665,6 +705,9 @@
"maple_leaf": "🍁",
"fallen_leaf": "🍂",
"leaves": "🍃",
"empty_nest": "🪹",
"nest_with_eggs": "🪺",
"mushroom": "🍄",
"grapes": "🍇",
"melon": "🍈",
"watermelon": "🍉",
@ -698,9 +741,11 @@
"broccoli": "🥦",
"garlic": "🧄",
"onion": "🧅",
"mushroom": "🍄",
"peanuts": "🥜",
"beans": "🫘",
"chestnut": "🌰",
"ginger_root": "🫚",
"pea_pod": "🫛",
"bread": "🍞",
"croissant": "🥐",
"baguette_bread": "🥖",
@ -785,6 +830,7 @@
"beers": "🍻",
"clinking_glasses": "🥂",
"tumbler_glass": "🥃",
"pouring_liquid": "🫗",
"cup_with_straw": "🥤",
"bubble_tea": "🧋",
"beverage_box": "🧃",
@ -796,6 +842,7 @@
"spoon": "🥄",
"hocho": "🔪",
"knife": "🔪",
"jar": "🫙",
"amphora": "🏺",
"earth_africa": "🌍",
"earth_americas": "🌎",
@ -858,6 +905,7 @@
"bridge_at_night": "🌉",
"hotsprings": "♨️",
"carousel_horse": "🎠",
"playground_slide": "🛝",
"ferris_wheel": "🎡",
"roller_coaster": "🎢",
"barber": "💈",
@ -907,12 +955,14 @@
"railway_track": "🛤️",
"oil_drum": "🛢️",
"fuelpump": "⛽",
"wheel": "🛞",
"rotating_light": "🚨",
"traffic_light": "🚥",
"vertical_traffic_light": "🚦",
"stop_sign": "🛑",
"construction": "🚧",
"anchor": "⚓",
"ring_buoy": "🛟",
"boat": "⛵",
"sailboat": "⛵",
"canoe": "🛶",
@ -1072,10 +1122,10 @@
"dart": "🎯",
"yo_yo": "🪀",
"kite": "🪁",
"gun": "🔫",
"8ball": "🎱",
"crystal_ball": "🔮",
"magic_wand": "🪄",
"nazar_amulet": "🧿",
"video_game": "🎮",
"joystick": "🕹️",
"slot_machine": "🎰",
@ -1083,6 +1133,7 @@
"jigsaw": "🧩",
"teddy_bear": "🧸",
"pinata": "🪅",
"mirror_ball": "🪩",
"nesting_dolls": "🪆",
"spades": "♠️",
"hearts": "♥️",
@ -1120,6 +1171,7 @@
"shorts": "🩳",
"bikini": "👙",
"womans_clothes": "👚",
"folding_hand_fan": "🪭",
"purse": "👛",
"handbag": "👜",
"pouch": "👝",
@ -1135,6 +1187,7 @@
"sandal": "👡",
"ballet_shoes": "🩰",
"boot": "👢",
"hair_pick": "🪮",
"crown": "👑",
"womans_hat": "👒",
"tophat": "🎩",
@ -1173,6 +1226,8 @@
"banjo": "🪕",
"drum": "🥁",
"long_drum": "🪘",
"maracas": "🪇",
"flute": "🪈",
"iphone": "📱",
"calling": "📲",
"phone": "☎️",
@ -1181,6 +1236,7 @@
"pager": "📟",
"fax": "📠",
"battery": "🔋",
"low_battery": "🪫",
"electric_plug": "🔌",
"computer": "💻",
"desktop_computer": "🖥️",
@ -1296,7 +1352,7 @@
"hammer_and_wrench": "🛠️",
"dagger": "🗡️",
"crossed_swords": "⚔️",
"gun": "🔫",
"bomb": "💣",
"boomerang": "🪃",
"bow_and_arrow": "🏹",
"shield": "🛡️",
@ -1325,7 +1381,9 @@
"drop_of_blood": "🩸",
"pill": "💊",
"adhesive_bandage": "🩹",
"crutch": "🩼",
"stethoscope": "🩺",
"x_ray": "🩻",
"door": "🚪",
"elevator": "🛗",
"mirror": "🪞",
@ -1346,6 +1404,7 @@
"roll_of_paper": "🧻",
"bucket": "🪣",
"soap": "🧼",
"bubbles": "🫧",
"toothbrush": "🪥",
"sponge": "🧽",
"fire_extinguisher": "🧯",
@ -1354,8 +1413,11 @@
"coffin": "⚰️",
"headstone": "🪦",
"funeral_urn": "⚱️",
"nazar_amulet": "🧿",
"hamsa": "🪬",
"moyai": "🗿",
"placard": "🪧",
"identification_card": "🪪",
"atm": "🏧",
"put_litter_in_its_place": "🚮",
"potable_water": "🚰",
@ -1415,6 +1477,7 @@
"peace_symbol": "☮️",
"menorah": "🕎",
"six_pointed_star": "🔯",
"khanda": "🪯",
"aries": "♈",
"taurus": "♉",
"gemini": "♊",
@ -1450,6 +1513,7 @@
"low_brightness": "🔅",
"high_brightness": "🔆",
"signal_strength": "📶",
"wireless": "🛜",
"vibration_mode": "📳",
"mobile_phone_off": "📴",
"female_sign": "♀️",
@ -1459,6 +1523,7 @@
"heavy_plus_sign": "",
"heavy_minus_sign": "",
"heavy_division_sign": "➗",
"heavy_equals_sign": "🟰",
"infinity": "♾️",
"bangbang": "‼️",
"interrobang": "⁉️",

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;
}
}
@ -83,6 +84,35 @@ Mousetrap.bind(["alt+shift+up", "alt+shift+down"], function (e, keys) {
return false;
});
// Switch to the next/previous unread chat
Mousetrap.bind(["alt+mod+up", "alt+mod+down"], function (e, keys) {
if (isIgnoredKeybind(e)) {
return true;
}
const channels = store.state.networks
.map((net) =>
net.channels.filter(
(chan) => chan.unread || chan === store.state.activeChannel?.channel
)
)
.flat();
if (channels.length === 0) {
return;
}
let index = channels.findIndex((chan) => chan === store.state.activeChannel?.channel);
const length = channels.length;
const direction = keys.split("+").pop() === "up" ? -1 : 1;
index = (((index + direction) % length) + length) % length;
jumpToChannel(channels[index]);
return false;
});
// Jump to the first window with a highlight in it, or the first with unread
// activity if there are none with highlights.
Mousetrap.bind(["alt+a"], function (e) {

View file

@ -110,26 +110,23 @@ router.beforeEach((to, from, next) => {
next();
});
router.beforeEach((to, from, next) => {
router.beforeEach((to, from) => {
// Disallow navigating to non-existing routes
if (!to.matched.length) {
next(false);
return;
return false;
}
// Disallow navigating to invalid channels
if (to.name === "RoutedChat" && !store.getters.findChannel(Number(to.params.id))) {
next(false);
return;
return false;
}
// Disallow navigating to invalid networks
if (to.name === "NetworkEdit" && !store.getters.findNetwork(String(to.params.uuid))) {
next(false);
return;
return false;
}
next();
return true;
});
router.afterEach((to) => {
@ -165,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;
@ -90,31 +83,30 @@ export type State = {
searchEnabled: boolean;
};
const state = () =>
({
appLoaded: false,
activeChannel: undefined,
currentUserVisibleError: null,
desktopNotificationState: detectDesktopNotificationState(),
isAutoCompleting: false,
isConnected: false,
networks: [],
mentions: [],
hasServiceWorker: false,
pushNotificationState: "unsupported",
serverConfiguration: null,
sessions: [],
sidebarOpen: false,
sidebarDragging: false,
userlistOpen: storage.get("thelounge.state.userlist") !== "false",
versionData: null,
versionStatus: "loading",
versionDataExpired: false,
serverHasSettings: false,
messageSearchResults: null,
messageSearchPendingQuery: null,
searchEnabled: false,
} as State);
const state = (): State => ({
appLoaded: false,
activeChannel: undefined,
currentUserVisibleError: null,
desktopNotificationState: detectDesktopNotificationState(),
isAutoCompleting: false,
isConnected: false,
networks: [],
mentions: [],
hasServiceWorker: false,
pushNotificationState: "unsupported",
serverConfiguration: null,
sessions: [],
sidebarOpen: false,
sidebarDragging: false,
userlistOpen: storage.get("thelounge.state.userlist") !== "false",
versionData: null,
versionStatus: "loading",
versionDataExpired: false,
serverHasSettings: false,
messageSearchResults: null,
messageSearchPendingQuery: null,
searchEnabled: false,
});
type Getters = {
findChannelOnCurrentNetwork: (state: State) => (name: string) => ClientChan | undefined;
@ -132,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
@ -203,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,39 +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/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

@ -304,6 +304,26 @@ module.exports = {
// This value is set to `["sqlite", "text"]` by default.
messageStorage: ["sqlite", "text"],
// ### `storagePolicy`
// When the sqlite storage is in use, control the maximum storage duration.
// A background task will periodically clean up messages older than the limit.
// The available keys for the `storagePolicy` object are:
//
// - `enabled`: If this is false, the cleaning task is not running.
// - `maxAgeDays`: Maximum age of an entry in days.
// - `deletionPolicy`: Controls what types of messages are being deleted.
// Valid options are:
// - `statusOnly`: Only delete message types which are status related (e.g. away, back, join, parts, mode, ctcp...)
// but keep actual messages from nicks. This keeps the DB size down while retaining "precious" messages.
// - `everything`: Delete everything, including messages from irc nicks
storagePolicy: {
enabled: false,
maxAgeDays: 7,
deletionPolicy: "statusOnly",
},
// ### `useHexIp`
//
// When set to `true`, users' IP addresses will be encoded as hex.

View file

@ -1,7 +1,7 @@
{
"name": "thelounge",
"description": "The self-hosted Web IRC client",
"version": "4.4.0-pre.2",
"version": "4.4.3",
"preferGlobal": true,
"bin": {
"thelounge": "index.js"
@ -18,15 +18,18 @@
"coverage": "run-s test:* && nyc --nycrc-path=test/.nycrc-report.json report",
"dev": "cross-env NODE_ENV=development ts-node --project server/tsconfig.json server/index.ts start --dev",
"format:prettier": "prettier --write \"**/*.*\"",
"generate:config:doc": "ts-node scripts/generate-config-doc.js",
"lint:check-eslint": "eslint-config-prettier .eslintrc.cjs",
"lint:eslint": "eslint . --report-unused-disable-directives --color",
"lint:prettier": "prettier --list-different \"**/*.*\"",
"lint:stylelint": "stylelint --color \"client/**/*.css\"",
"lint": "run-p --aggregate-output --continue-on-error lint:*",
"start": "node index start",
"test": "run-p --aggregate-output --continue-on-error lint:* test:*",
"test:mocha": "cross-env NODE_ENV=test webpack --mode=development && cross-env NODE_ENV=test TS_NODE_PROJECT='./test/tsconfig.json' nyc --nycrc-path=test/.nycrc-mocha.json mocha --require ts-node/register --colors --config=test/.mocharc.yml",
"watch": "webpack --watch"
"test": "run-p --aggregate-output --continue-on-error lint:* test:mocha",
"test:mocha": "webpack --mode=development && cross-env NODE_ENV=test TS_NODE_PROJECT='./test/tsconfig.json' mocha --config=test/.mocharc.yml 'test/**/*.ts'",
"test:nospec": "webpack --mode=development && cross-env NODE_ENV=test TS_NODE_PROJECT='./test/tsconfig.json' mocha --config=test/.mocharc.yml",
"watch": "webpack --watch",
"githooks-install": "git config core.hooksPath scripts/git-hooks"
},
"keywords": [
"lounge",
@ -40,7 +43,7 @@
],
"license": "MIT",
"engines": {
"node": ">=14.0.0"
"node": ">=18.0.0"
},
"files": [
"./.thelounge_home",
@ -55,10 +58,10 @@
"@fastify/busboy": "1.0.0",
"bcryptjs": "2.4.3",
"chalk": "4.1.2",
"cheerio": "1.0.0-rc.10",
"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",
@ -72,8 +75,8 @@
"package-json": "7.0.0",
"read": "1.0.7",
"read-chunk": "3.2.0",
"semver": "7.3.5",
"socket.io": "4.5.2",
"semver": "7.5.2",
"socket.io": "4.6.1",
"tlds": "1.228.0",
"ua-parser-js": "1.0.33",
"uuid": "8.3.2",
@ -81,7 +84,7 @@
"yarn": "1.22.17"
},
"optionalDependencies": {
"sqlite3": "5.1.6"
"sqlite3": "5.1.7"
},
"devDependencies": {
"@babel/core": "7.17.10",
@ -90,34 +93,34 @@
"@fortawesome/fontawesome-free": "5.15.4",
"@istanbuljs/nyc-config-typescript": "1.0.2",
"@textcomplete/core": "0.1.10",
"@textcomplete/textarea": "0.1.12",
"@types/bcryptjs": "2.4.2",
"@types/chai": "4.3.4",
"@types/cheerio": "0.22.31",
"@types/content-disposition": "0.5.5",
"@types/express": "4.17.13",
"@types/is-utf8": "0.2.1",
"@textcomplete/textarea": "0.1.13",
"@types/bcryptjs": "2.4.6",
"@types/chai": "4.3.5",
"@types/cheerio": "0.22.35",
"@types/content-disposition": "0.5.8",
"@types/express": "4.17.21",
"@types/is-utf8": "0.2.3",
"@types/ldapjs": "2.2.2",
"@types/linkify-it": "3.0.2",
"@types/lodash": "4.14.191",
"@types/linkify-it": "3.0.5",
"@types/lodash": "4.14.202",
"@types/mime-types": "2.1.1",
"@types/mocha": "9.1.1",
"@types/mousetrap": "1.6.11",
"@types/node": "17.0.31",
"@types/read": "0.0.29",
"@types/mousetrap": "1.6.15",
"@types/node": "17.0.45",
"@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.3.1",
"@vue/test-utils": "2.4.0",
"babel-loader": "8.2.5",
"babel-plugin-istanbul": "6.1.1",
"babel-preset-typescript-vue3": "2.0.17",
@ -127,21 +130,20 @@
"css-loader": "6.5.1",
"cssnano": "5.0.17",
"dayjs": "1.10.8",
"emoji-regex": "9.2.2",
"eslint": "8.16.0",
"eslint-config-prettier": "8.3.0",
"eslint-define-config": "1.5.1",
"eslint-plugin-vue": "9.0.1",
"emoji-regex": "10.2.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",
"husky": "4.3.8",
"mini-css-extract-plugin": "2.5.3",
"mocha": "9.2.2",
"mousetrap": "1.6.5",
"normalize.css": "8.0.1",
"npm-run-all": "4.1.5",
"nyc": "15.1.0",
"postcss": "8.4.21",
"postcss": "8.4.31",
"postcss-import": "14.0.2",
"postcss-loader": "6.2.1",
"postcss-preset-env": "7.3.0",
@ -156,21 +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-hot-middleware": "2.25.1"
},
"husky": {
"hooks": {
"pre-commit": "pretty-quick --staged"
}
"webpack-dev-middleware": "5.3.4",
"webpack-hot-middleware": "2.25.4"
}
}

View file

@ -53,13 +53,38 @@ const got = require("got");
const dayjs = require("dayjs");
const semver = require("semver");
const util = require("util");
const log = require("../server/log");
const packageJson = require("../package.json");
let token = process.env.CHANGELOG_TOKEN;
const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);
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 */
};
const changelogPath = path.resolve(__dirname, "..", "CHANGELOG.md");
// CLI argument validations
@ -529,9 +554,8 @@ function printPullRequest(pullRequest) {
// Builds a Markdown list item for a commit made directly in `master`
function printCommit(commit) {
return `- ${commit.messageHeadline} (${printEntryLink(commit)} ${printAuthorLink(
commit.author.user
)})`;
const author_link = commit.author.user ? printAuthorLink(commit.author.user) : "unknown author";
return `- ${commit.messageHeadline} (${printEntryLink(commit)} ${author_link})`;
}
// Builds a Markdown list of all given items
@ -787,15 +811,31 @@ function dedupeEntries(changelog, items) {
// (with format `@username`) of everyone who contributed to this version.
function extractContributors(entries) {
const set = Object.values(entries).reduce((memo, {__typename, author}) => {
if (__typename === "PullRequest" && author.__typename !== "Bot") {
memo.add("@" + author.login);
// Commit authors are *always* of type "User", so have to discriminate some
// other way. Making the assumption of a suffix for now, see how that goes.
} else if (__typename === "Commit" && !author.user.login.endsWith("-bot")) {
memo.add("@" + author.user.login);
if (!author) {
// author can be null if GH doesn't recognize them
return memo;
}
return memo;
switch (__typename) {
case "PullRequest":
if (author.__typename !== "Bot") {
memo.add("@" + author.login);
}
return memo;
case "Commit":
// Commit authors are *always* of type "User", so have to discriminate some
// other way. Making the assumption of a suffix for now, see how that goes.
// author.user can be nil if GH doesn't recognize the email
if (!author.user || author.user.login.endsWith("-bot")) {
return memo;
}
memo.add("@" + author.user.login);
return memo;
default:
throw new Error(`got an unexpected type for extractContributors: ${__typename}`);
}
}, new Set());
return Array.from(set).sort((a, b) => a.localeCompare(b, "en", {sensitivity: "base"}));

View file

@ -1,27 +1,62 @@
"use strict";
// Usage: `node generate-config-doc.js DOC_REPO_PATH`
// Usage: `npm run generate:config:doc DOC_REPO_PATH`
//
// Example:
//
// ```sh
// node scripts/generate-config-doc.js ../thelounge.github.io/
// npm run generate:config:doc ../thelounge.github.io/
// ```
const {readFileSync, writeFileSync} = require("fs");
const colors = require("chalk");
const log = require("../server/log");
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();
}
const configContent = readFileSync(join(__dirname, "..", "defaults", "config.js"), "utf8");
const docRoot = process.argv[2];
if (!docRoot) {
log.error("Missing DOC_REPO_PATH. Pass the path to the cloned `thelounge.github.io` repo.");
process.exit(1);
}
const docPath = join(process.argv[2], "_includes", "config.js.md");
/** @type {string[]} */
const acc = [];
const extractedDoc = configContent
.replace(/https:\/\/thelounge\.chat\/docs/g, "/docs") // make links relative
.split("\n")
@ -37,7 +72,7 @@ const extractedDoc = configContent
}
return acc;
}, [])
}, acc)
.join("\n");
const infoBlockHeader = `<!--

View file

@ -1,3 +1,4 @@
#!/usr/bin/env node
"use strict";
const got = require("got");

1
scripts/git-hooks/pre-commit Executable file
View file

@ -0,0 +1 @@
yarn pretty-quick --staged

View file

@ -6,10 +6,12 @@ import crypto from "crypto";
import colors from "chalk";
import log from "./log";
import Chan, {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,10 +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";
type OrderItem = Chan["id"] | Network["uuid"];
type Order = OrderItem[];
import {MessageStorage} from "./plugins/messageStorage/types";
import {StorageCleaner} from "./storageCleaner";
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",
@ -81,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;
@ -97,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;
@ -112,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,
@ -138,6 +133,15 @@ class Client {
if (!Config.values.public && client.config.log) {
if (Config.values.messageStorage.includes("sqlite")) {
client.messageProvider = new SqliteMessageStorage(client.name);
if (Config.values.storagePolicy.enabled) {
log.info(
`Activating storage cleaner. Policy: ${Config.values.storagePolicy.deletionPolicy}. MaxAge: ${Config.values.storagePolicy.maxAgeDays} days`
);
const cleaner = new StorageCleaner(client.messageProvider);
cleaner.start();
}
client.messageStorage.push(client.messageProvider);
}
@ -219,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);
}
}
@ -251,11 +258,13 @@ class Client {
let channels: Chan[] = [];
if (Array.isArray(args.channels)) {
let badName = false;
let badChanConf = false;
args.channels.forEach((chan: Chan) => {
if (!chan.name) {
badName = true;
args.channels.forEach((chan: ChanConfig) => {
const type = ChanType[(chan.type || "channel").toUpperCase()];
if (!chan.name || !type) {
badChanConf = true;
return;
}
@ -263,13 +272,13 @@ class Client {
client.createChannel({
name: chan.name,
key: chan.key || "",
type: chan.type,
type: type,
muted: chan.muted,
})
);
});
if (badName && client.name) {
if (badChanConf && client.name) {
log.warn(
"User '" +
client.name +
@ -339,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)) {
@ -685,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}) {
@ -764,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) {
@ -824,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) {
@ -275,7 +272,7 @@ class ClientManager {
return true;
}
private readUserConfig(name: string) {
readUserConfig(name: string) {
const userPath = Config.getUserConfigPath(name);
if (!fs.existsSync(userPath)) {
@ -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

@ -42,6 +42,7 @@ program.addCommand(require("./install").default);
program.addCommand(require("./uninstall").default);
program.addCommand(require("./upgrade").default);
program.addCommand(require("./outdated").default);
program.addCommand(require("./storage").default);
if (!Config.values.public) {
require("./users").default.forEach((command: Command) => {

View file

@ -74,7 +74,9 @@ program
if (
json.thelounge.supports &&
!semver.satisfies(Helper.getVersionNumber(), json.thelounge.supports)
!semver.satisfies(Helper.getVersionNumber(), json.thelounge.supports, {
includePrerelease: true,
})
) {
log.error(
`${colors.red(

View file

@ -0,0 +1,123 @@
import log from "../log";
import {Command} from "commander";
import ClientManager from "../clientManager";
import Utils from "./utils";
import SqliteMessageStorage from "../plugins/messageStorage/sqlite";
import {StorageCleaner} from "../storageCleaner";
const program = new Command("storage").description(
"various utilities related to the message storage"
);
program
.command("migrate")
.argument("[username]", "migrate a specific user only, all if not provided")
.description("Migrate message storage where needed")
.on("--help", Utils.extraHelp)
.action(function (user) {
runMigrations(user).catch((err) => {
log.error(err.toString());
process.exit(1);
});
});
program
.command("clean")
.argument("[user]", "clean messages for a specific user only, all if not provided")
.description("Delete messages from the DB based on the storage policy")
.on("--help", Utils.extraHelp)
.action(function (user) {
runCleaning(user).catch((err) => {
log.error(err.toString());
process.exit(1);
});
});
async function runMigrations(user?: string) {
const manager = new ClientManager();
const users = manager.getUsers();
if (user) {
if (!users.includes(user)) {
throw new Error(`invalid user ${user}`);
}
return migrateUser(manager, user);
}
for (const name of users) {
await migrateUser(manager, name);
// if any migration fails we blow up,
// chances are the rest won't complete either
}
}
// runs sqlite migrations for a user, which must exist
async function migrateUser(manager: ClientManager, user: string) {
log.info("handling user", user);
if (!isUserLogEnabled(manager, user)) {
log.info("logging disabled for user", user, ". Skipping");
return;
}
const sqlite = new SqliteMessageStorage(user);
await sqlite.enable(); // enable runs migrations
await sqlite.close();
log.info("user", user, "migrated successfully");
}
function isUserLogEnabled(manager: ClientManager, user: string): boolean {
const conf = manager.readUserConfig(user);
if (!conf) {
log.error("Could not open user configuration of", user);
return false;
}
return conf.log;
}
async function runCleaning(user: string) {
const manager = new ClientManager();
const users = manager.getUsers();
if (user) {
if (!users.includes(user)) {
throw new Error(`invalid user ${user}`);
}
return cleanUser(manager, user);
}
for (const name of users) {
await cleanUser(manager, name);
// if any migration fails we blow up,
// chances are the rest won't complete either
}
}
async function cleanUser(manager: ClientManager, user: string) {
log.info("handling user", user);
if (!isUserLogEnabled(manager, user)) {
log.info("logging disabled for user", user, ". Skipping");
return;
}
const sqlite = new SqliteMessageStorage(user);
await sqlite.enable();
const cleaner = new StorageCleaner(sqlite);
const num_deleted = await cleaner.runDeletesNoLimit();
log.info(`deleted ${num_deleted} messages`);
log.info("running a vacuum now, this might take a while");
if (num_deleted > 0) {
await sqlite.vacuum();
}
await sqlite.close();
log.info(`cleaning messages for ${user} has been successful`);
}
export default program;

View file

@ -143,11 +143,11 @@ class Utils {
data.toString()
.trim()
.split("\n")
.forEach((line) => {
.forEach((line: string) => {
try {
line = JSON.parse(line);
const json = JSON.parse(line);
if (line.type === "success") {
if (json.type === "success") {
success = true;
}
} catch (e: any) {
@ -163,11 +163,26 @@ class Utils {
.trim()
.split("\n")
.forEach((line: string) => {
const json = JSON.parse(line);
try {
const json = JSON.parse(line);
if (json.type === "error") {
log.error(json.data);
switch (json.type) {
case "error":
log.error(json.data);
break;
case "warning":
// this includes pointless things like "ignored scripts due to flag"
// so let's hide it
break;
}
return;
} catch (e: any) {
// we simply fall through and log at debug... chances are there's nothing the user can do about it
// as it includes things like deprecation warnings, but we might want to know as developers
}
log.debug(line);
});
});

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 = {
@ -76,6 +77,12 @@ type Debug = {
raw: boolean;
};
type StoragePolicy = {
enabled: boolean;
maxAgeDays: number;
deletionPolicy: "statusOnly" | "everything";
};
export type ConfigType = {
public: boolean;
host: string | undefined;
@ -97,6 +104,7 @@ export type ConfigType = {
defaults: Defaults;
lockNetwork: boolean;
messageStorage: string[];
storagePolicy: StoragePolicy;
useHexIp: boolean;
webirc?: WebIRC;
identd: Identd;

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[];
@ -441,10 +434,8 @@ class Network {
}
if (this.irc) {
const connected = this.irc.connection && this.irc.connection.connected;
if (this.nick !== oldNick) {
if (connected) {
if (this.irc.connected) {
// Send new nick straight away
this.irc.changeNick(this.nick);
} else {
@ -459,7 +450,7 @@ class Network {
}
if (
connected &&
this.irc.connected &&
this.realname !== oldRealname &&
this.irc.network.cap.isEnabled("setname")
) {
@ -507,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

@ -3,6 +3,8 @@ import colors from "chalk";
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
@ -11,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();
@ -51,6 +39,7 @@ async function fetch() {
Accept: "application/vnd.github.v3.html", // Request rendered markdown
"User-Agent": pkg.name + "; +" + pkg.repository.url, // Identify the client
},
localAddress: Config.values.bind,
});
if (response.statusCode !== 200) {

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"];
@ -32,6 +33,8 @@ const input: PluginInputHandler = function ({irc}, chan, cmd, args) {
irc.action(chan.name, text);
// If the IRCd does not support echo-message, simulate the message
// being sent back to us.
if (!irc.network.cap.isEnabled("echo-message")) {
irc.emit("action", {
nick: irc.user.nick,

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;
@ -15,7 +16,7 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
return;
}
if (irc.connection && irc.connection.connected) {
if (irc.connected) {
chan.pushMessage(
this,
new Msg({

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"];
@ -93,12 +95,14 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
network.irc.say(targetName, msg);
// If the IRCd does not support echo-message, simulate the message
// 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;
@ -47,7 +48,7 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
// If connected to IRC, send to server and wait for ACK
// otherwise update the nick and UI straight away
if (network.irc) {
if (network.irc.connection && network.irc.connection.connected) {
if (network.irc.connected) {
network.irc.changeNick(newNick);
return;

View file

@ -12,6 +12,8 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
network.irc.notice(targetName, message);
// If the IRCd does not support echo-message, simulate the message
// being sent back to us.
if (!network.irc.network.cap.isEnabled("echo-message")) {
let targetGroup;
const parsedTarget = network.irc.network.extractTargetGroup(targetName);

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;
@ -36,9 +37,7 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
if (
target.type !== ChanType.CHANNEL ||
target.state === ChanState.PARTED ||
!network.irc ||
!network.irc.connection ||
!network.irc.connection.connected
!network.irc.connected
) {
this.part(network, target);
} else {

View file

@ -4,7 +4,7 @@ const commands = ["raw", "send", "quote"];
const input: PluginInputHandler = function ({irc}, chan, cmd, args) {
if (args.length !== 0) {
irc.connection.write(args.join(" "));
irc.raw(...args);
}
return 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: () =>
@ -12,7 +13,7 @@ const ctcpResponses = {
.join(" "),
PING: ({message}: {message: string}) => message.substring(5),
SOURCE: () => pkg.repository.url,
VERSION: () => pkg.name + " " + Helper.getVersion() + " -- " + pkg.homepage,
VERSION: () => pkg.name + " -- " + pkg.homepage,
};
export default <IrcEventHandler>function (irc, network) {
@ -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,
});

Some files were not shown because too many files have changed in this diff Show more