Compare commits

...

130 commits

Author SHA1 Message Date
renovate[bot] eed3c0efd8
chore(deps): lock file maintenance 2024-04-27 10:52:55 +00: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 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
Nachtalb daabb76781
Add shorcut to navigate between channels with undread msgs 2024-01-27 22:50:10 +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
112 changed files with 3576 additions and 2757 deletions

View file

@ -12,19 +12,19 @@ jobs:
strategy:
matrix:
include:
# EOL: April 2024
- os: ubuntu-latest
node_version: 16.x
# EOL: April 2025
- os: macOS-latest
node_version: 18.x
- os: windows-latest
node_version: 18.x
# EOL: April 2025
- 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 }}

View file

@ -20,6 +20,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "latest"
registry-url: "https://registry.npmjs.org/"
- name: Install
@ -33,9 +34,6 @@ jobs:
- name: Test
run: yarn test
- name: Update npm
run: npm install -g npm
- name: Publish latest
if: "!contains(github.ref, '-')"
run: npm publish --tag latest --provenance

View file

@ -4,6 +4,133 @@ 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

View file

@ -59,7 +59,7 @@
<script lang="ts">
import {filter as fuzzyFilter} from "fuzzy";
import {computed, defineComponent, nextTick, PropType, ref} from "vue";
import type {UserInMessage} from "../../server/models/msg";
import type {UserInMessage} from "../../shared/types/msg";
import type {ClientChan, ClientUser} from "../js/types";
import Username from "./Username.vue";
@ -104,7 +104,7 @@ export default defineComponent({
const result = filteredUsers.value;
for (const user of result) {
const mode = user.original.modes[0] || "";
const mode: string = user.original.modes[0] || "";
if (!groups[mode]) {
groups[mode] = [];

View file

@ -41,9 +41,9 @@
<script lang="ts">
import Mousetrap from "mousetrap";
import {computed, defineComponent, ref, watch} from "vue";
import {onBeforeRouteLeave, onBeforeRouteUpdate} from "vue-router";
import eventbus from "../js/eventbus";
import {ClientChan, ClientMessage, ClientLinkPreview} from "../js/types";
import {ClientChan, ClientLinkPreview} from "../js/types";
import {SharedMsg} from "../../shared/types/msg";
export default defineComponent({
name: "ImageViewer",
@ -104,9 +104,9 @@ export default defineComponent({
}
const links = channel.value.messages
.map((msg) => msg.previews)
.map((msg: SharedMsg) => msg.previews)
.flat()
.filter((preview) => preview.thumb);
.filter((preview) => preview && preview.thumb);
const currentIndex = links.indexOf(link.value);

View file

@ -150,10 +150,14 @@ export default defineComponent({
});
const messageComponent = computed(() => {
return "message-" + props.message.type;
return "message-" + (props.message.type || "invalid"); // TODO: force existence of type in sharedmsg
});
const isAction = () => {
if (!props.message.type) {
return false;
}
return typeof MessageTypes["message-" + props.message.type] !== "undefined";
};

View file

@ -79,7 +79,7 @@ import {
} from "vue";
import {useStore} from "../js/store";
import {ClientChan, ClientMessage, ClientNetwork, ClientLinkPreview} from "../js/types";
import Msg from "../../server/models/msg";
import {SharedMsg} from "../../shared/types/msg";
type CondensedMessageContainer = {
type: "condensed";
@ -242,7 +242,7 @@ export default defineComponent({
});
const shouldDisplayDateMarker = (
message: Msg | ClientMessage | CondensedMessageContainer,
message: SharedMsg | ClientMessage | CondensedMessageContainer,
id: number
) => {
const previousMessage = condensedMessages.value[id - 1];
@ -270,7 +270,7 @@ export default defineComponent({
return false;
};
const isPreviousSource = (currentMessage: ClientMessage | Msg, id: number) => {
const isPreviousSource = (currentMessage: ClientMessage | SharedMsg, id: number) => {
const previousMessage = condensedMessages.value[id - 1];
return !!(
previousMessage &&

View file

@ -26,36 +26,43 @@ export default defineComponent({
},
setup(props) {
const errorMessage = computed(() => {
// TODO: enforce chan and nick fields so that we can get rid of that
const chan = props.message.channel || "!UNKNOWN_CHAN";
const nick = props.message.nick || "!UNKNOWN_NICK";
switch (props.message.error) {
case "bad_channel_key":
return `Cannot join ${props.message.channel} - Bad channel key.`;
return `Cannot join ${chan} - Bad channel key.`;
case "banned_from_channel":
return `Cannot join ${props.message.channel} - You have been banned from the channel.`;
return `Cannot join ${chan} - You have been banned from the channel.`;
case "cannot_send_to_channel":
return `Cannot send to channel ${props.message.channel}`;
return `Cannot send to channel ${chan}`;
case "channel_is_full":
return `Cannot join ${props.message.channel} - Channel is full.`;
return `Cannot join ${chan} - Channel is full.`;
case "chanop_privs_needed":
return "Cannot perform action: You're not a channel operator.";
case "invite_only_channel":
return `Cannot join ${props.message.channel} - Channel is invite only.`;
return `Cannot join ${chan} - Channel is invite only.`;
case "no_such_nick":
return `User ${props.message.nick} hasn't logged in or does not exist.`;
return `User ${nick} hasn't logged in or does not exist.`;
case "not_on_channel":
return "Cannot perform action: You're not on the channel.";
case "password_mismatch":
return "Password mismatch.";
case "too_many_channels":
return `Cannot join ${props.message.channel} - You've already reached the maximum number of channels allowed.`;
return `Cannot join ${chan} - You've already reached the maximum number of channels allowed.`;
case "unknown_command":
return `Unknown command: ${props.message.command}`;
// TODO: not having message.command should never happen, so force existence
return `Unknown command: ${props.message.command || "!UNDEFINED_COMMAND_BUG"}`;
case "user_not_in_channel":
return `User ${props.message.nick} is not on the channel.`;
return `User ${nick} is not on the channel.`;
case "user_on_channel":
return `User ${props.message.nick} is already on the channel.`;
return `User ${nick} is already on the channel.`;
default:
if (props.message.reason) {
return `${props.message.reason} (${props.message.error})`;
return `${props.message.reason} (${
props.message.error || "!UNDEFINED_ERR"
})`;
}
return props.message.error;

View file

@ -498,6 +498,7 @@ export default defineComponent({
};
watch(
// eslint-disable-next-line
() => props.defaults?.commands,
() => {
void nextTick(() => {
@ -507,6 +508,7 @@ export default defineComponent({
);
watch(
// eslint-disable-next-line
() => props.defaults?.tls,
(isSecureChecked) => {
const ports = [6667, 6697];

View file

@ -309,8 +309,7 @@ export default defineComponent({
moveItemInArray(store.state.networks, oldIndex, newIndex);
socket.emit("sort", {
type: "networks",
socket.emit("sort:networks", {
order: store.state.networks.map((n) => n.uuid),
});
};
@ -341,9 +340,8 @@ export default defineComponent({
moveItemInArray(netChan.network.channels, oldIndex, newIndex);
socket.emit("sort", {
type: "channels",
target: netChan.network.uuid,
socket.emit("sort:channel", {
network: netChan.network.uuid,
order: netChan.network.channels.map((c) => c.id),
});
};

View file

@ -12,10 +12,10 @@
<script lang="ts">
import {computed, defineComponent, PropType} from "vue";
import {UserInMessage} from "../../server/models/msg";
import {UserInMessage} from "../../shared/types/msg";
import eventbus from "../js/eventbus";
import colorClass from "../js/helpers/colorClass";
import type {ClientChan, ClientNetwork, ClientUser} from "../js/types";
import type {ClientChan, ClientNetwork} from "../js/types";
import {useStore} from "../js/store";
type UsernameUser = Partial<UserInMessage> & {

View file

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

@ -106,7 +106,7 @@ import type {ClientMessage} from "../../js/types";
import {useStore} from "../../js/store";
import {useRoute, useRouter} from "vue-router";
import {switchToChannel} from "../../js/router";
import {SearchQuery} from "../../../server/plugins/messageStorage/types";
import {SearchQuery} from "../../../shared/types/storage";
export default defineComponent({
name: "SearchResults",

35
client/js/chan.ts Normal file
View file

@ -0,0 +1,35 @@
import {ClientChan, ClientMessage} from "./types";
import {SharedNetworkChan} from "../../shared/types/network";
import {SharedMsg} from "../../shared/types/msg";
export function toClientChan(shared: SharedNetworkChan): ClientChan {
const history: string[] = [""].concat(
shared.messages
.filter((m) => m.self && m.text && m.type === "message")
// TS is too stupid to see the nil guard on filter... so we monkey patch it
.map((m): string => (m.text ? m.text : ""))
.reverse()
.slice(0, 99)
);
// filter the unused vars
const {messages, totalMessages: _, ...props} = shared;
const channel: ClientChan = {
...props,
editTopic: false,
pendingMessage: "",
inputHistoryPosition: 0,
historyLoading: false,
scrolledToBottom: true,
users: [],
usersOutdated: shared.type === "channel" ? true : false,
moreHistoryAvailable: shared.totalMessages > shared.messages.length,
inputHistory: history,
messages: sharedMsgToClientMsg(messages),
};
return channel;
}
function sharedMsgToClientMsg(shared: SharedMsg[]): ClientMessage[] {
// TODO: this is a stub for now, we will want to populate client specific stuff here
return shared;
}

View file

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

View file

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

View file

@ -83,6 +83,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

@ -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 type {ClientChan, ClientMessage} from "../types";
socket.on("more", async (data) => {
const channel = store.getters.findChannel(data.chan)?.channel;
@ -14,13 +14,15 @@ socket.on("more", async (data) => {
channel.inputHistory = channel.inputHistory.concat(
data.messages
.filter((m) => m.self && m.text && m.type === "message")
.map((m) => m.text)
// TS is too stupid to see the guard in .filter(), so we monkey patch it
// to please the compiler
.map((m) => (m.text ? m.text : ""))
.reverse()
.slice(0, 100 - channel.inputHistory.length)
);
channel.moreHistoryAvailable =
data.totalMessages > channel.messages.length + data.messages.length;
channel.messages.unshift(...(data.messages as ClientMessage[]));
channel.messages.unshift(...data.messages);
await nextTick();
channel.historyLoading = false;

View file

@ -3,7 +3,8 @@ import socket from "../socket";
import {cleanIrcMessage} from "../../../shared/irc";
import {store} from "../store";
import {switchToChannel} from "../router";
import {ClientChan, ClientMention, ClientMessage, NetChan} from "../types";
import {ClientChan, NetChan, ClientMessage} from "../types";
import {SharedMsg} from "../../../shared/types/msg";
let pop;
@ -95,6 +96,14 @@ socket.on("msg", function (data) {
}
});
declare global {
// this extends the interface from lib.dom with additional stuff which is not
// exactly standard but implemented in some browsers
interface NotificationOptions {
timestamp?: number; // chrome has it, other browsers ignore it
}
}
function notifyMessage(
targetId: number,
channel: ClientChan,
@ -122,12 +131,14 @@ function notifyMessage(
) {
let title: string;
let body: string;
// TODO: fix msg type and get rid of that conditional
const nick = msg.from && msg.from.nick ? msg.from.nick : "unkonown";
if (msg.type === "invite") {
title = "New channel invite:";
body = msg.from.nick + " invited you to " + msg.channel;
body = nick + " invited you to " + msg.channel;
} else {
title = String(msg.from.nick);
title = nick;
if (channel.type !== "query") {
title += ` (${channel.name})`;
@ -137,7 +148,8 @@ function notifyMessage(
title += " says:";
}
body = cleanIrcMessage(msg.text);
// TODO: fix msg type and get rid of that conditional
body = cleanIrcMessage(msg.text ? msg.text : "");
}
const timestamp = Date.parse(String(msg.time));
@ -184,24 +196,40 @@ function notifyMessage(
}
}
function updateUserList(channel, msg) {
if (msg.type === "message" || msg.type === "action") {
const user = channel.users.find((u) => u.nick === msg.from.nick);
function updateUserList(channel: ClientChan, msg: SharedMsg) {
switch (msg.type) {
case "message": // fallthrough
if (user) {
user.lastMessage = new Date(msg.time).getTime() || Date.now();
case "action": {
const user = channel.users.find((u) => u.nick === msg.from?.nick);
if (user) {
user.lastMessage = new Date(msg.time).getTime() || Date.now();
}
break;
}
} else if (msg.type === "quit" || msg.type === "part") {
const idx = channel.users.findIndex((u) => u.nick === msg.from.nick);
if (idx > -1) {
channel.users.splice(idx, 1);
case "quit": // fallthrough
case "part": {
const idx = channel.users.findIndex((u) => u.nick === msg.from?.nick);
if (idx > -1) {
channel.users.splice(idx, 1);
}
break;
}
} else if (msg.type === "kick") {
const idx = channel.users.findIndex((u) => u.nick === msg.target.nick);
if (idx > -1) {
channel.users.splice(idx, 1);
case "kick": {
const idx = channel.users.findIndex((u) => u.nick === msg.target?.nick);
if (idx > -1) {
channel.users.splice(idx, 1);
}
break;
}
}
}

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,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,5 +1,5 @@
import io, {Socket} from "socket.io-client";
import type {ServerToClientEvents, ClientToServerEvents} from "../../server/types/socket-events";
import type {ServerToClientEvents, ClientToServerEvents} from "../../shared/types/socket-events";
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io({
transports: JSON.parse(document.body.dataset.transports || "['polling', 'websocket']"),

View file

@ -3,19 +3,12 @@
import {ActionContext, createStore, Store, useStore as baseUseStore} from "vuex";
import {createSettingsStore} from "./store-settings";
import storage from "./localStorage";
import type {
ClientChan,
ClientConfiguration,
ClientNetwork,
InitClientChan,
NetChan,
ClientMessage,
ClientMention,
} from "./types";
import type {ClientChan, ClientNetwork, NetChan, ClientMention, ClientMessage} from "./types";
import type {InjectionKey} from "vue";
import {SettingsState} from "./settings";
import {SearchQuery} from "../../server/plugins/messageStorage/types";
import {SearchQuery} from "../../shared/types/storage";
import {SharedConfiguration, LockedSharedConfiguration} from "../../shared/types/config";
const appName = document.title;
@ -59,7 +52,7 @@ export type State = {
mentions: ClientMention[];
hasServiceWorker: boolean;
pushNotificationState: string;
serverConfiguration: ClientConfiguration | null;
serverConfiguration: SharedConfiguration | LockedSharedConfiguration | null;
sessions: ClientSession[];
sidebarOpen: boolean;
sidebarDragging: boolean;
@ -131,7 +124,6 @@ type Getters = {
findNetwork: (state: State) => (uuid: string) => ClientNetwork | null;
highlightCount(state: State): number;
title(state: State, getters: Omit<Getters, "title">): string;
initChannel: () => (channel: InitClientChan) => ClientChan;
};
// getters without the state argument
@ -202,31 +194,6 @@ const getters: Getters = {
return alertEventCount + channelname + appName;
},
initChannel: () => (channel: InitClientChan) => {
// TODO: This should be a mutation
channel.pendingMessage = "";
channel.inputHistoryPosition = 0;
channel.inputHistory = [""].concat(
channel.messages
.filter((m) => m.self && m.text && m.type === "message")
.map((m) => m.text)
.reverse()
.slice(0, 99)
);
channel.historyLoading = false;
channel.scrolledToBottom = true;
channel.editTopic = false;
channel.moreHistoryAvailable = channel.totalMessages! > channel.messages.length;
delete channel.totalMessages;
if (channel.type === "channel") {
channel.usersOutdated = true;
}
return channel as ClientChan;
},
};
type Mutations = {

36
client/js/types.d.ts vendored
View file

@ -1,12 +1,11 @@
import {defineComponent} from "vue";
import Chan from "../../server/models/chan";
import Network from "../../server/models/network";
import User from "../../server/models/user";
import Message from "../../server/models/msg";
import {Mention} from "../../server/client";
import {ClientConfiguration} from "../../server/server";
import {LinkPreview} from "../../server/plugins/irc-events/link";
import {SharedChan} from "../../shared/types/chan";
import {SharedNetwork} from "../../shared/types/network";
import {SharedUser} from "../../shared/types/user";
import {SharedMention} from "../../shared/types/mention";
import {SharedConfiguration, LockedSharedConfiguration} from "../../shared/types/config";
import {LinkPreview, SharedMsg} from "../../shared/types/msg";
interface LoungeWindow extends Window {
g_TheLoungeRemoveLoading?: () => void;
@ -16,19 +15,15 @@ interface LoungeWindow extends Window {
};
}
type ClientUser = User & {
//
};
type ClientUser = SharedUser;
type ClientMessage = Omit<Message, "users"> & {
time: number;
users: string[];
};
// we will eventually need to put client specific fields here
// which are not shared with the server
export type ClientMessage = SharedMsg;
type ClientChan = Omit<Chan, "users" | "messages"> & {
type ClientChan = Omit<SharedChan, "messages"> & {
moreHistoryAvailable: boolean;
editTopic: boolean;
users: ClientUser[];
messages: ClientMessage[];
// these are added in store/initChannel
@ -38,6 +33,8 @@ type ClientChan = Omit<Chan, "users" | "messages"> & {
historyLoading: boolean;
scrolledToBottom: boolean;
usersOutdated: boolean;
users: ClientUser[];
};
type InitClientChan = ClientChan & {
@ -46,7 +43,7 @@ type InitClientChan = ClientChan & {
};
// We omit channels so we can use ClientChan[] instead of Chan[]
type ClientNetwork = Omit<Network, "channels"> & {
type ClientNetwork = Omit<SharedNetwork, "channels"> & {
isJoinChannelShown: boolean;
isCollapsed: boolean;
channels: ClientChan[];
@ -57,9 +54,8 @@ type NetChan = {
network: ClientNetwork;
};
type ClientConfiguration = ClientConfiguration;
type ClientMention = Mention & {
localetime: string;
type ClientMention = SharedMention & {
localetime: string; // TODO: this needs to go the way of the dodo, nothing but a single component uses it
channel: NetChan | null;
};

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

View file

@ -6,40 +6,8 @@
] /* Specifies a list of glob patterns that match files to be included in compilation. If no 'files' or 'include' property is present in a tsconfig.json, the compiler defaults to including all files in the containing directory and subdirectories except those specified by 'exclude'. Requires TypeScript version 2.0 or later. */,
"files": [
"../package.json",
"../server/types/socket-events.d.ts",
"../server/helper.ts",
"../server/log.ts",
"../server/config.ts",
"../server/client.ts",
"../server/storageCleaner.ts",
"../server/clientManager.ts",
"../server/identification.ts",
"../server/plugins/changelog.ts",
"../server/plugins/uploader.ts",
"../server/plugins/storage.ts",
"../server/plugins/inputs/index.ts",
"../server/plugins/messageStorage/sqlite.ts",
"../server/plugins/messageStorage/text.ts",
"../server/plugins/packages/index.ts",
"../server/plugins/packages/publicClient.ts",
"../server/plugins/packages/themes.ts",
"../server/plugins/dev-server.ts",
"../server/plugins/webpush.ts",
"../server/plugins/sts.ts",
"../server/plugins/clientCertificate.ts",
"../server/plugins/auth.ts",
"../server/plugins/auth/local.ts",
"../server/plugins/auth/ldap.ts",
"../server/plugins/irc-events/link.ts",
"../server/command-line/utils.ts",
"../server/models/network.ts",
"../server/models/user.ts",
"../server/models/msg.ts",
"../server/models/prefix.ts",
"./js/helpers/fullnamemap.json",
"./js/helpers/simplemap.json",
"../webpack.config.ts",
"../babel.config.cjs"
"./js/helpers/simplemap.json"
] /* If no 'files' or 'include' property is present in a tsconfig.json, the compiler defaults to including all files in the containing directory and subdirectories except those specified by 'exclude'. When a 'files' property is specified, only those files and those specified by 'include' are included. */,
// "exclude": [],
"compilerOptions": {

View file

@ -1,7 +1,7 @@
{
"name": "thelounge",
"description": "The self-hosted Web IRC client",
"version": "4.4.1",
"version": "4.4.3",
"preferGlobal": true,
"bin": {
"thelounge": "index.js"
@ -43,7 +43,7 @@
],
"license": "MIT",
"engines": {
"node": ">=16.0.0"
"node": ">=18.0.0"
},
"files": [
"./.thelounge_home",
@ -61,7 +61,7 @@
"cheerio": "1.0.0-rc.12",
"commander": "9.0.0",
"content-disposition": "0.5.4",
"express": "4.17.3",
"express": "4.19.2",
"file-type": "16.5.4",
"filenamify": "4.3.0",
"got": "11.8.5",
@ -98,20 +98,20 @@
"@types/chai": "4.3.5",
"@types/cheerio": "0.22.35",
"@types/content-disposition": "0.5.8",
"@types/express": "4.17.13",
"@types/express": "4.17.21",
"@types/is-utf8": "0.2.3",
"@types/ldapjs": "2.2.2",
"@types/linkify-it": "3.0.5",
"@types/lodash": "4.14.200",
"@types/lodash": "4.14.202",
"@types/mime-types": "2.1.1",
"@types/mocha": "9.1.1",
"@types/mousetrap": "1.6.13",
"@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",
@ -167,7 +167,7 @@
"vuex": "4.0.2",
"webpack": "5.76.0",
"webpack-cli": "4.9.2",
"webpack-dev-middleware": "5.3.3",
"webpack-dev-middleware": "5.3.4",
"webpack-hot-middleware": "2.25.4"
}
}

View file

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

@ -10,10 +10,35 @@
const {readFileSync, writeFileSync} = require("fs");
const colors = require("chalk");
const log = require("../server/log").default;
const {join} = require("path");
const {spawnSync} = require("child_process");
function timestamp() {
const datetime = new Date().toISOString().split(".")[0].replace("T", " ");
return colors.dim(datetime);
}
const log = {
/* eslint-disable no-console */
error(...args) {
console.error(timestamp(), colors.red("[ERROR]"), ...args);
},
warn(...args) {
console.error(timestamp(), colors.yellow("[WARN]"), ...args);
},
info(...args) {
console.log(timestamp(), colors.blue("[INFO]"), ...args);
},
debug(...args) {
console.log(timestamp(), colors.green("[DEBUG]"), ...args);
},
raw(...args) {
console.log(...args);
},
/* eslint-enable no-console */
};
function getGitUsername() {
return spawnSync("git", ["config", "user.name"], {encoding: "utf8"}).stdout.trim();
}

View file

@ -6,10 +6,12 @@ import crypto from "crypto";
import colors from "chalk";
import log from "./log";
import Chan, {ChanConfig, Channel, ChanType} from "./models/chan";
import Msg, {MessageType, UserInMessage} from "./models/msg";
import Chan, {ChanConfig} from "./models/chan";
import Msg from "./models/msg";
import Config from "./config";
import {condensedTypes} from "../shared/irc";
import {MessageType} from "../shared/types/msg";
import {SharedMention} from "../shared/types/mention";
import inputs from "./plugins/inputs";
import PublicClient from "./plugins/packages/publicClient";
@ -17,11 +19,12 @@ import SqliteMessageStorage from "./plugins/messageStorage/sqlite";
import TextFileMessageStorage from "./plugins/messageStorage/text";
import Network, {IgnoreListItem, NetworkConfig, NetworkWithIrcFramework} from "./models/network";
import ClientManager from "./clientManager";
import {MessageStorage, SearchQuery, SearchResponse} from "./plugins/messageStorage/types";
import {MessageStorage} from "./plugins/messageStorage/types";
import {StorageCleaner} from "./storageCleaner";
type OrderItem = Chan["id"] | Network["uuid"];
type Order = OrderItem[];
import {SearchQuery, SearchResponse} from "../shared/types/storage";
import {SharedChan, ChanType} from "../shared/types/chan";
import {SharedNetwork} from "../shared/types/network";
import {ServerToClientEvents} from "../shared/types/socket-events";
const events = [
"away",
@ -82,15 +85,6 @@ export type UserConfig = {
networks?: NetworkConfig[];
};
export type Mention = {
chanId: number;
msgId: number;
type: MessageType;
time: Date;
text: string;
from: UserInMessage;
};
class Client {
awayMessage!: string;
lastActiveChannel!: number;
@ -98,12 +92,12 @@ class Client {
[socketId: string]: {token: string; openChannel: number};
};
config!: UserConfig;
id!: number;
id: string;
idMsg!: number;
idChan!: number;
name!: string;
networks!: Network[];
mentions!: Mention[];
mentions!: SharedMention[];
manager!: ClientManager;
messageStorage!: MessageStorage[];
highlightRegex!: RegExp | null;
@ -113,12 +107,12 @@ class Client {
fileHash!: string;
constructor(manager: ClientManager, name?: string, config = {} as UserConfig) {
this.id = uuidv4();
_.merge(this, {
awayMessage: "",
lastActiveChannel: -1,
attachedClients: {},
config: config,
id: uuidv4(),
idChan: 1,
idMsg: 1,
name: name,
@ -229,9 +223,12 @@ class Client {
return chan;
}
emit(event: string, data?: any) {
emit<Ev extends keyof ServerToClientEvents>(
event: Ev,
...args: Parameters<ServerToClientEvents[Ev]>
) {
if (this.manager !== null) {
this.manager.sockets.in(this.id.toString()).emit(event, data);
this.manager.sockets.in(this.id).emit(event, ...args);
}
}
@ -351,7 +348,7 @@ class Client {
client.networks.push(network);
client.emit("network", {
networks: [network.getFilteredClone(this.lastActiveChannel, -1)],
network: network.getFilteredClone(this.lastActiveChannel, -1),
});
if (!network.validate(client)) {
@ -697,56 +694,39 @@ class Client {
this.emit("open", targetNetChan.chan.id);
}
sort(data: {order: Order; type: "networks" | "channels"; target: string}) {
const order = data.order;
sortChannels(netid: SharedNetwork["uuid"], order: SharedChan["id"][]) {
const network = _.find(this.networks, {uuid: netid});
if (!_.isArray(order)) {
if (!network) {
return;
}
switch (data.type) {
case "networks":
this.networks.sort((a, b) => order.indexOf(a.uuid) - order.indexOf(b.uuid));
// Sync order to connected clients
this.emit("sync_sort", {
order: this.networks.map((obj) => obj.uuid),
type: data.type,
});
break;
case "channels": {
const network = _.find(this.networks, {uuid: data.target});
if (!network) {
return;
}
network.channels.sort((a, b) => {
// Always sort lobby to the top regardless of what the client has sent
// Because there's a lot of code that presumes channels[0] is the lobby
if (a.type === ChanType.LOBBY) {
return -1;
} else if (b.type === ChanType.LOBBY) {
return 1;
}
return order.indexOf(a.id) - order.indexOf(b.id);
});
// Sync order to connected clients
this.emit("sync_sort", {
order: network.channels.map((obj) => obj.id),
type: data.type,
target: network.uuid,
});
break;
network.channels.sort((a, b) => {
// Always sort lobby to the top regardless of what the client has sent
// Because there's a lot of code that presumes channels[0] is the lobby
if (a.type === ChanType.LOBBY) {
return -1;
} else if (b.type === ChanType.LOBBY) {
return 1;
}
}
return order.indexOf(a.id) - order.indexOf(b.id);
});
this.save();
// Sync order to connected clients
this.emit("sync_sort:channels", {
network: network.uuid,
order: network.channels.map((obj) => obj.id),
});
}
sortNetworks(order: SharedNetwork["uuid"][]) {
this.networks.sort((a, b) => order.indexOf(a.uuid) - order.indexOf(b.uuid));
this.save();
// Sync order to connected clients
this.emit("sync_sort:networks", {
order: this.networks.map((obj) => obj.uuid),
});
}
names(data: {target: number}) {
@ -776,7 +756,7 @@ class Client {
quit(signOut?: boolean) {
const sockets = this.manager.sockets.sockets;
const room = sockets.adapter.rooms.get(this.id.toString());
const room = sockets.adapter.rooms.get(this.id);
if (room) {
for (const user of room) {
@ -836,12 +816,13 @@ class Client {
}
// TODO: type session to this.attachedClients
registerPushSubscription(session: any, subscription: ClientPushSubscription, noSave = false) {
registerPushSubscription(session: any, subscription: PushSubscriptionJSON, noSave = false) {
if (
!_.isPlainObject(subscription) ||
!_.isPlainObject(subscription.keys) ||
typeof subscription.endpoint !== "string" ||
!/^https?:\/\//.test(subscription.endpoint) ||
!_.isPlainObject(subscription.keys) ||
!subscription.keys || // TS compiler doesn't understand isPlainObject
typeof subscription.keys.p256dh !== "string" ||
typeof subscription.keys.auth !== "string"
) {

View file

@ -10,7 +10,7 @@ import Config from "./config";
import {NetworkConfig} from "./models/network";
import WebPush from "./plugins/webpush";
import log from "./log";
import {Server} from "socket.io";
import {Server} from "./server";
class ClientManager {
clients: Client[];

View file

@ -4,6 +4,7 @@ import fs, {Stats} from "fs";
import os from "os";
import _ from "lodash";
import colors from "chalk";
import {SearchOptions} from "ldapjs";
import log from "./log";
import Helper from "./helper";
@ -44,7 +45,7 @@ export type Defaults = Pick<
| "saslAccount"
| "saslPassword"
> & {
join?: string;
join: string;
};
type Identd = {
@ -57,7 +58,7 @@ type SearchDN = {
rootPassword: string;
filter: string;
base: string;
scope: string;
scope: SearchOptions["scope"];
};
type Ldap = {

View file

@ -2,36 +2,14 @@ import _ from "lodash";
import log from "../log";
import Config from "../config";
import User from "./user";
import Msg, {MessageType} from "./msg";
import Msg from "./msg";
import storage from "../plugins/storage";
import Client from "../client";
import Network from "./network";
import Prefix from "./prefix";
export enum ChanType {
CHANNEL = "channel",
LOBBY = "lobby",
QUERY = "query",
SPECIAL = "special",
}
export enum SpecialChanType {
BANLIST = "list_bans",
INVITELIST = "list_invites",
CHANNELLIST = "list_channels",
IGNORELIST = "list_ignored",
}
export enum ChanState {
PARTED = 0,
JOINED = 1,
}
// eslint-disable-next-line no-use-before-define
export type FilteredChannel = Chan & {
users: [];
totalMessages: number;
};
import {MessageType, SharedMsg} from "../../shared/types/msg";
import {ChanType, SpecialChanType, ChanState} from "../../shared/types/chan";
import {SharedNetworkChan} from "../../shared/types/network";
export type ChanConfig = {
name: string;
@ -60,7 +38,6 @@ class Chan {
data?: any;
closed?: boolean;
num_users?: number;
static optionalProperties = ["userAway", "special", "data", "closed", "num_users"];
constructor(attr?: Partial<Chan>) {
_.defaults(this, attr, {
@ -84,18 +61,11 @@ class Chan {
}
pushMessage(client: Client, msg: Msg, increasesUnread = false) {
const chan = this.id;
const obj = {chan, msg} as {
chan: number;
msg: Msg;
unread?: number;
highlight?: number;
};
const chanId = this.id;
msg.id = client.idMsg++;
// If this channel is open in any of the clients, do not increase unread counter
const isOpen = _.find(client.attachedClients, {openChannel: chan}) !== undefined;
const isOpen = _.find(client.attachedClients, {openChannel: chanId}) !== undefined;
if (msg.self) {
// reset counters/markers when receiving self-/echo-message
@ -108,15 +78,15 @@ class Chan {
}
if (increasesUnread || msg.highlight) {
obj.unread = ++this.unread;
this.unread++;
}
if (msg.highlight) {
obj.highlight = ++this.highlight;
this.highlight++;
}
}
client.emit("msg", obj);
client.emit("msg", {chan: chanId, msg, unread: this.unread, highlight: this.highlight});
// Never store messages in public mode as the session
// is completely destroyed when the page gets closed
@ -144,7 +114,8 @@ class Chan {
}
}
}
dereferencePreviews(messages) {
dereferencePreviews(messages: Msg[]) {
if (!Config.values.prefetch || !Config.values.prefetchStorage) {
return;
}
@ -160,6 +131,7 @@ class Chan {
}
});
}
getSortedUsers(irc?: Network["irc"]) {
const users = Array.from(this.users.values());
@ -182,21 +154,27 @@ class Chan {
return userModeSortPriority[a.mode] - userModeSortPriority[b.mode];
});
}
findMessage(msgId: number) {
return this.messages.find((message) => message.id === msgId);
}
findUser(nick: string) {
return this.users.get(nick.toLowerCase());
}
getUser(nick: string) {
return this.findUser(nick) || new User({nick}, new Prefix([]));
}
setUser(user: User) {
this.users.set(user.nick.toLowerCase(), user);
}
removeUser(user: User) {
this.users.delete(user.nick.toLowerCase());
}
/**
* Get a clean clone of this channel that will be sent to the client.
* This function performs manual cloning of channel object for
@ -206,38 +184,54 @@ class Chan {
* If true, channel is assumed active.
* @param {int} lastMessage - Last message id seen by active client to avoid sending duplicates.
*/
getFilteredClone(lastActiveChannel?: number | boolean, lastMessage?: number): FilteredChannel {
return Object.keys(this).reduce((newChannel, prop) => {
if (Chan.optionalProperties.includes(prop)) {
if (this[prop] !== undefined || (Array.isArray(this[prop]) && this[prop].length)) {
newChannel[prop] = this[prop];
}
} else if (prop === "users") {
// Do not send users, client requests updated user list whenever needed
newChannel[prop] = [];
} else if (prop === "messages") {
// If client is reconnecting, only send new messages that client has not seen yet
if (lastMessage && lastMessage > -1) {
// When reconnecting, always send up to 100 messages to prevent message gaps on the client
// See https://github.com/thelounge/thelounge/issues/1883
newChannel[prop] = this[prop].filter((m) => m.id > lastMessage).slice(-100);
} else {
// If channel is active, send up to 100 last messages, for all others send just 1
// Client will automatically load more messages whenever needed based on last seen messages
const messagesToSend =
lastActiveChannel === true || this.id === lastActiveChannel ? 100 : 1;
getFilteredClone(
lastActiveChannel?: number | boolean,
lastMessage?: number
): SharedNetworkChan {
let msgs: SharedMsg[];
newChannel[prop] = this[prop].slice(-messagesToSend);
}
// If client is reconnecting, only send new messages that client has not seen yet
if (lastMessage && lastMessage > -1) {
// When reconnecting, always send up to 100 messages to prevent message gaps on the client
// See https://github.com/thelounge/thelounge/issues/1883
msgs = this.messages.filter((m) => m.id > lastMessage).slice(-100);
} else {
// If channel is active, send up to 100 last messages, for all others send just 1
// Client will automatically load more messages whenever needed based on last seen messages
const messagesToSend =
lastActiveChannel === true || this.id === lastActiveChannel ? 100 : 1;
msgs = this.messages.slice(-messagesToSend);
}
(newChannel as FilteredChannel).totalMessages = this[prop].length;
} else {
newChannel[prop] = this[prop];
}
return {
id: this.id,
messages: msgs,
totalMessages: this.messages.length,
name: this.name,
key: this.key,
topic: this.topic,
firstUnread: this.firstUnread,
unread: this.unread,
highlight: this.highlight,
muted: this.muted,
type: this.type,
state: this.state,
return newChannel;
}, {}) as FilteredChannel;
special: this.special,
data: this.data,
closed: this.closed,
num_users: this.num_users,
};
// TODO: funny array mutation below might need to be reproduced
// static optionalProperties = ["userAway", "special", "data", "closed", "num_users"];
// return Object.keys(this).reduce((newChannel, prop) => {
// if (Chan.optionalProperties.includes(prop)) {
// if (this[prop] !== undefined || (Array.isArray(this[prop]) && this[prop].length)) {
// newChannel[prop] = this[prop];
// }
// }
}
writeUserLog(client: Client, msg: Msg) {
this.messages.push(msg);
@ -270,6 +264,7 @@ class Chan {
messageStorage.index(target.network, targetChannel, msg).catch((e) => log.error(e));
}
}
loadMessages(client: Client, network: Network) {
if (!this.isLoggable()) {
return;
@ -326,15 +321,23 @@ class Chan {
log.error(`Failed to load messages for ${client.name}: ${err.toString()}`)
);
}
isLoggable() {
return this.type === ChanType.CHANNEL || this.type === ChanType.QUERY;
}
setMuteStatus(muted: boolean) {
this.muted = !!muted;
}
}
function requestZncPlayback(channel, network, from) {
function requestZncPlayback(channel: Chan, network: Network, from: number) {
if (!network.irc) {
throw new Error(
`requestZncPlayback: no irc field on network "${network.name}", this is a bug`
);
}
network.irc.raw("ZNC", "*playback", "PLAY", channel.name, from.toString());
}

View file

@ -1,41 +1,5 @@
import _ from "lodash";
import {LinkPreview} from "../plugins/irc-events/link";
import User from "./user";
export type UserInMessage = Partial<User> & {
mode: string;
};
export enum MessageType {
UNHANDLED = "unhandled",
ACTION = "action",
AWAY = "away",
BACK = "back",
ERROR = "error",
INVITE = "invite",
JOIN = "join",
KICK = "kick",
LOGIN = "login",
LOGOUT = "logout",
MESSAGE = "message",
MODE = "mode",
MODE_CHANNEL = "mode_channel",
MODE_USER = "mode_user", // RPL_UMODEIS
MONOSPACE_BLOCK = "monospace_block",
NICK = "nick",
NOTICE = "notice",
PART = "part",
QUIT = "quit",
CTCP = "ctcp",
CTCP_REQUEST = "ctcp_request",
CHGHOST = "chghost",
TOPIC = "topic",
TOPIC_SET_BY = "topic_set_by",
WHOIS = "whois",
RAW = "raw",
PLUGIN = "plugin",
WALLOPS = "wallops",
}
import {MessageType, LinkPreview, UserInMessage} from "../../shared/types/msg";
class Msg {
from!: UserInMessage;
@ -70,7 +34,7 @@ class Msg {
raw_modes!: any;
when!: Date;
whois!: any;
users!: UserInMessage[] | string[];
users!: string[];
statusmsgGroup!: string;
params!: string[];

View file

@ -1,24 +1,17 @@
import _ from "lodash";
import {v4 as uuidv4} from "uuid";
import IrcFramework, {Client as IRCClient} from "irc-framework";
import Chan, {ChanConfig, Channel, ChanType} from "./chan";
import Msg, {MessageType} from "./msg";
import Chan, {ChanConfig, Channel} from "./chan";
import Msg from "./msg";
import Prefix from "./prefix";
import Helper, {Hostmask} from "../helper";
import Config, {WebIRC} from "../config";
import STSPolicies from "../plugins/sts";
import ClientCertificate, {ClientCertificateType} from "../plugins/clientCertificate";
import Client from "../client";
/**
* List of keys which should be sent to the client by default.
*/
const fieldsForClient = {
uuid: true,
name: true,
nick: true,
serverOptions: true,
};
import {MessageType} from "../../shared/types/msg";
import {ChanType} from "../../shared/types/chan";
import {SharedNetwork} from "../../shared/types/network";
type NetworkIrcOptions = {
host: string;
@ -52,7 +45,7 @@ type NetworkStatus = {
};
export type IgnoreListItem = Hostmask & {
when?: number;
when: number;
};
type IgnoreList = IgnoreListItem[];
@ -505,24 +498,17 @@ class Network {
}
}
getFilteredClone(lastActiveChannel?: number, lastMessage?: number) {
const filteredNetwork = Object.keys(this).reduce((newNetwork, prop) => {
if (prop === "channels") {
// Channels objects perform their own cloning
newNetwork[prop] = this[prop].map((channel) =>
channel.getFilteredClone(lastActiveChannel, lastMessage)
);
} else if (fieldsForClient[prop]) {
// Some properties that are not useful for the client are skipped
newNetwork[prop] = this[prop];
}
return newNetwork;
}, {}) as Network;
filteredNetwork.status = this.getNetworkStatus();
return filteredNetwork;
getFilteredClone(lastActiveChannel?: number, lastMessage?: number): SharedNetwork {
return {
uuid: this.uuid,
name: this.name,
nick: this.nick,
serverOptions: this.serverOptions,
status: this.getNetworkStatus(),
channels: this.channels.map((channel) =>
channel.getFilteredClone(lastActiveChannel, lastMessage)
),
};
}
getNetworkStatus() {

View file

@ -67,11 +67,11 @@ function advancedLdapAuth(user: string, password: string, callback: (success: bo
});
const base = config.ldap.searchDN.base;
const searchOptions = {
const searchOptions: SearchOptions = {
scope: config.ldap.searchDN.scope,
filter: `(&(${config.ldap.primaryKey}=${userDN})${config.ldap.searchDN.filter})`,
attributes: ["dn"],
} as SearchOptions;
};
ldapclient.on("error", function (err: Error) {
log.error(`Unable to connect to LDAP server: ${err.toString()}`);
@ -178,12 +178,12 @@ function advancedLdapLoadUsers(users: string[], callbackLoadUser) {
const remainingUsers = new Set(users);
const searchOptions = {
const searchOptions: SearchOptions = {
scope: config.ldap.searchDN.scope,
filter: `${config.ldap.searchDN.filter}`,
attributes: [config.ldap.primaryKey],
paged: true,
} as SearchOptions;
};
ldapclient.search(base, searchOptions, function (err2, res) {
if (err2) {

View file

@ -4,6 +4,7 @@ import log from "../log";
import pkg from "../../package.json";
import ClientManager from "../clientManager";
import Config from "../config";
import {SharedChangelogData} from "../../shared/types/changelog";
const TIME_TO_LIVE = 15 * 60 * 1000; // 15 minutes, in milliseconds
@ -12,31 +13,17 @@ export default {
fetch,
checkForUpdates,
};
export type ChangelogData = {
current: {
prerelease: boolean;
version: string;
changelog?: string;
url: string;
};
expiresAt: number;
latest?: {
prerelease: boolean;
version: string;
url: string;
};
packages?: boolean;
};
const versions = {
const versions: SharedChangelogData = {
current: {
prerelease: false,
version: `v${pkg.version}`,
changelog: undefined,
url: "", // TODO: properly init
},
expiresAt: -1,
latest: undefined,
packages: undefined,
} as ChangelogData;
};
async function fetch() {
const time = Date.now();

View file

@ -31,7 +31,7 @@ function get(uuid: string): ClientCertificateType | null {
return {
private_key: fs.readFileSync(paths.privateKeyPath, "utf-8"),
certificate: fs.readFileSync(paths.certificatePath, "utf-8"),
} as ClientCertificateType;
};
} catch (e: any) {
log.error("Unable to get certificate", e);
}
@ -122,10 +122,10 @@ function generate() {
// Sign this certificate with a SHA256 signature
cert.sign(keys.privateKey, md.sha256.create());
const pem = {
const pem: ClientCertificateType = {
private_key: pki.privateKeyToPem(keys.privateKey),
certificate: pki.certificateToPem(cert),
} as ClientCertificateType;
};
return pem;
}

View file

@ -1,6 +1,7 @@
import {PluginInputHandler} from "./index";
import Msg, {MessageType} from "../../models/msg";
import {ChanType} from "../../models/chan";
import Msg from "../../models/msg";
import {MessageType} from "../../../shared/types/msg";
import {ChanType} from "../../../shared/types/chan";
const commands = ["slap", "me"];

View file

@ -1,6 +1,7 @@
import {ChanType} from "../../models/chan";
import Msg, {MessageType} from "../../models/msg";
import Msg from "../../models/msg";
import {PluginInputHandler} from "./index";
import {MessageType} from "../../../shared/types/msg";
import {ChanType} from "../../../shared/types/chan";
const commands = ["ban", "unban", "banlist", "kickban"];

View file

@ -1,5 +1,6 @@
import Msg, {MessageType} from "../../models/msg";
import Msg from "../../models/msg";
import {PluginInputHandler} from "./index";
import {MessageType} from "../../../shared/types/msg";
const commands = ["connect", "server"];
const allowDisconnected = true;

View file

@ -1,5 +1,6 @@
import Msg, {MessageType} from "../../models/msg";
import Msg from "../../models/msg";
import {PluginInputHandler} from "./index";
import {MessageType} from "../../../shared/types/msg";
const commands = ["ctcp"];

View file

@ -1,18 +1,14 @@
import Msg, {MessageType} from "../../models/msg";
import Msg from "../../models/msg";
import Helper from "../../helper";
import {PluginInputHandler} from "./index";
import {IgnoreListItem} from "../../models/network";
import {ChanType, SpecialChanType} from "../../models/chan";
import {MessageType} from "../../../shared/types/msg";
const commands = ["ignore", "unignore", "ignorelist"];
const commands = ["ignore", "unignore"];
const input: PluginInputHandler = function (network, chan, cmd, args) {
const client = this;
let target: string;
// let hostmask: cmd === "ignoreList" ? string : undefined;
let hostmask: IgnoreListItem | undefined;
if (cmd !== "ignorelist" && (args.length === 0 || args[0].trim().length === 0)) {
if (args.length === 0 || args[0].trim().length === 0) {
chan.pushMessage(
client,
new Msg({
@ -24,16 +20,13 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
return;
}
if (cmd !== "ignorelist") {
// Trim to remove any spaces from the hostmask
target = args[0].trim();
hostmask = Helper.parseHostmask(target) as IgnoreListItem;
}
const target = args[0].trim();
const hostmask = Helper.parseHostmask(target);
switch (cmd) {
case "ignore": {
// IRC nicks are case insensitive
if (hostmask!.nick.toLowerCase() === network.nick.toLowerCase()) {
if (hostmask.nick.toLowerCase() === network.nick.toLowerCase()) {
chan.pushMessage(
client,
new Msg({
@ -41,25 +34,14 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
text: "You can't ignore yourself",
})
);
} else if (
!network.ignoreList.some(function (entry) {
return Helper.compareHostmask(entry, hostmask!);
return;
}
if (
network.ignoreList.some(function (entry) {
return Helper.compareHostmask(entry, hostmask);
})
) {
hostmask!.when = Date.now();
network.ignoreList.push(hostmask!);
client.save();
chan.pushMessage(
client,
new Msg({
type: MessageType.ERROR,
text: `\u0002${hostmask!.nick}!${hostmask!.ident}@${
hostmask!.hostname
}\u000f added to ignorelist`,
})
);
} else {
chan.pushMessage(
client,
new Msg({
@ -67,32 +49,31 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
text: "The specified user/hostmask is already ignored",
})
);
return;
}
break;
network.ignoreList.push({
...hostmask,
when: Date.now(),
});
client.save();
chan.pushMessage(
client,
new Msg({
type: MessageType.ERROR, // TODO: Successfully added via type.Error 🤔 ?
text: `\u0002${hostmask.nick}!${hostmask.ident}@${hostmask.hostname}\u000f added to ignorelist`,
})
);
return;
}
case "unignore": {
const idx = network.ignoreList.findIndex(function (entry) {
return Helper.compareHostmask(entry, hostmask!);
return Helper.compareHostmask(entry, hostmask);
});
// Check if the entry exists before removing it, otherwise
// let the user know.
if (idx !== -1) {
network.ignoreList.splice(idx, 1);
client.save();
chan.pushMessage(
client,
new Msg({
type: MessageType.ERROR,
text: `Successfully removed \u0002${hostmask!.nick}!${hostmask!.ident}@${
hostmask!.hostname
}\u000f from ignorelist`,
})
);
} else {
if (idx === -1) {
chan.pushMessage(
client,
new Msg({
@ -100,52 +81,20 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
text: "The specified user/hostmask is not ignored",
})
);
return;
}
break;
network.ignoreList.splice(idx, 1);
client.save();
chan.pushMessage(
client,
new Msg({
type: MessageType.ERROR, // TODO: Successfully removed via type.Error 🤔 ?
text: `Successfully removed \u0002${hostmask.nick}!${hostmask.ident}@${hostmask.hostname}\u000f from ignorelist`,
})
);
}
case "ignorelist":
if (network.ignoreList.length === 0) {
chan.pushMessage(
client,
new Msg({
type: MessageType.ERROR,
text: "Ignorelist is empty",
})
);
} else {
const chanName = "Ignored users";
const ignored = network.ignoreList.map((data) => ({
hostmask: `${data.nick}!${data.ident}@${data.hostname}`,
when: data.when,
}));
let newChan = network.getChannel(chanName);
if (typeof newChan === "undefined") {
newChan = client.createChannel({
type: ChanType.SPECIAL,
special: SpecialChanType.IGNORELIST,
name: chanName,
data: ignored,
});
client.emit("join", {
network: network.uuid,
chan: newChan.getFilteredClone(true),
index: network.addChannel(newChan),
});
} else {
// TODO: add type for this chan/event
newChan.data = ignored;
client.emit("msg:special", {
chan: newChan.id,
data: ignored,
});
}
}
break;
}
};

View file

@ -0,0 +1,57 @@
import {PluginInputHandler} from "./index";
import Msg from "../../models/msg";
import {ChanType, SpecialChanType} from "../../../shared/types/chan";
import {MessageType} from "../../../shared/types/msg";
const commands = ["ignorelist"];
const input: PluginInputHandler = function (network, chan, _cmd, _args) {
const client = this;
if (network.ignoreList.length === 0) {
chan.pushMessage(
client,
new Msg({
type: MessageType.ERROR,
text: "Ignorelist is empty",
})
);
return;
}
const chanName = "Ignored users";
const ignored = network.ignoreList.map((data) => ({
hostmask: `${data.nick}!${data.ident}@${data.hostname}`,
when: data.when,
}));
let newChan = network.getChannel(chanName);
if (typeof newChan === "undefined") {
newChan = client.createChannel({
type: ChanType.SPECIAL,
special: SpecialChanType.IGNORELIST,
name: chanName,
data: ignored,
});
client.emit("join", {
network: network.uuid,
chan: newChan.getFilteredClone(true),
shouldOpen: false,
index: network.addChannel(newChan),
});
return;
}
// TODO: add type for this chan/event
newChan.data = ignored;
client.emit("msg:special", {
chan: newChan.id,
data: ignored,
});
};
export default {
commands,
input,
};

View file

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

View file

@ -1,6 +1,7 @@
import {PluginInputHandler} from "./index";
import Msg, {MessageType} from "../../models/msg";
import {ChanType} from "../../models/chan";
import Msg from "../../models/msg";
import {MessageType} from "../../../shared/types/msg";
import {ChanType} from "../../../shared/types/chan";
const commands = ["invite", "invitelist"];

View file

@ -1,6 +1,7 @@
import {PluginInputHandler} from "./index";
import Msg, {MessageType} from "../../models/msg";
import {ChanType} from "../../models/chan";
import Msg from "../../models/msg";
import {MessageType} from "../../../shared/types/msg";
import {ChanType} from "../../../shared/types/chan";
const commands = ["kick"];

View file

@ -1,7 +1,8 @@
import {PluginInputHandler} from "./index";
import Msg, {MessageType} from "../../models/msg";
import {ChanType} from "../../models/chan";
import Msg from "../../models/msg";
import {MessageType} from "../../../shared/types/msg";
import {ChanType} from "../../../shared/types/chan";
const commands = ["mode", "umode", "op", "deop", "hop", "dehop", "voice", "devoice"];

View file

@ -1,6 +1,8 @@
import {PluginInputHandler} from "./index";
import Msg, {MessageType} from "../../models/msg";
import Chan, {ChanType} from "../../models/chan";
import Msg from "../../models/msg";
import Chan from "../../models/chan";
import {MessageType} from "../../../shared/types/msg";
import {ChanType} from "../../../shared/types/chan";
const commands = ["query", "msg", "say"];
@ -97,10 +99,10 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
// being sent back to us.
if (!network.irc.network.cap.isEnabled("echo-message")) {
const parsedTarget = network.irc.network.extractTargetGroup(targetName);
let targetGroup;
let targetGroup: string | undefined = undefined;
if (parsedTarget) {
targetName = parsedTarget.target as string;
targetName = parsedTarget.target;
targetGroup = parsedTarget.target_group;
}

View file

@ -2,9 +2,10 @@ import Chan from "../../models/chan";
import Network from "../../models/network";
import {PluginInputHandler} from "./index";
import Msg, {MessageType} from "../../models/msg";
import Msg from "../../models/msg";
import Client from "../../client";
import {MessageType} from "../../../shared/types/msg";
const commands = ["mute", "unmute"];
const allowDisconnected = true;

View file

@ -1,6 +1,7 @@
import {PluginInputHandler} from "./index";
import Msg, {MessageType} from "../../models/msg";
import Msg from "../../models/msg";
import {MessageType} from "../../../shared/types/msg";
const commands = ["nick"];
const allowDisconnected = true;

View file

@ -1,8 +1,9 @@
import {PluginInputHandler} from "./index";
import Msg, {MessageType} from "../../models/msg";
import Msg from "../../models/msg";
import Config from "../../config";
import {ChanType, ChanState} from "../../models/chan";
import {MessageType} from "../../../shared/types/msg";
import {ChanType, ChanState} from "../../../shared/types/chan";
const commands = ["close", "leave", "part"];
const allowDisconnected = true;

View file

@ -1,7 +1,8 @@
import {PluginInputHandler} from "./index";
import Msg, {MessageType} from "../../models/msg";
import {ChanType} from "../../models/chan";
import Msg from "../../models/msg";
import {MessageType} from "../../../shared/types/msg";
import {ChanType} from "../../../shared/types/chan";
const commands = ["cycle", "rejoin"];

View file

@ -1,7 +1,8 @@
import {PluginInputHandler} from "./index";
import Msg, {MessageType} from "../../models/msg";
import {ChanType} from "../../models/chan";
import Msg from "../../models/msg";
import {MessageType} from "../../../shared/types/msg";
import {ChanType} from "../../../shared/types/chan";
const commands = ["topic"];

View file

@ -1,6 +1,7 @@
import {IrcEventHandler} from "../../client";
import {ChanType} from "../../models/chan";
import Msg, {MessageType} from "../../models/msg";
import Msg from "../../models/msg";
import {MessageType} from "../../../shared/types/msg";
import {ChanType} from "../../../shared/types/chan";
export default <IrcEventHandler>function (irc, network) {
const client = this;

View file

@ -14,7 +14,7 @@ export default <IrcEventHandler>function (irc, network) {
handleSTS(data, false);
});
function handleSTS(data, shouldReconnect) {
function handleSTS(data, shouldReconnect: boolean) {
if (!Object.prototype.hasOwnProperty.call(data.capabilities, "sts")) {
return;
}

View file

@ -1,6 +1,7 @@
import {IrcEventHandler} from "../../client";
import Msg, {MessageType} from "../../models/msg";
import Msg from "../../models/msg";
import {MessageType} from "../../../shared/types/msg";
export default <IrcEventHandler>function (irc, network) {
const client = this;

View file

@ -3,10 +3,11 @@ import _ from "lodash";
import {IrcEventHandler} from "../../client";
import log from "../../log";
import Msg, {MessageType} from "../../models/msg";
import Msg from "../../models/msg";
import Helper from "../../helper";
import Config from "../../config";
import {ChanType, ChanState} from "../../models/chan";
import {MessageType} from "../../../shared/types/msg";
import {ChanType, ChanState} from "../../../shared/types/chan";
export default <IrcEventHandler>function (irc, network) {
const client = this;

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

View file

@ -1,7 +1,8 @@
import {IrcEventHandler} from "../../client";
import Msg, {MessageType} from "../../models/msg";
import Msg from "../../models/msg";
import Config from "../../config";
import {MessageType} from "../../../shared/types/msg";
export default <IrcEventHandler>function (irc, network) {
const client = this;

View file

@ -1,5 +1,6 @@
import Msg, {MessageType} from "../../models/msg";
import Msg from "../../models/msg";
import {IrcEventHandler} from "../../client";
import {MessageType} from "../../../shared/types/msg";
export default <IrcEventHandler>function (irc, network) {
const client = this;

View file

@ -1,5 +1,6 @@
import Msg, {MessageType} from "../../models/msg";
import Msg from "../../models/msg";
import {IrcEventHandler} from "../../client";
import {MessageType} from "../../../shared/types/msg";
export default <IrcEventHandler>function (irc, network) {
const client = this;

View file

@ -1,6 +1,7 @@
import {IrcEventHandler} from "../../client";
import Msg, {MessageType} from "../../models/msg";
import Msg from "../../models/msg";
import {MessageType} from "../../../shared/types/msg";
export default <IrcEventHandler>function (irc, network) {
const client = this;

View file

@ -1,7 +1,8 @@
import Msg, {MessageType} from "../../models/msg";
import Msg from "../../models/msg";
import User from "../../models/user";
import type {IrcEventHandler} from "../../client";
import {ChanState} from "../../models/chan";
import {MessageType} from "../../../shared/types/msg";
import {ChanState} from "../../../shared/types/chan";
export default <IrcEventHandler>function (irc, network) {
const client = this;
@ -18,6 +19,7 @@ export default <IrcEventHandler>function (irc, network) {
client.emit("join", {
network: network.uuid,
chan: chan.getFilteredClone(true),
shouldOpen: false,
index: network.addChannel(chan),
});
client.save();

View file

@ -1,8 +1,8 @@
import {IrcEventHandler} from "../../client";
import {ChanState} from "../../models/chan";
import Msg, {MessageType} from "../../models/msg";
import User from "../../models/user";
import Msg from "../../models/msg";
import {MessageType} from "../../../shared/types/msg";
import {ChanState} from "../../../shared/types/chan";
export default <IrcEventHandler>function (irc, network) {
const client = this;
@ -14,11 +14,12 @@ export default <IrcEventHandler>function (irc, network) {
return;
}
const user = chan.getUser(data.kicked!);
const msg = new Msg({
type: MessageType.KICK,
time: data.time,
from: chan.getUser(data.nick),
target: chan.getUser(data.kicked!),
target: user,
text: data.message || "",
highlight: data.kicked === irc.user.nick,
self: data.nick === irc.user.nick,
@ -34,7 +35,7 @@ export default <IrcEventHandler>function (irc, network) {
state: chan.state,
});
} else {
chan.removeUser(msg.target as User);
chan.removeUser(user);
}
});
};

View file

@ -6,6 +6,7 @@ import mime from "mime-types";
import log from "../../log";
import Config from "../../config";
import {findLinksWithSchema} from "../../../shared/linkify";
import {LinkPreview} from "../../../shared/types/msg";
import storage from "../storage";
import Client from "../../client";
import Chan from "../../models/chan";
@ -20,23 +21,6 @@ const currentFetchPromises = new Map<string, Promise<FetchRequest>>();
const imageTypeRegex = /^image\/.+/;
const mediaTypeRegex = /^(audio|video)\/.+/;
export type LinkPreview = {
type: string;
head: string;
body: string;
thumb: string;
size: number;
link: string; // Send original matched link to the client
shown?: boolean | null;
error?: string;
message?: string;
media?: string;
mediaType?: string;
maxSize?: number;
thumbActualUrl?: string;
};
export default function (client: Client, chan: Chan, msg: Msg, cleanText: string) {
if (!Config.values.prefetch) {
return;

View file

@ -1,6 +1,7 @@
import {IrcEventHandler} from "../../client";
import Chan, {ChanType, SpecialChanType} from "../../models/chan";
import Chan from "../../models/chan";
import {ChanType, SpecialChanType} from "../../../shared/types/chan";
export default <IrcEventHandler>function (irc, network) {
const client = this;
@ -50,6 +51,7 @@ export default <IrcEventHandler>function (irc, network) {
client.emit("join", {
network: network.uuid,
chan: chan.getFilteredClone(true),
shouldOpen: false,
index: network.addChannel(chan),
});
} else {

View file

@ -1,24 +1,38 @@
import Msg, {MessageType} from "../../models/msg";
import Msg from "../../models/msg";
import LinkPrefetch from "./link";
import {cleanIrcMessage} from "../../../shared/irc";
import Helper from "../../helper";
import {IrcEventHandler} from "../../client";
import Chan, {ChanType} from "../../models/chan";
import Chan from "../../models/chan";
import User from "../../models/user";
import {MessageType} from "../../../shared/types/msg";
import {ChanType} from "../../../shared/types/chan";
import {MessageEventArgs} from "irc-framework";
const nickRegExp = /(?:\x03[0-9]{1,2}(?:,[0-9]{1,2})?)?([\w[\]\\`^{|}-]+)/g;
type HandleInput = {
nick: string;
hostname: string;
ident: string;
target: string;
type: MessageType;
time: number;
text?: string;
from_server?: boolean;
message: string;
group?: string;
};
function convertForHandle(type: MessageType, data: MessageEventArgs): HandleInput {
return {...data, time: data.time ? data.time : new Date().getTime(), type: type};
}
export default <IrcEventHandler>function (irc, network) {
const client = this;
irc.on("notice", function (data) {
data.type = MessageType.NOTICE;
type ModifiedData = typeof data & {
type: MessageType.NOTICE;
};
handleMessage(data as ModifiedData);
handleMessage(convertForHandle(MessageType.NOTICE, data));
});
irc.on("action", function (data) {
@ -37,18 +51,7 @@ export default <IrcEventHandler>function (irc, network) {
handleMessage(data);
});
function handleMessage(data: {
nick: string;
hostname: string;
ident: string;
target: string;
type: MessageType;
time: number;
text?: string;
from_server?: boolean;
message: string;
group?: string;
}) {
function handleMessage(data: HandleInput) {
let chan: Chan | undefined;
let from: User;
let highlight = false;
@ -105,6 +108,7 @@ export default <IrcEventHandler>function (irc, network) {
client.emit("join", {
network: network.uuid,
chan: chan.getFilteredClone(true),
shouldOpen: false,
index: network.addChannel(chan),
});
client.save();
@ -125,7 +129,7 @@ export default <IrcEventHandler>function (irc, network) {
// msg is constructed down here because `from` is being copied in the constructor
const msg = new Msg({
type: data.type,
time: data.time as any,
time: new Date(data.time),
text: data.message,
self: self,
from: from,
@ -164,7 +168,6 @@ export default <IrcEventHandler>function (irc, network) {
while ((match = nickRegExp.exec(data.message))) {
if (chan.findUser(match[1])) {
// @ts-expect-error Type 'string' is not assignable to type '{ mode: string; }'.ts(2345)
msg.users.push(match[1]);
}
}

View file

@ -1,7 +1,8 @@
import _ from "lodash";
import {IrcEventHandler} from "../../client";
import Msg, {MessageType} from "../../models/msg";
import Msg from "../../models/msg";
import {MessageType} from "../../../shared/types/msg";
export default <IrcEventHandler>function (irc, network) {
const client = this;

View file

@ -1,7 +1,8 @@
import {IrcEventHandler} from "../../client";
import {SpecialChanType, ChanType} from "../../models/chan";
import Msg, {MessageType} from "../../models/msg";
import Msg from "../../models/msg";
import {MessageType} from "../../../shared/types/msg";
import {SpecialChanType, ChanType} from "../../../shared/types/chan";
export default <IrcEventHandler>function (irc, network) {
const client = this;
@ -68,6 +69,7 @@ export default <IrcEventHandler>function (irc, network) {
client.emit("join", {
network: network.uuid,
chan: chan.getFilteredClone(true),
shouldOpen: false,
index: network.addChannel(chan),
});
} else {

View file

@ -1,6 +1,7 @@
import {IrcEventHandler} from "../../client";
import Msg, {MessageType} from "../../models/msg";
import Msg from "../../models/msg";
import {MessageType} from "../../../shared/types/msg";
export default <IrcEventHandler>function (irc, network) {
const client = this;

View file

@ -1,6 +1,7 @@
import {IrcEventHandler} from "../../client";
import Msg, {MessageType} from "../../models/msg";
import Msg from "../../models/msg";
import {MessageType} from "../../../shared/types/msg";
export default <IrcEventHandler>function (irc, network) {
const client = this;

View file

@ -1,6 +1,7 @@
import {IrcEventHandler} from "../../client";
import Msg, {MessageType} from "../../models/msg";
import Msg from "../../models/msg";
import {MessageType} from "../../../shared/types/msg";
export default <IrcEventHandler>function (irc, network) {
const client = this;

View file

@ -1,6 +1,7 @@
import {IrcEventHandler} from "../../client";
import Msg, {MessageType} from "../../models/msg";
import Msg from "../../models/msg";
import {MessageType} from "../../../shared/types/msg";
export default <IrcEventHandler>function (irc, network) {
const client = this;

View file

@ -1,6 +1,7 @@
import {IrcEventHandler} from "../../client";
import Msg, {MessageType} from "../../models/msg";
import Msg from "../../models/msg";
import {MessageType} from "../../../shared/types/msg";
export default <IrcEventHandler>function (irc, network) {
const client = this;

View file

@ -1,6 +1,7 @@
import {IrcEventHandler} from "../../client";
import Msg, {MessageType} from "../../models/msg";
import Msg from "../../models/msg";
import {MessageType} from "../../../shared/types/msg";
export default <IrcEventHandler>function (irc, network) {
const client = this;

View file

@ -1,6 +1,7 @@
import {IrcEventHandler} from "../../client";
import Msg, {MessageType} from "../../models/msg";
import Msg from "../../models/msg";
import {MessageType} from "../../../shared/types/msg";
export default <IrcEventHandler>function (irc, network) {
const client = this;

View file

@ -1,7 +1,8 @@
import {IrcEventHandler} from "../../client";
import {ChanType} from "../../models/chan";
import Msg, {MessageType} from "../../models/msg";
import Msg from "../../models/msg";
import {MessageType} from "../../../shared/types/msg";
import {ChanType} from "../../../shared/types/chan";
export default <IrcEventHandler>function (irc, network) {
const client = this;
@ -28,9 +29,9 @@ export default <IrcEventHandler>function (irc, network) {
});
client.emit("join", {
shouldOpen: true,
network: network.uuid,
chan: chan.getFilteredClone(true),
shouldOpen: true,
index: network.addChannel(chan),
});
chan.loadMessages(client, network);

View file

@ -7,8 +7,9 @@ import Config from "../../config";
import Msg, {Message} from "../../models/msg";
import Chan, {Channel} from "../../models/chan";
import Helper from "../../helper";
import type {SearchResponse, SearchQuery, SearchableMessageStorage, DeletionRequest} from "./types";
import type {SearchableMessageStorage, DeletionRequest} from "./types";
import Network from "../../models/network";
import {SearchQuery, SearchResponse} from "../../../shared/types/storage";
// TODO; type
let sqlite3: any;

View file

@ -6,8 +6,9 @@ import filenamify from "filenamify";
import Config from "../../config";
import {MessageStorage} from "./types";
import Channel from "../../models/chan";
import {Message, MessageType} from "../../models/msg";
import {Message} from "../../models/msg";
import Network from "../../models/network";
import {MessageType} from "../../../shared/types/msg";
class TextFileMessageStorage implements MessageStorage {
isEnabled: boolean;

View file

@ -4,7 +4,8 @@ import {Channel} from "../../models/channel";
import {Message} from "../../models/message";
import {Network} from "../../models/network";
import Client from "../../client";
import type {MessageType} from "../../models/msg";
import {SearchQuery, SearchResponse} from "../../../shared/types/storage";
import type {MessageType} from "../../../shared/types/msg";
export type DeletionRequest = {
olderThanDays: number;
@ -28,17 +29,6 @@ interface MessageStorage {
canProvideMessages(): boolean;
}
export type SearchQuery = {
searchTerm: string;
networkUuid: string;
channelName: string;
offset: number;
};
export type SearchResponse = SearchQuery & {
results: Message[];
};
type SearchFunction = (query: SearchQuery) => Promise<SearchResponse>;
export interface SearchableMessageStorage extends MessageStorage {

View file

@ -1,7 +1,8 @@
import {PackageInfo} from "./index";
import Client from "../../client";
import Chan from "../../models/chan";
import Msg, {MessageType, UserInMessage} from "../../models/msg";
import Msg from "../../models/msg";
import {MessageType} from "../../../shared/types/msg";
export default class PublicClient {
private client: Client;
@ -35,8 +36,11 @@ export default class PublicClient {
* @param {String} event - Name of the event, must be something the browser will recognise
* @param {Object} data - Body of the event, can be anything, but will need to be properly interpreted by the client
*/
sendToBrowser(event: string, data) {
this.client.emit(event, data);
// FIXME: this is utterly bonkers
// This needs to get wrapped into its own, typed plugin event
// Plus it is completely insane to let a plugin inject arbitrary events like that
sendToBrowser(event: string, data: any) {
this.client.emit(event as any, data);
}
/**
@ -61,7 +65,8 @@ export default class PublicClient {
text: text,
from: {
nick: this.packageInfo.name || this.packageInfo.packageName,
} as UserInMessage,
mode: "",
},
})
);
}

View file

@ -3,7 +3,7 @@ import {Server as wsServer} from "ws";
import express, {NextFunction, Request, Response} from "express";
import fs from "fs";
import path from "path";
import {Server, Socket} from "socket.io";
import {Server as ioServer, Socket as ioSocket} from "socket.io";
import dns from "dns";
import colors from "chalk";
import net from "net";
@ -13,25 +13,32 @@ import Client from "./client";
import ClientManager from "./clientManager";
import Uploader from "./plugins/uploader";
import Helper from "./helper";
import Config, {ConfigType, Defaults} from "./config";
import Config, {ConfigType} from "./config";
import Identification from "./identification";
import changelog from "./plugins/changelog";
import inputs from "./plugins/inputs";
import Auth from "./plugins/auth";
import themes, {ThemeForClient} from "./plugins/packages/themes";
import themes from "./plugins/packages/themes";
themes.loadLocalThemes();
import packages from "./plugins/packages/index";
import {NetworkWithIrcFramework} from "./models/network";
import {ChanType} from "./models/chan";
import Utils from "./command-line/utils";
import type {
ClientToServerEvents,
ServerToClientEvents,
InterServerEvents,
SocketData,
} from "./types/socket-events";
AuthPerformData,
} from "../shared/types/socket-events";
import {ChanType} from "../shared/types/chan";
import {
LockedSharedConfiguration,
SharedConfiguration,
ConfigNetDefaults,
LockedConfigNetDefaults,
} from "../shared/types/config";
type ServerOptions = {
dev: boolean;
@ -45,21 +52,13 @@ type IndexTemplateConfiguration = ServerConfiguration & {
cacheBust: string;
};
export type ClientConfiguration = Pick<
ConfigType,
"public" | "lockNetwork" | "useHexIp" | "prefetch" | "defaults"
> & {
fileUpload: boolean;
ldapEnabled: boolean;
isUpdateAvailable: boolean;
applicationServerKey: string;
version: string;
gitCommit: string | null;
defaultTheme: string;
themes: ThemeForClient[];
defaults: Defaults;
fileUploadMaxFileSize?: number;
};
type Socket = ioSocket<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>;
export type Server = ioServer<
ClientToServerEvents,
ServerToClientEvents,
InterServerEvents,
SocketData
>;
// A random number that will force clients to reload the page if it differs
const serverHash = Math.floor(Date.now() * Math.random());
@ -219,12 +218,7 @@ export default async function (
return;
}
const sockets = new Server<
ClientToServerEvents,
ServerToClientEvents,
InterServerEvents,
SocketData
>(server, {
const sockets: Server = new ioServer(server, {
wsEngine: wsServer,
cookie: false,
serveClient: false,
@ -330,7 +324,7 @@ export default async function (
return server;
}
function getClientLanguage(socket: Socket): string | null {
function getClientLanguage(socket: Socket): string | undefined {
const acceptLanguage = socket.handshake.headers["accept-language"];
if (typeof acceptLanguage === "string" && /^[\x00-\x7F]{1,50}$/.test(acceptLanguage)) {
@ -338,10 +332,10 @@ function getClientLanguage(socket: Socket): string | null {
return acceptLanguage;
}
return null;
return undefined;
}
function getClientIp(socket: Socket) {
function getClientIp(socket: Socket): string {
let ip = socket.handshake.address || "127.0.0.1";
if (Config.values.reverseProxy) {
@ -367,12 +361,12 @@ function getClientSecure(socket: Socket) {
return secure;
}
function allRequests(req: Request, res: Response, next: NextFunction) {
function allRequests(_req: Request, res: Response, next: NextFunction) {
res.setHeader("X-Content-Type-Options", "nosniff");
return next();
}
function addSecurityHeaders(req: Request, res: Response, next: NextFunction) {
function addSecurityHeaders(_req: Request, res: Response, next: NextFunction) {
const policies = [
"default-src 'none'", // default to nothing
"base-uri 'none'", // disallow <base>, has no fallback to default-src
@ -402,32 +396,30 @@ function addSecurityHeaders(req: Request, res: Response, next: NextFunction) {
return next();
}
function forceNoCacheRequest(req: Request, res: Response, next: NextFunction) {
function forceNoCacheRequest(_req: Request, res: Response, next: NextFunction) {
// Intermittent proxies must not cache the following requests,
// browsers must fetch the latest version of these files (service worker, source maps)
res.setHeader("Cache-Control", "no-cache, no-transform");
return next();
}
function indexRequest(req: Request, res: Response) {
function indexRequest(_req: Request, res: Response) {
res.setHeader("Content-Type", "text/html");
return fs.readFile(
Utils.getFileFromRelativeToRoot("client/index.html.tpl"),
"utf-8",
(err, file) => {
if (err) {
throw err;
}
const config: IndexTemplateConfiguration = {
...getServerConfiguration(),
...{cacheBust: Helper.getVersionCacheBust()},
};
res.send(_.template(file)(config));
fs.readFile(Utils.getFileFromRelativeToRoot("client/index.html.tpl"), "utf-8", (err, file) => {
if (err) {
log.error(`failed to server index request: ${err.name}, ${err.message}`);
res.sendStatus(500);
return;
}
);
const config: IndexTemplateConfiguration = {
...getServerConfiguration(),
...{cacheBust: Helper.getVersionCacheBust()},
};
res.send(_.template(file)(config));
});
}
function initializeClient(
@ -552,18 +544,10 @@ function initializeClient(
const hash = Helper.password.hash(p1);
client.setPassword(hash, (success: boolean) => {
const obj = {success: false, error: undefined} as {
success: boolean;
error: string | undefined;
};
if (success) {
obj.success = true;
} else {
obj.error = "update_failed";
}
socket.emit("change-password", obj);
socket.emit("change-password", {
success: success,
error: success ? undefined : "update_failed",
});
});
})
.catch((error: Error) => {
@ -577,10 +561,28 @@ function initializeClient(
client.open(socket.id, data);
});
socket.on("sort", (data) => {
if (_.isPlainObject(data)) {
client.sort(data);
socket.on("sort:networks", (data) => {
if (!_.isPlainObject(data)) {
return;
}
if (!Array.isArray(data.order)) {
return;
}
client.sortNetworks(data.order);
});
socket.on("sort:channels", (data) => {
if (!_.isPlainObject(data)) {
return;
}
if (!Array.isArray(data.order) || typeof data.network !== "string") {
return;
}
client.sortChannels(data.network, data.order);
});
socket.on("names", (data) => {
@ -630,13 +632,13 @@ function initializeClient(
return;
}
const message = networkAndChan.chan.findMessage(data.msgId);
const message = data.msgId ? networkAndChan.chan.findMessage(data.msgId) : null;
if (!message) {
return;
}
const preview = message.findPreview(data.link);
const preview = data.link ? message.findPreview(data.link) : null;
if (preview) {
preview.shown = newState;
@ -828,9 +830,9 @@ function initializeClient(
});
// socket.join is a promise depending on the adapter.
void socket.join(client.id?.toString());
void socket.join(client.id);
const sendInitEvent = (tokenToSend: string | null) => {
const sendInitEvent = (tokenToSend?: string) => {
socket.emit("init", {
active: openChannel,
networks: client.networks.map((network) =>
@ -842,7 +844,7 @@ function initializeClient(
};
if (Config.values.public) {
sendInitEvent(null);
sendInitEvent();
} else if (!token) {
client.generateToken((newToken) => {
token = client.calculateTokenHash(newToken);
@ -853,73 +855,108 @@ function initializeClient(
});
} else {
client.updateSession(token, getClientIp(socket), socket.request);
sendInitEvent(null);
sendInitEvent();
}
}
function getClientConfiguration(): ClientConfiguration {
const config = _.pick(Config.values, [
"public",
"lockNetwork",
"useHexIp",
"prefetch",
]) as ClientConfiguration;
function getClientConfiguration(): SharedConfiguration | LockedSharedConfiguration {
const common = {
fileUpload: Config.values.fileUpload.enable,
ldapEnabled: Config.values.ldap.enable,
isUpdateAvailable: changelog.isUpdateAvailable,
applicationServerKey: manager!.webPush.vapidKeys!.publicKey,
version: Helper.getVersionNumber(),
gitCommit: Helper.getGitCommit(),
themes: themes.getAll(),
defaultTheme: Config.values.theme,
public: Config.values.public,
useHexIp: Config.values.useHexIp,
prefetch: Config.values.prefetch,
fileUploadMaxFileSize: Uploader ? Uploader.getMaxFileSize() : undefined, // TODO can't be undefined?
};
config.fileUpload = Config.values.fileUpload.enable;
config.ldapEnabled = Config.values.ldap.enable;
const defaultsOverride = {
nick: Config.getDefaultNick(), // expand the number part
if (!config.lockNetwork) {
config.defaults = _.clone(Config.values.defaults);
} else {
// Only send defaults that are visible on the client
config.defaults = _.pick(Config.values.defaults, [
"name",
"nick",
"username",
"password",
"realname",
"join",
]) as Defaults;
// TODO: this doesn't seem right, if the client needs this as a buffer
// the client ought to add it on its own
sasl: "",
saslAccount: "",
saslPassword: "",
};
if (!Config.values.lockNetwork) {
const defaults: ConfigNetDefaults = {
..._.clone(Config.values.defaults),
...defaultsOverride,
};
const result: SharedConfiguration = {
...common,
defaults: defaults,
lockNetwork: Config.values.lockNetwork,
};
return result;
}
config.isUpdateAvailable = changelog.isUpdateAvailable;
config.applicationServerKey = manager!.webPush.vapidKeys!.publicKey;
config.version = Helper.getVersionNumber();
config.gitCommit = Helper.getGitCommit();
config.themes = themes.getAll();
config.defaultTheme = Config.values.theme;
config.defaults.nick = Config.getDefaultNick();
config.defaults.sasl = "";
config.defaults.saslAccount = "";
config.defaults.saslPassword = "";
// Only send defaults that are visible on the client
const defaults: LockedConfigNetDefaults = {
..._.pick(Config.values.defaults, ["name", "username", "password", "realname", "join"]),
...defaultsOverride,
};
if (Uploader) {
config.fileUploadMaxFileSize = Uploader.getMaxFileSize();
}
const result: LockedSharedConfiguration = {
...common,
lockNetwork: Config.values.lockNetwork,
defaults: defaults,
};
return config;
return result;
}
function getServerConfiguration(): ServerConfiguration {
return {...Config.values, ...{stylesheets: packages.getStylesheets()}};
}
function performAuthentication(this: Socket, data) {
function performAuthentication(this: Socket, data: AuthPerformData) {
if (!_.isPlainObject(data)) {
return;
}
const socket = this;
let client;
let client: Client | undefined;
let token: string;
const finalInit = () =>
initializeClient(socket, client, token, data.lastMessage || -1, data.openChannel);
const finalInit = () => {
let lastMessage = -1;
if (data && "lastMessage" in data && data.lastMessage) {
lastMessage = data.lastMessage;
}
// TODO: bonkers, but for now good enough until we rewrite the logic properly
// initializeClient will check for if(openChannel) and as 0 is falsey it does the fallback...
let openChannel = 0;
if (data && "openChannel" in data && data.openChannel) {
openChannel = data.openChannel;
}
// TODO: remove this once the logic is cleaned up
if (!client) {
throw new Error("finalInit called with undefined client, this is a bug");
}
initializeClient(socket, client, token, lastMessage, openChannel);
};
const initClient = () => {
if (!client) {
throw new Error("initClient called with undefined client");
}
// Configuration does not change during runtime of TL,
// and the client listens to this event only once
if (!data.hasConfig) {
if (data && (!("hasConfig" in data) || !data.hasConfig)) {
socket.emit("configuration", getClientConfiguration());
socket.emit(
@ -928,8 +965,10 @@ function performAuthentication(this: Socket, data) {
);
}
const clientIP = getClientIp(socket);
client.config.browser = {
ip: getClientIp(socket),
ip: clientIP,
isSecure: getClientSecure(socket),
language: getClientLanguage(socket),
};
@ -939,8 +978,9 @@ function performAuthentication(this: Socket, data) {
return finalInit();
}
reverseDnsLookup(client.config.browser?.ip, (hostname) => {
client.config.browser!.hostname = hostname;
const cb_client = client; // ensure that TS figures out that client can't be nil
reverseDnsLookup(clientIP, (hostname) => {
cb_client.config.browser!.hostname = hostname;
finalInit();
});
@ -951,9 +991,10 @@ function performAuthentication(this: Socket, data) {
client.connect();
manager!.clients.push(client);
const cb_client = client; // ensure TS can see we never have a nil client
socket.on("disconnect", function () {
manager!.clients = _.without(manager!.clients, client);
client.quit();
manager!.clients = _.without(manager!.clients, cb_client);
cb_client.quit();
});
initClient();
@ -965,7 +1006,7 @@ function performAuthentication(this: Socket, data) {
return;
}
const authCallback = (success) => {
const authCallback = (success: boolean) => {
// Authorization failed
if (!success) {
if (!client) {
@ -990,6 +1031,10 @@ function performAuthentication(this: Socket, data) {
// load it and find the user again (this happens with LDAP)
if (!client) {
client = manager!.loadUser(data.user);
if (!client) {
throw new Error(`authCallback: ${data.user} not found after second lookup`);
}
}
initClient();
@ -998,16 +1043,23 @@ function performAuthentication(this: Socket, data) {
client = manager!.findClient(data.user);
// We have found an existing user and client has provided a token
if (client && data.token) {
if (client && "token" in data && data.token) {
const providedToken = client.calculateTokenHash(data.token);
if (Object.prototype.hasOwnProperty.call(client.config.sessions, providedToken)) {
token = providedToken;
return authCallback(true);
authCallback(true);
return;
}
}
if (!("user" in data && "password" in data)) {
log.warn("performAuthentication: callback data has no user or no password");
authCallback(false);
return;
}
Auth.initialize().then(() => {
// Perform password checking
Auth.auth(manager, client, data.user, data.password, authCallback);

View file

@ -1,8 +1,8 @@
import SqliteMessageStorage from "./plugins/messageStorage/sqlite";
import {MessageType} from "./models/msg";
import Config from "./config";
import {DeletionRequest} from "./plugins/messageStorage/types";
import log from "./log";
import {MessageType} from "../shared/types/msg";
const status_types = [
MessageType.AWAY,

View file

@ -1,7 +1,7 @@
{
"extends": "../tsconfig.base.json" /* Path to base configuration file to inherit from. Requires TypeScript version 2.1 or later. */,
"include": [
"**/*",
".",
"../shared/"
] /* Specifies a list of glob patterns that match files to be included in compilation. If no 'files' or 'include' property is present in a tsconfig.json, the compiler defaults to including all files in the containing directory and subdirectories except those specified by 'exclude'. Requires TypeScript version 2.0 or later. */,
"files": [

View file

@ -1,2 +1 @@
import "./modules";
import "./socket-events";

View file

@ -33,8 +33,7 @@ declare module "irc-framework" {
reply: (message: string) => void;
tags: {[key: string]: string};
target: string;
time?: any;
type: "privmsg" | "action" | "notice" | "wallops";
time?: number;
}
export interface JoinEventArgs {
account: boolean;
@ -117,7 +116,11 @@ declare module "irc-framework" {
isEnabled: (cap: string) => boolean;
enabled: string[];
};
extractTargetGroup: (target: string) => any;
extractTargetGroup: (target: string) => {
target: string;
target_group: string;
};
supports(feature: "MODES"): string;
supports(feature: string): boolean;
};

View file

@ -1,224 +0,0 @@
import {ClientMessage, ClientNetwork, InitClientChan} from "../../client/js/types";
import {Mention} from "../client";
import {ChanState} from "../models/chan";
import Msg from "../models/msg";
import Network from "../models/network";
import User from "../models/user";
import {ChangelogData} from "../plugins/changelog";
import {LinkPreview} from "../plugins/irc-events/link";
import {ClientConfiguration} from "../server";
type Session = {
current: boolean;
active: number;
lastUse: number;
ip: string;
agent: string;
token: string;
};
interface ServerToClientEvents {
"auth:failed": () => void;
"auth:start": (serverHash: number) => void;
"auth:success": () => void;
"upload:auth": (token: string) => void;
changelog: (data: ChangelogData) => void;
"changelog:newversion": () => void;
"channel:state": (data: {chan: number; state: ChanState}) => void;
"change-password": ({success, error}: {success: boolean; error?: any}) => void;
commands: (data: string[]) => void;
configuration: (config: ClientConfiguration) => void;
"push:issubscribed": (isSubscribed: boolean) => void;
"push:unregister": () => void;
"sessions:list": (data: Session[]) => void;
"mentions:list": (data: Mention[]) => void;
"setting:new": ({name: string, value: any}) => void;
"setting:all": (settings: {[key: string]: any}) => void;
"history:clear": ({target}: {target: number}) => void;
"mute:changed": (response: {target: number; status: boolean}) => void;
names: (data: {id: number; users: User[]}) => void;
network: (data: {networks: ClientNetwork[]}) => void;
"network:options": (data: {network: string; serverOptions: {[key: string]: any}}) => void;
"network:status": (data: {network: string; connected: boolean; secure: boolean}) => void;
"network:info": (data: {uuid: string}) => void;
"network:name": (data: {uuid: string; name: string}) => void;
nick: (data: {network: string; nick: string}) => void;
open: (id: number) => void;
part: (data: {chan: number}) => void;
"sign-out": () => void;
sync_sort: (
data:
| {
type: "networks";
order: string[];
target: string;
}
| {
type: "channels";
order: number[];
target: string;
}
) => void;
topic: (data: {chan: number; topic: string}) => void;
users: (data: {chan: number}) => void;
more: ({
chan,
messages,
totalMessages,
}: {
chan: number;
messages: Msg[];
totalMessages: number;
}) => void;
"msg:preview": ({id, chan, preview}: {id: number; chan: number; preview: LinkPreview}) => void;
"msg:special": (data: {chan: number; data?: Record<string, any>}) => void;
msg: (data: {msg: ClientMessage; chan: number; highlight?: number; unread?: number}) => void;
init: ({
active,
networks,
token,
}: {
active: number;
networks: ClientNetwork[];
token: string;
}) => void;
"search:results": (response: SearchResponse) => void;
quit: (args: {network: string}) => void;
error: (error: any) => void;
connecting: () => void;
join: (args: {
shouldOpen: boolean;
index: number;
network: string;
chan: InitClientChan;
}) => void;
}
interface ClientToServerEvents {
"auth:perform":
| (({user, password}: {user: string; password: string}) => void)
| (({
user,
token,
lastMessage,
openChannel,
hasConfig,
}: {
user: string;
token: string;
lastMessage: number;
openChannel: number | null;
hasConfig: boolean;
}) => void);
changelog: () => void;
"change-password": ({
old_password: string,
new_password: string,
verify_password: string,
}) => void;
open: (channelId: number) => void;
names: ({target: number}) => void;
input: ({target, text}: {target: number; text: string}) => void;
"upload:auth": () => void;
"upload:ping": (token: string) => void;
"mute:change": (response: {target: number; setMutedTo: boolean}) => void;
"push:register": (subscriptionJson: PushSubscriptionJSON) => void;
"push:unregister": () => void;
"setting:get": () => void;
"setting:set": ({name: string, value: any}) => void;
"sessions:get": () => void;
sort: ({type, order}: {type: string; order: any; target?: string}) => void;
"mentions:dismiss": (msgId: number) => void;
"mentions:dismiss_all": () => void;
"mentions:get": () => void;
more: ({
target,
lastId,
condensed,
}: {
target: number;
lastId: number;
condensed: boolean;
}) => void;
"msg:preview:toggle": ({
target,
messageIds,
msgId,
shown,
link,
}: {
target: number;
messageIds?: number[];
msgId?: number;
shown?: boolean | null;
link?: string;
}) => void;
"network:get": (uuid: string) => void;
"network:edit": (data: Record<string, any>) => void;
"network:new": (data: Record<string, any>) => void;
"sign-out": (token?: string) => void;
"history:clear": ({target}: {target: number}) => void;
search: ({
networkUuid,
channelName,
searchTerm,
offset,
}: {
networkUuid?: string;
channelName?: string;
searchTerm?: string;
offset: number;
}) => void;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface InterServerEvents {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface SocketData {}

View file

@ -1,36 +1,12 @@
import LinkifyIt, {Match} from "linkify-it";
import tlds from "tlds";
export type NoSchemaMatch = Match & {
noschema: boolean;
};
export type LinkPart = {
start: number;
end: number;
link: string;
};
LinkifyIt.prototype.normalize = function normalize(match: NoSchemaMatch) {
match.noschema = false;
if (!match.schema) {
match.schema = "http:";
match.url = "http://" + match.url;
match.noschema = true;
}
if (match.schema === "//") {
match.schema = "http:";
match.url = "http:" + match.url;
match.noschema = true;
}
if (match.schema === "mailto:" && !/^mailto:/i.test(match.url)) {
match.url = "mailto:" + match.url;
}
};
const linkify = LinkifyIt().tlds(tlds).tlds("onion", true);
// Known schemes to detect in text
@ -73,12 +49,25 @@ linkify.add("web+", {
},
normalize(match) {
match.schema = match.text.slice(0, match.text.indexOf(":") + 1);
LinkifyIt.prototype.normalize(match); // hand over to the global override
},
});
// we must rewrite protocol less urls to http, else if TL is hosted
// on https, this would incorrectly use https for the remote link.
// See https://github.com/thelounge/thelounge/issues/2525
//
// We take the validation logic from linkify and just add our own
// normalizer.
linkify.add("//", {
validate: (linkify as any).__schemas__["//"].validate,
normalize(match) {
match.schema = ""; // this counts as not having a schema
match.url = "http:" + match.url;
},
});
export function findLinks(text: string) {
const matches = linkify.match(text) as NoSchemaMatch[];
const matches = linkify.match(text);
if (!matches) {
return [];
@ -88,16 +77,16 @@ export function findLinks(text: string) {
}
export function findLinksWithSchema(text: string) {
const matches = linkify.match(text) as NoSchemaMatch[];
const matches = linkify.match(text);
if (!matches) {
return [];
}
return matches.filter((url) => !url.noschema).map(makeLinkPart);
return matches.filter((url) => !!url.schema).map(makeLinkPart);
}
function makeLinkPart(url: NoSchemaMatch): LinkPart {
function makeLinkPart(url: Match): LinkPart {
return {
start: url.index,
end: url.lastIndex,

42
shared/types/chan.ts Normal file
View file

@ -0,0 +1,42 @@
import {SharedMsg} from "./msg";
import {SharedUser} from "./user";
import {SharedNetworkChan} from "./network";
export enum ChanType {
CHANNEL = "channel",
LOBBY = "lobby",
QUERY = "query",
SPECIAL = "special",
}
export enum SpecialChanType {
BANLIST = "list_bans",
INVITELIST = "list_invites",
CHANNELLIST = "list_channels",
IGNORELIST = "list_ignored",
}
export enum ChanState {
PARTED = 0,
JOINED = 1,
}
export type SharedChan = {
// TODO: don't force existence, figure out how to make TS infer it.
id: number;
messages: SharedMsg[];
name: string;
key: string;
topic: string;
firstUnread: number;
unread: number;
highlight: number;
muted: boolean;
type: ChanType;
state: ChanState;
special?: SpecialChanType;
data?: any;
closed?: boolean;
num_users?: number;
};

15
shared/types/changelog.ts Normal file
View file

@ -0,0 +1,15 @@
export type SharedChangelogData = {
current: {
prerelease: boolean;
version: string;
changelog?: string;
url: string;
};
expiresAt: number;
latest?: {
prerelease: boolean;
version: string;
url: string;
};
packages?: boolean;
};

50
shared/types/config.ts Normal file
View file

@ -0,0 +1,50 @@
export type ConfigTheme = {
displayName: string;
name: string;
themeColor: string | null;
};
type SharedConfigurationBase = {
public: boolean;
useHexIp: boolean;
prefetch: boolean;
fileUpload: boolean;
ldapEnabled: boolean;
isUpdateAvailable: boolean;
applicationServerKey: string;
version: string;
gitCommit: string | null;
themes: ConfigTheme[];
defaultTheme: string;
fileUploadMaxFileSize?: number;
};
export type ConfigNetDefaults = {
name: string;
host: string;
port: number;
password: string;
tls: boolean;
rejectUnauthorized: boolean;
nick: string;
username: string;
realname: string;
join: string;
leaveMessage: string;
sasl: string;
saslAccount: string;
saslPassword: string;
};
export type LockedConfigNetDefaults = Pick<
ConfigNetDefaults,
"name" | "nick" | "username" | "password" | "realname" | "join"
>;
export type LockedSharedConfiguration = SharedConfigurationBase & {
lockNetwork: true;
defaults: LockedConfigNetDefaults;
};
export type SharedConfiguration = SharedConfigurationBase & {
lockNetwork: false;
defaults: ConfigNetDefaults;
};

10
shared/types/mention.ts Normal file
View file

@ -0,0 +1,10 @@
import {MessageType, UserInMessage} from "./msg";
export type SharedMention = {
chanId: number;
msgId: number;
type: MessageType;
time: Date;
text: string;
from: UserInMessage;
};

100
shared/types/msg.ts Normal file
View file

@ -0,0 +1,100 @@
export enum MessageType {
UNHANDLED = "unhandled",
ACTION = "action",
AWAY = "away",
BACK = "back",
ERROR = "error",
INVITE = "invite",
JOIN = "join",
KICK = "kick",
LOGIN = "login",
LOGOUT = "logout",
MESSAGE = "message",
MODE = "mode",
MODE_CHANNEL = "mode_channel",
MODE_USER = "mode_user", // RPL_UMODEIS
MONOSPACE_BLOCK = "monospace_block",
NICK = "nick",
NOTICE = "notice",
PART = "part",
QUIT = "quit",
CTCP = "ctcp",
CTCP_REQUEST = "ctcp_request",
CHGHOST = "chghost",
TOPIC = "topic",
TOPIC_SET_BY = "topic_set_by",
WHOIS = "whois",
RAW = "raw",
PLUGIN = "plugin",
WALLOPS = "wallops",
}
export type SharedUser = {
modes: string[];
// Users in the channel have only one mode assigned
mode: string;
away: string;
nick: string;
lastMessage: number;
};
export type UserInMessage = Partial<SharedUser> & {
mode: string;
};
export type LinkPreview = {
type: string;
head: string;
body: string;
thumb: string;
size: number;
link: string; // Send original matched link to the client
shown?: boolean | null;
error?: string;
message?: string;
media?: string;
mediaType?: string;
maxSize?: number;
thumbActualUrl?: string;
};
export type SharedMsg = {
from?: UserInMessage;
id: number;
previews?: LinkPreview[];
text?: string;
type?: MessageType;
self?: boolean;
time: Date;
hostmask?: string;
target?: UserInMessage;
// TODO: new_nick is only on MessageType.NICK,
// we should probably make Msgs that extend this class and use those
// throughout. I'll leave any similar fields below.
new_nick?: string;
highlight?: boolean;
showInActive?: boolean;
new_ident?: string;
new_host?: string;
ctcpMessage?: string;
command?: string;
invitedYou?: boolean;
gecos?: string;
account?: boolean;
// these are all just for error:
error?: string;
nick?: string;
channel?: string;
reason?: string;
raw_modes?: any;
when?: Date;
whois?: any;
users: string[];
statusmsgGroup?: string;
params?: string[];
};

36
shared/types/network.ts Normal file
View file

@ -0,0 +1,36 @@
import {SharedChan} from "./chan";
export type SharedPrefixObject = {
symbol: string;
mode: string;
};
export type SharedNetworkChan = SharedChan & {
totalMessages: number;
};
export type SharedPrefix = {
prefix: SharedPrefixObject[];
modeToSymbol: {[mode: string]: string};
symbols: string[];
};
export type SharedServerOptions = {
CHANTYPES: string[];
PREFIX: SharedPrefix;
NETWORK: string;
};
export type SharedNetworkStatus = {
connected: boolean;
secure: boolean;
};
export type SharedNetwork = {
uuid: string;
name: string;
nick: string;
serverOptions: SharedServerOptions;
status: SharedNetworkStatus;
channels: SharedNetworkChan[];
};

181
shared/types/socket-events.d.ts vendored Normal file
View file

@ -0,0 +1,181 @@
import {SharedMention} from "./mention";
import {ChanState, SharedChan} from "./chan";
import {SharedNetwork, SharedServerOptions} from "./network";
import {SharedMsg, LinkPreview} from "./msg";
import {SharedUser} from "./user";
import {SharedChangelogData} from "./changelog";
import {SharedConfiguration, LockedSharedConfiguration} from "./config";
import {SearchResponse, SearchQuery} from "./storage";
type Session = {
current: boolean;
active: number;
lastUse: number;
ip: string;
agent: string;
token: string;
};
type EventHandler<T> = (data: T) => void;
type NoPayloadEventHandler = EventHandler<void>;
interface ServerToClientEvents {
"auth:start": (serverHash: number) => void;
"auth:failed": NoPayloadEventHandler;
"auth:success": NoPayloadEventHandler;
"upload:auth": (token: string) => void;
changelog: EventHandler<SharedChangelogData>;
"changelog:newversion": NoPayloadEventHandler;
"channel:state": EventHandler<{chan: number; state: ChanState}>;
"change-password": EventHandler<{success: boolean; error?: any}>;
commands: EventHandler<string[]>;
configuration: EventHandler<SharedConfiguration | LockedSharedConfiguration>;
"push:issubscribed": EventHandler<boolean>;
"push:unregister": NoPayloadEventHandler;
"sessions:list": EventHandler<Session[]>;
"mentions:list": EventHandler<SharedMention[]>;
"setting:new": EventHandler<{name: string; value: any}>;
"setting:all": EventHandler<{[key: string]: any}>;
"history:clear": EventHandler<{target: number}>;
"mute:changed": EventHandler<{target: number; status: boolean}>;
names: EventHandler<{id: number; users: SharedUser[]}>;
network: EventHandler<{network: SharedNetwork}>;
"network:options": EventHandler<{network: string; serverOptions: SharedServerOptions}>;
"network:status": EventHandler<{network: string; connected: boolean; secure: boolean}>;
"network:info": EventHandler<{uuid: string}>;
"network:name": EventHandler<{uuid: string; name: string}>;
nick: EventHandler<{network: string; nick: string}>;
open: (id: number) => void;
part: EventHandler<{chan: number}>;
"sign-out": NoPayloadEventHandler;
"sync_sort:networks": EventHandler<{order: SharedNetwork["uuid"][]}>;
"sync_sort:channels": EventHandler<{
network: SharedNetwork["uuid"];
order: SharedChan["id"][];
}>;
topic: EventHandler<{chan: number; topic: string}>;
users: EventHandler<{chan: number}>;
more: EventHandler<{chan: number; messages: SharedMsg[]; totalMessages: number}>;
"msg:preview": EventHandler<{id: number; chan: number; preview: LinkPreview}>;
"msg:special": EventHandler<{chan: number; data?: Record<string, any>}>;
msg: EventHandler<{msg: SharedMsg; chan: number; highlight?: number; unread?: number}>;
init: EventHandler<{active: number; networks: SharedNetwork[]; token?: string}>;
"search:results": (response: SearchResponse) => void;
quit: EventHandler<{network: string}>;
error: (error: any) => void;
connecting: NoPayloadEventHandler;
join: EventHandler<{
shouldOpen: boolean;
index: number;
network: string;
chan: SharedNetworkChan;
}>;
}
type AuthPerformData =
| Record<string, never> // funny way of saying an empty object
| {user: string; password: string}
| {
user: string;
token: string;
lastMessage: number;
openChannel: number | null;
hasConfig: boolean;
};
interface ClientToServerEvents {
"auth:perform": EventHandler<AuthPerformData>;
changelog: NoPayloadEventHandler;
"change-password": EventHandler<{
old_password: string;
new_password: string;
verify_password: string;
}>;
open: (channelId: number) => void;
names: EventHandler<{target: number}>;
input: EventHandler<{target: number; text: string}>;
"upload:auth": NoPayloadEventHandler;
"upload:ping": (token: string) => void;
"mute:change": EventHandler<{target: number; setMutedTo: boolean}>;
"push:register": EventHandler<PushSubscriptionJSON>;
"push:unregister": NoPayloadEventHandler;
"setting:get": NoPayloadEventHandler;
"setting:set": EventHandler<{name: string; value: any}>;
"sessions:get": NoPayloadEventHandler;
"sort:networks": EventHandler<{order: SharedNetwork["uuid"][]}>;
"sort:channels": EventHandler<{
network: SharedNetwork["uuid"];
order: SharedChan["id"][];
}>;
"mentions:dismiss": (msgId: number) => void;
"mentions:dismiss_all": NoPayloadEventHandler;
"mentions:get": NoPayloadEventHandler;
more: EventHandler<{target: number; lastId: number; condensed: boolean}>;
"msg:preview:toggle": EventHandler<{
target: number;
messageIds?: number[];
msgId?: number;
shown?: boolean | null;
link?: string;
}>;
"network:get": (uuid: string) => void;
// TODO typing
"network:edit": (data: Record<string, any>) => void;
"network:new": (data: Record<string, any>) => void;
"sign-out": (token?: string) => void;
"history:clear": EventHandler<{target: number}>;
search: EventHandler<SearchQuery>;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface InterServerEvents {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface SocketData {}

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