mirror of
https://github.com/thelounge/thelounge.git
synced 2024-06-08 00:32:19 +02:00
Compare commits
130 commits
413fb37489
...
eed3c0efd8
Author | SHA1 | Date | |
---|---|---|---|
eed3c0efd8 | |||
da2572fe25 | |||
d9977df315 | |||
cc0aa5e8e5 | |||
02df78b0f2 | |||
18b0e06855 | |||
d5db9c653b | |||
f7926267d9 | |||
8eb398c5cc | |||
36cb75ee99 | |||
1ec67a6605 | |||
8372c5a57e | |||
5567f07a7c | |||
a200bab8bd | |||
91ac363cc6 | |||
6c9d2c36a1 | |||
6241eed8f4 | |||
03151e0ab1 | |||
7f5e0f3ebf | |||
5e444be37b | |||
c8664301ba | |||
1edb5a72c1 | |||
31d987283a | |||
4ceafb653f | |||
f25fee4c6c | |||
96848c1c1b | |||
4b07e05491 | |||
fc9805545b | |||
82e4150cc8 | |||
e61e356f1e | |||
5001d607b1 | |||
8c41356ae9 | |||
e2b56cf16b | |||
92a0affba1 | |||
edb96f683b | |||
5c8951ffc3 | |||
c3fc54e158 | |||
917fdb2a0a | |||
b8400a3a46 | |||
071a5afda6 | |||
5274fdc21a | |||
b8a9fe08ab | |||
a4afa08add | |||
4614c35486 | |||
540144c417 | |||
bb7c3925c6 | |||
9898f38de6 | |||
9f2c82e152 | |||
17ba07db3b | |||
0311e5f836 | |||
4d0474b897 | |||
14b9169899 | |||
50037644c0 | |||
7287c6bcaa | |||
bfca0ca612 | |||
300bd4c84c | |||
42ea66c343 | |||
1565eb8d05 | |||
29750a3e51 | |||
3ea5170e6a | |||
fe4f497fad | |||
c20cd6bda1 | |||
1c4ce5d4a5 | |||
9c4d24d1f7 | |||
35e38d13c4 | |||
bf7eb0e727 | |||
5ee9c2b338 | |||
e15b121080 | |||
98452ccc18 | |||
a8e7022d04 | |||
60486bf5e3 | |||
46f3fd9682 | |||
56215382a3 | |||
9ab9ad0f56 | |||
0660a8772c | |||
f5c691f37b | |||
0067c30273 | |||
843db1727b | |||
e9ef59b641 | |||
fceffd42b9 | |||
b89b0cad53 | |||
c869ea9a73 | |||
9aee3e3e98 | |||
636b5c5b04 | |||
6984e8f25a | |||
e43cbb139c | |||
e57e547b74 | |||
3217536245 | |||
194b4e1a2f | |||
88c8830a17 | |||
7073584f1c | |||
8e6920af1d | |||
7bc184b252 | |||
4d237600d5 | |||
383907c2b8 | |||
f0ee3be6fb | |||
12a0b0b6f9 | |||
d716402da2 | |||
d0b71aba32 | |||
3f0ee6a961 | |||
b67e4699f5 | |||
68ba13ca12 | |||
3eb19135f5 | |||
549c445853 | |||
2466c1b1e4 | |||
f5867c3643 | |||
231c498def | |||
eeaec413d6 | |||
515f894c13 | |||
e8f6ba5b08 | |||
07276bbde4 | |||
9ad92e1860 | |||
7923d4a2cd | |||
9248358169 | |||
6ab52bc9a9 | |||
48213955b9 | |||
682b3b91aa | |||
be3e27aa19 | |||
c09f751552 | |||
fb5864ee00 | |||
3bd5b704c7 | |||
139ce47b73 | |||
45563d9a59 | |||
e2fda1fb84 | |||
a77fbb894f | |||
fe50a90235 | |||
a8be84028c | |||
113e9bd2fb | |||
daabb76781 | |||
dd24cb1300 |
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
|
@ -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 }}
|
||||
|
||||
|
|
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
|
@ -20,6 +20,7 @@ jobs:
|
|||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "latest"
|
||||
registry-url: "https://registry.npmjs.org/"
|
||||
|
||||
- name: Install
|
||||
|
@ -33,9 +34,6 @@ jobs:
|
|||
- name: Test
|
||||
run: yarn test
|
||||
|
||||
- name: Update npm
|
||||
run: npm install -g npm
|
||||
|
||||
- name: Publish latest
|
||||
if: "!contains(github.ref, '-')"
|
||||
run: npm publish --tag latest --provenance
|
||||
|
|
127
CHANGELOG.md
127
CHANGELOG.md
|
@ -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
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
<script lang="ts">
|
||||
import {filter as fuzzyFilter} from "fuzzy";
|
||||
import {computed, defineComponent, nextTick, PropType, ref} from "vue";
|
||||
import type {UserInMessage} from "../../server/models/msg";
|
||||
import type {UserInMessage} from "../../shared/types/msg";
|
||||
import type {ClientChan, ClientUser} from "../js/types";
|
||||
import Username from "./Username.vue";
|
||||
|
||||
|
@ -104,7 +104,7 @@ export default defineComponent({
|
|||
const result = filteredUsers.value;
|
||||
|
||||
for (const user of result) {
|
||||
const mode = user.original.modes[0] || "";
|
||||
const mode: string = user.original.modes[0] || "";
|
||||
|
||||
if (!groups[mode]) {
|
||||
groups[mode] = [];
|
||||
|
|
|
@ -41,9 +41,9 @@
|
|||
<script lang="ts">
|
||||
import Mousetrap from "mousetrap";
|
||||
import {computed, defineComponent, ref, watch} from "vue";
|
||||
import {onBeforeRouteLeave, onBeforeRouteUpdate} from "vue-router";
|
||||
import eventbus from "../js/eventbus";
|
||||
import {ClientChan, ClientMessage, ClientLinkPreview} from "../js/types";
|
||||
import {ClientChan, ClientLinkPreview} from "../js/types";
|
||||
import {SharedMsg} from "../../shared/types/msg";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ImageViewer",
|
||||
|
@ -104,9 +104,9 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
const links = channel.value.messages
|
||||
.map((msg) => msg.previews)
|
||||
.map((msg: SharedMsg) => msg.previews)
|
||||
.flat()
|
||||
.filter((preview) => preview.thumb);
|
||||
.filter((preview) => preview && preview.thumb);
|
||||
|
||||
const currentIndex = links.indexOf(link.value);
|
||||
|
||||
|
|
|
@ -150,10 +150,14 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
const messageComponent = computed(() => {
|
||||
return "message-" + props.message.type;
|
||||
return "message-" + (props.message.type || "invalid"); // TODO: force existence of type in sharedmsg
|
||||
});
|
||||
|
||||
const isAction = () => {
|
||||
if (!props.message.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return typeof MessageTypes["message-" + props.message.type] !== "undefined";
|
||||
};
|
||||
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -26,36 +26,43 @@ export default defineComponent({
|
|||
},
|
||||
setup(props) {
|
||||
const errorMessage = computed(() => {
|
||||
// TODO: enforce chan and nick fields so that we can get rid of that
|
||||
const chan = props.message.channel || "!UNKNOWN_CHAN";
|
||||
const nick = props.message.nick || "!UNKNOWN_NICK";
|
||||
|
||||
switch (props.message.error) {
|
||||
case "bad_channel_key":
|
||||
return `Cannot join ${props.message.channel} - Bad channel key.`;
|
||||
return `Cannot join ${chan} - Bad channel key.`;
|
||||
case "banned_from_channel":
|
||||
return `Cannot join ${props.message.channel} - You have been banned from the channel.`;
|
||||
return `Cannot join ${chan} - You have been banned from the channel.`;
|
||||
case "cannot_send_to_channel":
|
||||
return `Cannot send to channel ${props.message.channel}`;
|
||||
return `Cannot send to channel ${chan}`;
|
||||
case "channel_is_full":
|
||||
return `Cannot join ${props.message.channel} - Channel is full.`;
|
||||
return `Cannot join ${chan} - Channel is full.`;
|
||||
case "chanop_privs_needed":
|
||||
return "Cannot perform action: You're not a channel operator.";
|
||||
case "invite_only_channel":
|
||||
return `Cannot join ${props.message.channel} - Channel is invite only.`;
|
||||
return `Cannot join ${chan} - Channel is invite only.`;
|
||||
case "no_such_nick":
|
||||
return `User ${props.message.nick} hasn't logged in or does not exist.`;
|
||||
return `User ${nick} hasn't logged in or does not exist.`;
|
||||
case "not_on_channel":
|
||||
return "Cannot perform action: You're not on the channel.";
|
||||
case "password_mismatch":
|
||||
return "Password mismatch.";
|
||||
case "too_many_channels":
|
||||
return `Cannot join ${props.message.channel} - You've already reached the maximum number of channels allowed.`;
|
||||
return `Cannot join ${chan} - You've already reached the maximum number of channels allowed.`;
|
||||
case "unknown_command":
|
||||
return `Unknown command: ${props.message.command}`;
|
||||
// TODO: not having message.command should never happen, so force existence
|
||||
return `Unknown command: ${props.message.command || "!UNDEFINED_COMMAND_BUG"}`;
|
||||
case "user_not_in_channel":
|
||||
return `User ${props.message.nick} is not on the channel.`;
|
||||
return `User ${nick} is not on the channel.`;
|
||||
case "user_on_channel":
|
||||
return `User ${props.message.nick} is already on the channel.`;
|
||||
return `User ${nick} is already on the channel.`;
|
||||
default:
|
||||
if (props.message.reason) {
|
||||
return `${props.message.reason} (${props.message.error})`;
|
||||
return `${props.message.reason} (${
|
||||
props.message.error || "!UNDEFINED_ERR"
|
||||
})`;
|
||||
}
|
||||
|
||||
return props.message.error;
|
||||
|
|
|
@ -498,6 +498,7 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
watch(
|
||||
// eslint-disable-next-line
|
||||
() => props.defaults?.commands,
|
||||
() => {
|
||||
void nextTick(() => {
|
||||
|
@ -507,6 +508,7 @@ export default defineComponent({
|
|||
);
|
||||
|
||||
watch(
|
||||
// eslint-disable-next-line
|
||||
() => props.defaults?.tls,
|
||||
(isSecureChecked) => {
|
||||
const ports = [6667, 6697];
|
||||
|
|
|
@ -309,8 +309,7 @@ export default defineComponent({
|
|||
|
||||
moveItemInArray(store.state.networks, oldIndex, newIndex);
|
||||
|
||||
socket.emit("sort", {
|
||||
type: "networks",
|
||||
socket.emit("sort:networks", {
|
||||
order: store.state.networks.map((n) => n.uuid),
|
||||
});
|
||||
};
|
||||
|
@ -341,9 +340,8 @@ export default defineComponent({
|
|||
|
||||
moveItemInArray(netChan.network.channels, oldIndex, newIndex);
|
||||
|
||||
socket.emit("sort", {
|
||||
type: "channels",
|
||||
target: netChan.network.uuid,
|
||||
socket.emit("sort:channel", {
|
||||
network: netChan.network.uuid,
|
||||
order: netChan.network.channels.map((c) => c.id),
|
||||
});
|
||||
};
|
||||
|
|
|
@ -12,10 +12,10 @@
|
|||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, PropType} from "vue";
|
||||
import {UserInMessage} from "../../server/models/msg";
|
||||
import {UserInMessage} from "../../shared/types/msg";
|
||||
import eventbus from "../js/eventbus";
|
||||
import colorClass from "../js/helpers/colorClass";
|
||||
import type {ClientChan, ClientNetwork, ClientUser} from "../js/types";
|
||||
import type {ClientChan, ClientNetwork} from "../js/types";
|
||||
import {useStore} from "../js/store";
|
||||
|
||||
type UsernameUser = Partial<UserInMessage> & {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
35
client/js/chan.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import {ClientChan, ClientMessage} from "./types";
|
||||
import {SharedNetworkChan} from "../../shared/types/network";
|
||||
import {SharedMsg} from "../../shared/types/msg";
|
||||
|
||||
export function toClientChan(shared: SharedNetworkChan): ClientChan {
|
||||
const history: string[] = [""].concat(
|
||||
shared.messages
|
||||
.filter((m) => m.self && m.text && m.type === "message")
|
||||
// TS is too stupid to see the nil guard on filter... so we monkey patch it
|
||||
.map((m): string => (m.text ? m.text : ""))
|
||||
.reverse()
|
||||
.slice(0, 99)
|
||||
);
|
||||
// filter the unused vars
|
||||
const {messages, totalMessages: _, ...props} = shared;
|
||||
const channel: ClientChan = {
|
||||
...props,
|
||||
editTopic: false,
|
||||
pendingMessage: "",
|
||||
inputHistoryPosition: 0,
|
||||
historyLoading: false,
|
||||
scrolledToBottom: true,
|
||||
users: [],
|
||||
usersOutdated: shared.type === "channel" ? true : false,
|
||||
moreHistoryAvailable: shared.totalMessages > shared.messages.length,
|
||||
inputHistory: history,
|
||||
messages: sharedMsgToClientMsg(messages),
|
||||
};
|
||||
return channel;
|
||||
}
|
||||
|
||||
function sharedMsgToClientMsg(shared: SharedMsg[]): ClientMessage[] {
|
||||
// TODO: this is a stub for now, we will want to populate client specific stuff here
|
||||
return shared;
|
||||
}
|
|
@ -11,7 +11,7 @@ function input() {
|
|||
for (const message of store.state.activeChannel.channel.messages) {
|
||||
let toggled = false;
|
||||
|
||||
for (const preview of message.previews) {
|
||||
for (const preview of message.previews || []) {
|
||||
if (preview.shown) {
|
||||
preview.shown = false;
|
||||
toggled = true;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,17 +32,15 @@ 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
|
||||
if (!handledQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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"
|
||||
) {
|
||||
if (!router.currentRoute?.value?.name || router.currentRoute?.value?.name === "SignIn") {
|
||||
const channel = store.getters.findChannel(data.active);
|
||||
|
||||
if (channel) {
|
||||
|
@ -55,30 +54,32 @@ socket.on("init", async function (data) {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
emitNamesOrMarkUsersOudated(currentChannel); // TODO: this should not carry logic like that
|
||||
|
||||
// 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);
|
||||
if (currentChannel.messages && newChannel.messages.length < 100) {
|
||||
currentChannel.messages = currentChannel.messages.concat(newChannel.messages);
|
||||
} else {
|
||||
currentChannel[key] = channel[key];
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
return newChannels;
|
||||
if (key === "messages") {
|
||||
// already handled
|
||||
continue;
|
||||
}
|
||||
|
||||
currentChannel[key] = newChannel[key];
|
||||
}
|
||||
|
||||
result.push(currentChannel);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function emitNamesOrMarkUsersOudated(chan: ClientChan) {
|
||||
if (store.state.activeChannel && store.state.activeChannel.channel === chan) {
|
||||
// For currently open channel, request the user list straight away
|
||||
socket.emit("names", {
|
||||
target: chan.id,
|
||||
});
|
||||
chan.usersOutdated = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// For all other channels, mark the user list as outdated
|
||||
// so an update will be requested whenever user switches to these channels
|
||||
chan.usersOutdated = true;
|
||||
}
|
||||
|
||||
async function handleQueryParams() {
|
||||
|
@ -170,30 +172,28 @@ async function handleQueryParams() {
|
|||
|
||||
const params = new URLSearchParams(document.location.search);
|
||||
|
||||
const cleanParams = () => {
|
||||
// Remove query parameters from url without reloading the page
|
||||
const cleanUri = window.location.origin + window.location.pathname + window.location.hash;
|
||||
window.history.replaceState({}, document.title, cleanUri);
|
||||
};
|
||||
|
||||
if (params.has("uri")) {
|
||||
// Set default connection settings from IRC protocol links
|
||||
const uri = params.get("uri");
|
||||
const queryParams = parseIrcUri(String(uri));
|
||||
|
||||
cleanParams();
|
||||
removeQueryParams();
|
||||
await router.push({name: "Connect", query: queryParams});
|
||||
|
||||
return true;
|
||||
} else if (document.body.classList.contains("public") && document.location.search) {
|
||||
}
|
||||
|
||||
if (document.body.classList.contains("public") && document.location.search) {
|
||||
// Set default connection settings from url params
|
||||
const queryParams = Object.fromEntries(params.entries());
|
||||
|
||||
cleanParams();
|
||||
removeQueryParams();
|
||||
await router.push({name: "Connect", query: queryParams});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove query parameters from url without reloading the page
|
||||
function removeQueryParams() {
|
||||
const cleanUri = window.location.origin + window.location.pathname + window.location.hash;
|
||||
window.history.replaceState(null, "", cleanUri);
|
||||
}
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
import {switchToChannel} from "../router";
|
||||
import {ClientChan} from "../types";
|
||||
import {toClientChan} from "../chan";
|
||||
|
||||
socket.on("join", function (data) {
|
||||
store.getters.initChannel(data.chan);
|
||||
|
||||
const network = store.getters.findNetwork(data.network);
|
||||
|
||||
if (!network) {
|
||||
return;
|
||||
}
|
||||
|
||||
network.channels.splice(data.index || -1, 0, data.chan);
|
||||
const clientChan: ClientChan = toClientChan(data.chan);
|
||||
network.channels.splice(data.index || -1, 0, clientChan);
|
||||
|
||||
// Queries do not automatically focus, unless the user did a whois
|
||||
if (data.chan.type === "query" && !data.shouldOpen) {
|
||||
|
|
|
@ -1,7 +1,17 @@
|
|||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
import {ClientMention} from "../types";
|
||||
import {SharedMention} from "../../../shared/types/mention";
|
||||
|
||||
socket.on("mentions:list", function (data) {
|
||||
store.commit("mentions", data as ClientMention[]);
|
||||
store.commit("mentions", data.map(sharedToClientMention));
|
||||
});
|
||||
|
||||
function sharedToClientMention(shared: SharedMention): ClientMention {
|
||||
const mention: ClientMention = {
|
||||
...shared,
|
||||
localetime: "", // TODO: can't be right
|
||||
channel: null,
|
||||
};
|
||||
return mention;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import {nextTick} from "vue";
|
|||
|
||||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
import {ClientMessage} from "../types";
|
||||
import 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;
|
||||
|
|
|
@ -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
|
||||
|
||||
case "action": {
|
||||
const user = channel.users.find((u) => u.nick === msg.from?.nick);
|
||||
|
||||
if (user) {
|
||||
user.lastMessage = new Date(msg.time).getTime() || Date.now();
|
||||
}
|
||||
} else if (msg.type === "quit" || msg.type === "part") {
|
||||
const idx = channel.users.findIndex((u) => u.nick === msg.from.nick);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "quit": // fallthrough
|
||||
|
||||
case "part": {
|
||||
const idx = channel.users.findIndex((u) => u.nick === msg.from?.nick);
|
||||
|
||||
if (idx > -1) {
|
||||
channel.users.splice(idx, 1);
|
||||
}
|
||||
} else if (msg.type === "kick") {
|
||||
const idx = channel.users.findIndex((u) => u.nick === msg.target.nick);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "kick": {
|
||||
const idx = channel.users.findIndex((u) => u.nick === msg.target?.nick);
|
||||
|
||||
if (idx > -1) {
|
||||
channel.users.splice(idx, 1);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ socket.on("msg:preview", function (data) {
|
|||
const netChan = store.getters.findChannel(data.chan);
|
||||
const message = netChan?.channel.messages.find((m) => m.id === data.id);
|
||||
|
||||
if (!message) {
|
||||
if (!message || !message.previews) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
import {switchToChannel} from "../router";
|
||||
import {toClientChan} from "../chan";
|
||||
import {ClientNetwork} from "../types";
|
||||
import {ChanState} from "../../../shared/types/chan";
|
||||
|
||||
socket.on("network", function (data) {
|
||||
const network = data.networks[0];
|
||||
|
||||
network.isJoinChannelShown = false;
|
||||
network.isCollapsed = false;
|
||||
network.channels.forEach(store.getters.initChannel);
|
||||
const network: ClientNetwork = {
|
||||
...data.network,
|
||||
channels: data.network.channels.map(toClientChan),
|
||||
isJoinChannelShown: false,
|
||||
isCollapsed: false,
|
||||
};
|
||||
|
||||
store.commit("networks", [...store.state.networks, network]);
|
||||
|
||||
|
@ -19,7 +23,7 @@ socket.on("network:options", function (data) {
|
|||
const network = store.getters.findNetwork(data.network);
|
||||
|
||||
if (network) {
|
||||
network.serverOptions = data.serverOptions as typeof network.serverOptions;
|
||||
network.serverOptions = data.serverOptions;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -35,8 +39,8 @@ socket.on("network:status", function (data) {
|
|||
|
||||
if (!data.connected) {
|
||||
network.channels.forEach((channel) => {
|
||||
channel.users = [];
|
||||
channel.state = 0;
|
||||
channel.users = []; // TODO: untangle this
|
||||
channel.state = ChanState.PARTED;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,30 +1,16 @@
|
|||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
|
||||
socket.on("sync_sort", function (data) {
|
||||
const order = data.order;
|
||||
socket.on("sync_sort:networks", function (data) {
|
||||
store.commit("sortNetworks", (a, b) => data.order.indexOf(a.uuid) - data.order.indexOf(b.uuid));
|
||||
});
|
||||
|
||||
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);
|
||||
socket.on("sync_sort:channels", function (data) {
|
||||
const network = store.getters.findNetwork(data.network);
|
||||
|
||||
if (!network) {
|
||||
return;
|
||||
}
|
||||
|
||||
network.channels.sort(
|
||||
(a, b) => (order as number[]).indexOf(a.id) - (order as number[]).indexOf(b.id)
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
network.channels.sort((a, b) => data.order.indexOf(a.id) - data.order.indexOf(b.id));
|
||||
});
|
||||
|
|
|
@ -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']"),
|
||||
|
|
|
@ -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
36
client/js/types.d.ts
vendored
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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": {
|
||||
|
|
22
package.json
22
package.json
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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") {
|
||||
if (!author) {
|
||||
// author can be null if GH doesn't recognize them
|
||||
return memo;
|
||||
}
|
||||
|
||||
switch (__typename) {
|
||||
case "PullRequest":
|
||||
if (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);
|
||||
}
|
||||
|
||||
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"}));
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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,27 +694,8 @@ class Client {
|
|||
this.emit("open", targetNetChan.chan.id);
|
||||
}
|
||||
|
||||
sort(data: {order: Order; type: "networks" | "channels"; target: string}) {
|
||||
const order = data.order;
|
||||
|
||||
if (!_.isArray(order)) {
|
||||
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});
|
||||
sortChannels(netid: SharedNetwork["uuid"], order: SharedChan["id"][]) {
|
||||
const network = _.find(this.networks, {uuid: netid});
|
||||
|
||||
if (!network) {
|
||||
return;
|
||||
|
@ -734,19 +712,21 @@ class Client {
|
|||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
) {
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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") {
|
||||
getFilteredClone(
|
||||
lastActiveChannel?: number | boolean,
|
||||
lastMessage?: number
|
||||
): SharedNetworkChan {
|
||||
let msgs: SharedMsg[];
|
||||
|
||||
// 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);
|
||||
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;
|
||||
|
||||
newChannel[prop] = this[prop].slice(-messagesToSend);
|
||||
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,
|
||||
|
||||
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];
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
return newChannel;
|
||||
}, {}) as FilteredChannel;
|
||||
}
|
||||
writeUserLog(client: Client, msg: Msg) {
|
||||
this.messages.push(msg);
|
||||
|
||||
|
@ -270,6 +264,7 @@ class Chan {
|
|||
messageStorage.index(target.network, targetChannel, msg).catch((e) => log.error(e));
|
||||
}
|
||||
}
|
||||
|
||||
loadMessages(client: Client, network: Network) {
|
||||
if (!this.isLoggable()) {
|
||||
return;
|
||||
|
@ -326,15 +321,23 @@ class Chan {
|
|||
log.error(`Failed to load messages for ${client.name}: ${err.toString()}`)
|
||||
);
|
||||
}
|
||||
|
||||
isLoggable() {
|
||||
return this.type === ChanType.CHANNEL || this.type === ChanType.QUERY;
|
||||
}
|
||||
|
||||
setMuteStatus(muted: boolean) {
|
||||
this.muted = !!muted;
|
||||
}
|
||||
}
|
||||
|
||||
function requestZncPlayback(channel, network, from) {
|
||||
function requestZncPlayback(channel: Chan, network: Network, from: number) {
|
||||
if (!network.irc) {
|
||||
throw new Error(
|
||||
`requestZncPlayback: no irc field on network "${network.name}", this is a bug`
|
||||
);
|
||||
}
|
||||
|
||||
network.irc.raw("ZNC", "*playback", "PLAY", channel.name, from.toString());
|
||||
}
|
||||
|
||||
|
|
|
@ -1,41 +1,5 @@
|
|||
import _ from "lodash";
|
||||
import {LinkPreview} from "../plugins/irc-events/link";
|
||||
import User from "./user";
|
||||
|
||||
export type UserInMessage = Partial<User> & {
|
||||
mode: string;
|
||||
};
|
||||
|
||||
export enum MessageType {
|
||||
UNHANDLED = "unhandled",
|
||||
ACTION = "action",
|
||||
AWAY = "away",
|
||||
BACK = "back",
|
||||
ERROR = "error",
|
||||
INVITE = "invite",
|
||||
JOIN = "join",
|
||||
KICK = "kick",
|
||||
LOGIN = "login",
|
||||
LOGOUT = "logout",
|
||||
MESSAGE = "message",
|
||||
MODE = "mode",
|
||||
MODE_CHANNEL = "mode_channel",
|
||||
MODE_USER = "mode_user", // RPL_UMODEIS
|
||||
MONOSPACE_BLOCK = "monospace_block",
|
||||
NICK = "nick",
|
||||
NOTICE = "notice",
|
||||
PART = "part",
|
||||
QUIT = "quit",
|
||||
CTCP = "ctcp",
|
||||
CTCP_REQUEST = "ctcp_request",
|
||||
CHGHOST = "chghost",
|
||||
TOPIC = "topic",
|
||||
TOPIC_SET_BY = "topic_set_by",
|
||||
WHOIS = "whois",
|
||||
RAW = "raw",
|
||||
PLUGIN = "plugin",
|
||||
WALLOPS = "wallops",
|
||||
}
|
||||
import {MessageType, LinkPreview, UserInMessage} from "../../shared/types/msg";
|
||||
|
||||
class Msg {
|
||||
from!: UserInMessage;
|
||||
|
@ -70,7 +34,7 @@ class Msg {
|
|||
raw_modes!: any;
|
||||
when!: Date;
|
||||
whois!: any;
|
||||
users!: UserInMessage[] | string[];
|
||||
users!: string[];
|
||||
statusmsgGroup!: string;
|
||||
params!: string[];
|
||||
|
||||
|
|
|
@ -1,24 +1,17 @@
|
|||
import _ from "lodash";
|
||||
import {v4 as uuidv4} from "uuid";
|
||||
import IrcFramework, {Client as IRCClient} from "irc-framework";
|
||||
import Chan, {ChanConfig, Channel, ChanType} from "./chan";
|
||||
import Msg, {MessageType} from "./msg";
|
||||
import Chan, {ChanConfig, Channel} from "./chan";
|
||||
import Msg from "./msg";
|
||||
import Prefix from "./prefix";
|
||||
import Helper, {Hostmask} from "../helper";
|
||||
import Config, {WebIRC} from "../config";
|
||||
import STSPolicies from "../plugins/sts";
|
||||
import ClientCertificate, {ClientCertificateType} from "../plugins/clientCertificate";
|
||||
import Client from "../client";
|
||||
|
||||
/**
|
||||
* List of keys which should be sent to the client by default.
|
||||
*/
|
||||
const fieldsForClient = {
|
||||
uuid: true,
|
||||
name: true,
|
||||
nick: true,
|
||||
serverOptions: true,
|
||||
};
|
||||
import {MessageType} from "../../shared/types/msg";
|
||||
import {ChanType} from "../../shared/types/chan";
|
||||
import {SharedNetwork} from "../../shared/types/network";
|
||||
|
||||
type NetworkIrcOptions = {
|
||||
host: string;
|
||||
|
@ -52,7 +45,7 @@ type NetworkStatus = {
|
|||
};
|
||||
|
||||
export type IgnoreListItem = Hostmask & {
|
||||
when?: number;
|
||||
when: number;
|
||||
};
|
||||
|
||||
type IgnoreList = IgnoreListItem[];
|
||||
|
@ -505,24 +498,17 @@ class Network {
|
|||
}
|
||||
}
|
||||
|
||||
getFilteredClone(lastActiveChannel?: number, lastMessage?: number) {
|
||||
const filteredNetwork = Object.keys(this).reduce((newNetwork, prop) => {
|
||||
if (prop === "channels") {
|
||||
// Channels objects perform their own cloning
|
||||
newNetwork[prop] = this[prop].map((channel) =>
|
||||
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)
|
||||
);
|
||||
} 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;
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
getNetworkStatus() {
|
||||
|
|
|
@ -67,11 +67,11 @@ function advancedLdapAuth(user: string, password: string, callback: (success: bo
|
|||
});
|
||||
|
||||
const base = config.ldap.searchDN.base;
|
||||
const searchOptions = {
|
||||
const searchOptions: SearchOptions = {
|
||||
scope: config.ldap.searchDN.scope,
|
||||
filter: `(&(${config.ldap.primaryKey}=${userDN})${config.ldap.searchDN.filter})`,
|
||||
attributes: ["dn"],
|
||||
} as SearchOptions;
|
||||
};
|
||||
|
||||
ldapclient.on("error", function (err: Error) {
|
||||
log.error(`Unable to connect to LDAP server: ${err.toString()}`);
|
||||
|
@ -178,12 +178,12 @@ function advancedLdapLoadUsers(users: string[], callbackLoadUser) {
|
|||
|
||||
const remainingUsers = new Set(users);
|
||||
|
||||
const searchOptions = {
|
||||
const searchOptions: SearchOptions = {
|
||||
scope: config.ldap.searchDN.scope,
|
||||
filter: `${config.ldap.searchDN.filter}`,
|
||||
attributes: [config.ldap.primaryKey],
|
||||
paged: true,
|
||||
} as SearchOptions;
|
||||
};
|
||||
|
||||
ldapclient.search(base, searchOptions, function (err2, res) {
|
||||
if (err2) {
|
||||
|
|
|
@ -4,6 +4,7 @@ import log from "../log";
|
|||
import pkg from "../../package.json";
|
||||
import ClientManager from "../clientManager";
|
||||
import Config from "../config";
|
||||
import {SharedChangelogData} from "../../shared/types/changelog";
|
||||
|
||||
const TIME_TO_LIVE = 15 * 60 * 1000; // 15 minutes, in milliseconds
|
||||
|
||||
|
@ -12,31 +13,17 @@ export default {
|
|||
fetch,
|
||||
checkForUpdates,
|
||||
};
|
||||
export type ChangelogData = {
|
||||
current: {
|
||||
prerelease: boolean;
|
||||
version: string;
|
||||
changelog?: string;
|
||||
url: string;
|
||||
};
|
||||
expiresAt: number;
|
||||
latest?: {
|
||||
prerelease: boolean;
|
||||
version: string;
|
||||
url: string;
|
||||
};
|
||||
packages?: boolean;
|
||||
};
|
||||
|
||||
const versions = {
|
||||
const versions: SharedChangelogData = {
|
||||
current: {
|
||||
prerelease: false,
|
||||
version: `v${pkg.version}`,
|
||||
changelog: undefined,
|
||||
url: "", // TODO: properly init
|
||||
},
|
||||
expiresAt: -1,
|
||||
latest: undefined,
|
||||
packages: undefined,
|
||||
} as ChangelogData;
|
||||
};
|
||||
|
||||
async function fetch() {
|
||||
const time = Date.now();
|
||||
|
|
|
@ -31,7 +31,7 @@ function get(uuid: string): ClientCertificateType | null {
|
|||
return {
|
||||
private_key: fs.readFileSync(paths.privateKeyPath, "utf-8"),
|
||||
certificate: fs.readFileSync(paths.certificatePath, "utf-8"),
|
||||
} as ClientCertificateType;
|
||||
};
|
||||
} catch (e: any) {
|
||||
log.error("Unable to get certificate", e);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import {ChanType} from "../../models/chan";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["slap", "me"];
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {ChanType} from "../../models/chan";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {PluginInputHandler} from "./index";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["ban", "unban", "banlist", "kickban"];
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {PluginInputHandler} from "./index";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
const commands = ["connect", "server"];
|
||||
const allowDisconnected = true;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {PluginInputHandler} from "./index";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
const commands = ["ctcp"];
|
||||
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import Helper from "../../helper";
|
||||
import {PluginInputHandler} from "./index";
|
||||
import {IgnoreListItem} from "../../models/network";
|
||||
import {ChanType, SpecialChanType} from "../../models/chan";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
const commands = ["ignore", "unignore", "ignorelist"];
|
||||
const commands = ["ignore", "unignore"];
|
||||
|
||||
const input: PluginInputHandler = function (network, chan, cmd, args) {
|
||||
const client = this;
|
||||
let target: string;
|
||||
// let hostmask: cmd === "ignoreList" ? string : undefined;
|
||||
let hostmask: IgnoreListItem | undefined;
|
||||
|
||||
if (cmd !== "ignorelist" && (args.length === 0 || args[0].trim().length === 0)) {
|
||||
if (args.length === 0 || args[0].trim().length === 0) {
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
|
@ -24,16 +20,13 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (cmd !== "ignorelist") {
|
||||
// Trim to remove any spaces from the hostmask
|
||||
target = args[0].trim();
|
||||
hostmask = Helper.parseHostmask(target) as IgnoreListItem;
|
||||
}
|
||||
const target = args[0].trim();
|
||||
const hostmask = Helper.parseHostmask(target);
|
||||
|
||||
switch (cmd) {
|
||||
case "ignore": {
|
||||
// IRC nicks are case insensitive
|
||||
if (hostmask!.nick.toLowerCase() === network.nick.toLowerCase()) {
|
||||
if (hostmask.nick.toLowerCase() === network.nick.toLowerCase()) {
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
|
@ -41,25 +34,14 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
|||
text: "You can't ignore yourself",
|
||||
})
|
||||
);
|
||||
} else if (
|
||||
!network.ignoreList.some(function (entry) {
|
||||
return Helper.compareHostmask(entry, hostmask!);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
network.ignoreList.some(function (entry) {
|
||||
return Helper.compareHostmask(entry, hostmask);
|
||||
})
|
||||
) {
|
||||
hostmask!.when = Date.now();
|
||||
network.ignoreList.push(hostmask!);
|
||||
|
||||
client.save();
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: MessageType.ERROR,
|
||||
text: `\u0002${hostmask!.nick}!${hostmask!.ident}@${
|
||||
hostmask!.hostname
|
||||
}\u000f added to ignorelist`,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
|
@ -67,32 +49,31 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
|||
text: "The specified user/hostmask is already ignored",
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
network.ignoreList.push({
|
||||
...hostmask,
|
||||
when: Date.now(),
|
||||
});
|
||||
|
||||
client.save();
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: MessageType.ERROR, // TODO: Successfully added via type.Error 🤔 ?
|
||||
text: `\u0002${hostmask.nick}!${hostmask.ident}@${hostmask.hostname}\u000f added to ignorelist`,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
case "unignore": {
|
||||
const idx = network.ignoreList.findIndex(function (entry) {
|
||||
return Helper.compareHostmask(entry, hostmask!);
|
||||
return Helper.compareHostmask(entry, hostmask);
|
||||
});
|
||||
|
||||
// Check if the entry exists before removing it, otherwise
|
||||
// let the user know.
|
||||
if (idx !== -1) {
|
||||
network.ignoreList.splice(idx, 1);
|
||||
client.save();
|
||||
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: MessageType.ERROR,
|
||||
text: `Successfully removed \u0002${hostmask!.nick}!${hostmask!.ident}@${
|
||||
hostmask!.hostname
|
||||
}\u000f from ignorelist`,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
if (idx === -1) {
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
|
@ -100,53 +81,21 @@ 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();
|
||||
|
||||
case "ignorelist":
|
||||
if (network.ignoreList.length === 0) {
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: MessageType.ERROR,
|
||||
text: "Ignorelist is empty",
|
||||
type: MessageType.ERROR, // TODO: Successfully removed via type.Error 🤔 ?
|
||||
text: `Successfully removed \u0002${hostmask.nick}!${hostmask.ident}@${hostmask.hostname}\u000f from ignorelist`,
|
||||
})
|
||||
);
|
||||
} 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;
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
57
server/plugins/inputs/ignorelist.ts
Normal file
57
server/plugins/inputs/ignorelist.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
import Msg from "../../models/msg";
|
||||
import {ChanType, SpecialChanType} from "../../../shared/types/chan";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
const commands = ["ignorelist"];
|
||||
|
||||
const input: PluginInputHandler = function (network, chan, _cmd, _args) {
|
||||
const client = this;
|
||||
|
||||
if (network.ignoreList.length === 0) {
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: MessageType.ERROR,
|
||||
text: "Ignorelist is empty",
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const chanName = "Ignored users";
|
||||
const ignored = network.ignoreList.map((data) => ({
|
||||
hostmask: `${data.nick}!${data.ident}@${data.hostname}`,
|
||||
when: data.when,
|
||||
}));
|
||||
let newChan = network.getChannel(chanName);
|
||||
|
||||
if (typeof newChan === "undefined") {
|
||||
newChan = client.createChannel({
|
||||
type: ChanType.SPECIAL,
|
||||
special: SpecialChanType.IGNORELIST,
|
||||
name: chanName,
|
||||
data: ignored,
|
||||
});
|
||||
client.emit("join", {
|
||||
network: network.uuid,
|
||||
chan: newChan.getFilteredClone(true),
|
||||
shouldOpen: false,
|
||||
index: network.addChannel(newChan),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: add type for this chan/event
|
||||
newChan.data = ignored;
|
||||
|
||||
client.emit("msg:special", {
|
||||
chan: newChan.id,
|
||||
data: ignored,
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
commands,
|
||||
input,
|
||||
};
|
|
@ -54,6 +54,7 @@ const builtInInputs = [
|
|||
"ctcp",
|
||||
"disconnect",
|
||||
"ignore",
|
||||
"ignorelist",
|
||||
"invite",
|
||||
"kick",
|
||||
"kill",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import {ChanType} from "../../models/chan";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["invite", "invitelist"];
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import {ChanType} from "../../models/chan";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["kick"];
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import {ChanType} from "../../models/chan";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["mode", "umode", "op", "deop", "hop", "dehop", "voice", "devoice"];
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Chan, {ChanType} from "../../models/chan";
|
||||
import Msg from "../../models/msg";
|
||||
import Chan from "../../models/chan";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["query", "msg", "say"];
|
||||
|
||||
|
@ -97,10 +99,10 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
|||
// being sent back to us.
|
||||
if (!network.irc.network.cap.isEnabled("echo-message")) {
|
||||
const parsedTarget = network.irc.network.extractTargetGroup(targetName);
|
||||
let targetGroup;
|
||||
let targetGroup: string | undefined = undefined;
|
||||
|
||||
if (parsedTarget) {
|
||||
targetName = parsedTarget.target as string;
|
||||
targetName = parsedTarget.target;
|
||||
targetGroup = parsedTarget.target_group;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,9 +2,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;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
const commands = ["nick"];
|
||||
const allowDisconnected = true;
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import Config from "../../config";
|
||||
import {ChanType, ChanState} from "../../models/chan";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType, ChanState} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["close", "leave", "part"];
|
||||
const allowDisconnected = true;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import {ChanType} from "../../models/chan";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["cycle", "rejoin"];
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import {ChanType} from "../../models/chan";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["topic"];
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
import {ChanType} from "../../models/chan";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -14,7 +14,7 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
handleSTS(data, false);
|
||||
});
|
||||
|
||||
function handleSTS(data, shouldReconnect) {
|
||||
function handleSTS(data, shouldReconnect: boolean) {
|
||||
if (!Object.prototype.hasOwnProperty.call(data.capabilities, "sts")) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {IrcEventHandler} from "../../client";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {IrcEventHandler} from "../../client";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import User from "../../models/user";
|
||||
import type {IrcEventHandler} from "../../client";
|
||||
import {ChanState} from "../../models/chan";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanState} from "../../../shared/types/chan";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
@ -18,6 +19,7 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
client.emit("join", {
|
||||
network: network.uuid,
|
||||
chan: chan.getFilteredClone(true),
|
||||
shouldOpen: false,
|
||||
index: network.addChannel(chan),
|
||||
});
|
||||
client.save();
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
import {ChanState} from "../../models/chan";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import User from "../../models/user";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanState} from "../../../shared/types/chan";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
@ -14,11 +14,12 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
return;
|
||||
}
|
||||
|
||||
const user = chan.getUser(data.kicked!);
|
||||
const msg = new Msg({
|
||||
type: MessageType.KICK,
|
||||
time: data.time,
|
||||
from: chan.getUser(data.nick),
|
||||
target: chan.getUser(data.kicked!),
|
||||
target: user,
|
||||
text: data.message || "",
|
||||
highlight: data.kicked === irc.user.nick,
|
||||
self: data.nick === irc.user.nick,
|
||||
|
@ -34,7 +35,7 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
state: chan.state,
|
||||
});
|
||||
} else {
|
||||
chan.removeUser(msg.target as User);
|
||||
chan.removeUser(user);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@ import mime from "mime-types";
|
|||
import log from "../../log";
|
||||
import Config from "../../config";
|
||||
import {findLinksWithSchema} from "../../../shared/linkify";
|
||||
import {LinkPreview} from "../../../shared/types/msg";
|
||||
import storage from "../storage";
|
||||
import Client from "../../client";
|
||||
import Chan from "../../models/chan";
|
||||
|
@ -20,23 +21,6 @@ const currentFetchPromises = new Map<string, Promise<FetchRequest>>();
|
|||
const imageTypeRegex = /^image\/.+/;
|
||||
const mediaTypeRegex = /^(audio|video)\/.+/;
|
||||
|
||||
export type LinkPreview = {
|
||||
type: string;
|
||||
head: string;
|
||||
body: string;
|
||||
thumb: string;
|
||||
size: number;
|
||||
link: string; // Send original matched link to the client
|
||||
shown?: boolean | null;
|
||||
error?: string;
|
||||
message?: string;
|
||||
|
||||
media?: string;
|
||||
mediaType?: string;
|
||||
maxSize?: number;
|
||||
thumbActualUrl?: string;
|
||||
};
|
||||
|
||||
export default function (client: Client, chan: Chan, msg: Msg, cleanText: string) {
|
||||
if (!Config.values.prefetch) {
|
||||
return;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Chan, {ChanType, SpecialChanType} from "../../models/chan";
|
||||
import Chan from "../../models/chan";
|
||||
import {ChanType, SpecialChanType} from "../../../shared/types/chan";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
@ -50,6 +51,7 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
client.emit("join", {
|
||||
network: network.uuid,
|
||||
chan: chan.getFilteredClone(true),
|
||||
shouldOpen: false,
|
||||
index: network.addChannel(chan),
|
||||
});
|
||||
} else {
|
||||
|
|
|
@ -1,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]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
import {SpecialChanType, ChanType} from "../../models/chan";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {SpecialChanType, ChanType} from "../../../shared/types/chan";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
@ -68,6 +69,7 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
client.emit("join", {
|
||||
network: network.uuid,
|
||||
chan: chan.getFilteredClone(true),
|
||||
shouldOpen: false,
|
||||
index: network.addChannel(chan),
|
||||
});
|
||||
} else {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
import {ChanType} from "../../models/chan";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
@ -28,9 +29,9 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
});
|
||||
|
||||
client.emit("join", {
|
||||
shouldOpen: true,
|
||||
network: network.uuid,
|
||||
chan: chan.getFilteredClone(true),
|
||||
shouldOpen: true,
|
||||
index: network.addChannel(chan),
|
||||
});
|
||||
chan.loadMessages(client, network);
|
||||
|
|
|
@ -7,8 +7,9 @@ import Config from "../../config";
|
|||
import Msg, {Message} from "../../models/msg";
|
||||
import Chan, {Channel} from "../../models/chan";
|
||||
import Helper from "../../helper";
|
||||
import type {SearchResponse, SearchQuery, SearchableMessageStorage, DeletionRequest} from "./types";
|
||||
import type {SearchableMessageStorage, DeletionRequest} from "./types";
|
||||
import Network from "../../models/network";
|
||||
import {SearchQuery, SearchResponse} from "../../../shared/types/storage";
|
||||
|
||||
// TODO; type
|
||||
let sqlite3: any;
|
||||
|
|
|
@ -6,8 +6,9 @@ import filenamify from "filenamify";
|
|||
import Config from "../../config";
|
||||
import {MessageStorage} from "./types";
|
||||
import Channel from "../../models/chan";
|
||||
import {Message, MessageType} from "../../models/msg";
|
||||
import {Message} from "../../models/msg";
|
||||
import Network from "../../models/network";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
class TextFileMessageStorage implements MessageStorage {
|
||||
isEnabled: boolean;
|
||||
|
|
14
server/plugins/messageStorage/types.d.ts
vendored
14
server/plugins/messageStorage/types.d.ts
vendored
|
@ -4,7 +4,8 @@ import {Channel} from "../../models/channel";
|
|||
import {Message} from "../../models/message";
|
||||
import {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 {
|
||||
|
|
|
@ -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: "",
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
272
server/server.ts
272
server/server.ts
|
@ -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,22 +396,21 @@ 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) => {
|
||||
fs.readFile(Utils.getFileFromRelativeToRoot("client/index.html.tpl"), "utf-8", (err, file) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
log.error(`failed to server index request: ${err.name}, ${err.message}`);
|
||||
res.sendStatus(500);
|
||||
return;
|
||||
}
|
||||
|
||||
const config: IndexTemplateConfiguration = {
|
||||
|
@ -426,8 +419,7 @@ function indexRequest(req: Request, res: Response) {
|
|||
};
|
||||
|
||||
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
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
const defaults: LockedConfigNetDefaults = {
|
||||
..._.pick(Config.values.defaults, ["name", "username", "password", "realname", "join"]),
|
||||
...defaultsOverride,
|
||||
};
|
||||
|
||||
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 = "";
|
||||
const result: LockedSharedConfiguration = {
|
||||
...common,
|
||||
lockNetwork: Config.values.lockNetwork,
|
||||
defaults: defaults,
|
||||
};
|
||||
|
||||
if (Uploader) {
|
||||
config.fileUploadMaxFileSize = Uploader.getMaxFileSize();
|
||||
}
|
||||
|
||||
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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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": [
|
||||
|
|
1
server/types/index.d.ts
vendored
1
server/types/index.d.ts
vendored
|
@ -1,2 +1 @@
|
|||
import "./modules";
|
||||
import "./socket-events";
|
||||
|
|
9
server/types/modules/irc-framework.d.ts
vendored
9
server/types/modules/irc-framework.d.ts
vendored
|
@ -33,8 +33,7 @@ declare module "irc-framework" {
|
|||
reply: (message: string) => void;
|
||||
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;
|
||||
};
|
||||
|
|
224
server/types/socket-events.d.ts
vendored
224
server/types/socket-events.d.ts
vendored
|
@ -1,224 +0,0 @@
|
|||
import {ClientMessage, ClientNetwork, InitClientChan} from "../../client/js/types";
|
||||
import {Mention} from "../client";
|
||||
import {ChanState} from "../models/chan";
|
||||
import Msg from "../models/msg";
|
||||
import Network from "../models/network";
|
||||
import User from "../models/user";
|
||||
import {ChangelogData} from "../plugins/changelog";
|
||||
import {LinkPreview} from "../plugins/irc-events/link";
|
||||
import {ClientConfiguration} from "../server";
|
||||
|
||||
type Session = {
|
||||
current: boolean;
|
||||
active: number;
|
||||
lastUse: number;
|
||||
ip: string;
|
||||
agent: string;
|
||||
token: string;
|
||||
};
|
||||
|
||||
interface ServerToClientEvents {
|
||||
"auth:failed": () => void;
|
||||
"auth:start": (serverHash: number) => void;
|
||||
"auth:success": () => void;
|
||||
|
||||
"upload:auth": (token: string) => void;
|
||||
|
||||
changelog: (data: ChangelogData) => void;
|
||||
"changelog:newversion": () => void;
|
||||
|
||||
"channel:state": (data: {chan: number; state: ChanState}) => void;
|
||||
|
||||
"change-password": ({success, error}: {success: boolean; error?: any}) => void;
|
||||
|
||||
commands: (data: string[]) => void;
|
||||
|
||||
configuration: (config: ClientConfiguration) => void;
|
||||
|
||||
"push:issubscribed": (isSubscribed: boolean) => void;
|
||||
"push:unregister": () => void;
|
||||
|
||||
"sessions:list": (data: Session[]) => void;
|
||||
|
||||
"mentions:list": (data: Mention[]) => void;
|
||||
|
||||
"setting:new": ({name: string, value: any}) => void;
|
||||
"setting:all": (settings: {[key: string]: any}) => void;
|
||||
|
||||
"history:clear": ({target}: {target: number}) => void;
|
||||
|
||||
"mute:changed": (response: {target: number; status: boolean}) => void;
|
||||
|
||||
names: (data: {id: number; users: User[]}) => void;
|
||||
|
||||
network: (data: {networks: ClientNetwork[]}) => void;
|
||||
"network:options": (data: {network: string; serverOptions: {[key: string]: any}}) => void;
|
||||
"network:status": (data: {network: string; connected: boolean; secure: boolean}) => void;
|
||||
"network:info": (data: {uuid: string}) => void;
|
||||
"network:name": (data: {uuid: string; name: string}) => void;
|
||||
|
||||
nick: (data: {network: string; nick: string}) => void;
|
||||
|
||||
open: (id: number) => void;
|
||||
|
||||
part: (data: {chan: number}) => void;
|
||||
|
||||
"sign-out": () => void;
|
||||
|
||||
sync_sort: (
|
||||
data:
|
||||
| {
|
||||
type: "networks";
|
||||
order: string[];
|
||||
target: string;
|
||||
}
|
||||
| {
|
||||
type: "channels";
|
||||
order: number[];
|
||||
target: string;
|
||||
}
|
||||
) => void;
|
||||
|
||||
topic: (data: {chan: number; topic: string}) => void;
|
||||
|
||||
users: (data: {chan: number}) => void;
|
||||
|
||||
more: ({
|
||||
chan,
|
||||
messages,
|
||||
totalMessages,
|
||||
}: {
|
||||
chan: number;
|
||||
messages: Msg[];
|
||||
totalMessages: number;
|
||||
}) => void;
|
||||
|
||||
"msg:preview": ({id, chan, preview}: {id: number; chan: number; preview: LinkPreview}) => void;
|
||||
"msg:special": (data: {chan: number; data?: Record<string, any>}) => void;
|
||||
msg: (data: {msg: ClientMessage; chan: number; highlight?: number; unread?: number}) => void;
|
||||
|
||||
init: ({
|
||||
active,
|
||||
networks,
|
||||
token,
|
||||
}: {
|
||||
active: number;
|
||||
networks: ClientNetwork[];
|
||||
token: string;
|
||||
}) => void;
|
||||
|
||||
"search:results": (response: SearchResponse) => void;
|
||||
|
||||
quit: (args: {network: string}) => void;
|
||||
|
||||
error: (error: any) => void;
|
||||
connecting: () => void;
|
||||
|
||||
join: (args: {
|
||||
shouldOpen: boolean;
|
||||
index: number;
|
||||
network: string;
|
||||
chan: InitClientChan;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
interface ClientToServerEvents {
|
||||
"auth:perform":
|
||||
| (({user, password}: {user: string; password: string}) => void)
|
||||
| (({
|
||||
user,
|
||||
token,
|
||||
lastMessage,
|
||||
openChannel,
|
||||
hasConfig,
|
||||
}: {
|
||||
user: string;
|
||||
token: string;
|
||||
lastMessage: number;
|
||||
openChannel: number | null;
|
||||
hasConfig: boolean;
|
||||
}) => void);
|
||||
|
||||
changelog: () => void;
|
||||
|
||||
"change-password": ({
|
||||
old_password: string,
|
||||
new_password: string,
|
||||
verify_password: string,
|
||||
}) => void;
|
||||
|
||||
open: (channelId: number) => void;
|
||||
|
||||
names: ({target: number}) => void;
|
||||
|
||||
input: ({target, text}: {target: number; text: string}) => void;
|
||||
|
||||
"upload:auth": () => void;
|
||||
"upload:ping": (token: string) => void;
|
||||
|
||||
"mute:change": (response: {target: number; setMutedTo: boolean}) => void;
|
||||
|
||||
"push:register": (subscriptionJson: PushSubscriptionJSON) => void;
|
||||
"push:unregister": () => void;
|
||||
|
||||
"setting:get": () => void;
|
||||
"setting:set": ({name: string, value: any}) => void;
|
||||
|
||||
"sessions:get": () => void;
|
||||
|
||||
sort: ({type, order}: {type: string; order: any; target?: string}) => void;
|
||||
|
||||
"mentions:dismiss": (msgId: number) => void;
|
||||
"mentions:dismiss_all": () => void;
|
||||
"mentions:get": () => void;
|
||||
|
||||
more: ({
|
||||
target,
|
||||
lastId,
|
||||
condensed,
|
||||
}: {
|
||||
target: number;
|
||||
lastId: number;
|
||||
condensed: boolean;
|
||||
}) => void;
|
||||
|
||||
"msg:preview:toggle": ({
|
||||
target,
|
||||
messageIds,
|
||||
msgId,
|
||||
shown,
|
||||
link,
|
||||
}: {
|
||||
target: number;
|
||||
messageIds?: number[];
|
||||
msgId?: number;
|
||||
shown?: boolean | null;
|
||||
link?: string;
|
||||
}) => void;
|
||||
|
||||
"network:get": (uuid: string) => void;
|
||||
"network:edit": (data: Record<string, any>) => void;
|
||||
"network:new": (data: Record<string, any>) => void;
|
||||
|
||||
"sign-out": (token?: string) => void;
|
||||
|
||||
"history:clear": ({target}: {target: number}) => void;
|
||||
|
||||
search: ({
|
||||
networkUuid,
|
||||
channelName,
|
||||
searchTerm,
|
||||
offset,
|
||||
}: {
|
||||
networkUuid?: string;
|
||||
channelName?: string;
|
||||
searchTerm?: string;
|
||||
offset: number;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface InterServerEvents {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface SocketData {}
|
|
@ -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
42
shared/types/chan.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import {SharedMsg} from "./msg";
|
||||
import {SharedUser} from "./user";
|
||||
import {SharedNetworkChan} from "./network";
|
||||
|
||||
export enum ChanType {
|
||||
CHANNEL = "channel",
|
||||
LOBBY = "lobby",
|
||||
QUERY = "query",
|
||||
SPECIAL = "special",
|
||||
}
|
||||
|
||||
export enum SpecialChanType {
|
||||
BANLIST = "list_bans",
|
||||
INVITELIST = "list_invites",
|
||||
CHANNELLIST = "list_channels",
|
||||
IGNORELIST = "list_ignored",
|
||||
}
|
||||
|
||||
export enum ChanState {
|
||||
PARTED = 0,
|
||||
JOINED = 1,
|
||||
}
|
||||
|
||||
export type SharedChan = {
|
||||
// TODO: don't force existence, figure out how to make TS infer it.
|
||||
id: number;
|
||||
messages: SharedMsg[];
|
||||
name: string;
|
||||
key: string;
|
||||
topic: string;
|
||||
firstUnread: number;
|
||||
unread: number;
|
||||
highlight: number;
|
||||
muted: boolean;
|
||||
type: ChanType;
|
||||
state: ChanState;
|
||||
|
||||
special?: SpecialChanType;
|
||||
data?: any;
|
||||
closed?: boolean;
|
||||
num_users?: number;
|
||||
};
|
15
shared/types/changelog.ts
Normal file
15
shared/types/changelog.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
export type SharedChangelogData = {
|
||||
current: {
|
||||
prerelease: boolean;
|
||||
version: string;
|
||||
changelog?: string;
|
||||
url: string;
|
||||
};
|
||||
expiresAt: number;
|
||||
latest?: {
|
||||
prerelease: boolean;
|
||||
version: string;
|
||||
url: string;
|
||||
};
|
||||
packages?: boolean;
|
||||
};
|
50
shared/types/config.ts
Normal file
50
shared/types/config.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
export type ConfigTheme = {
|
||||
displayName: string;
|
||||
name: string;
|
||||
themeColor: string | null;
|
||||
};
|
||||
type SharedConfigurationBase = {
|
||||
public: boolean;
|
||||
useHexIp: boolean;
|
||||
prefetch: boolean;
|
||||
fileUpload: boolean;
|
||||
ldapEnabled: boolean;
|
||||
isUpdateAvailable: boolean;
|
||||
applicationServerKey: string;
|
||||
version: string;
|
||||
gitCommit: string | null;
|
||||
themes: ConfigTheme[];
|
||||
defaultTheme: string;
|
||||
fileUploadMaxFileSize?: number;
|
||||
};
|
||||
|
||||
export type ConfigNetDefaults = {
|
||||
name: string;
|
||||
host: string;
|
||||
port: number;
|
||||
password: string;
|
||||
tls: boolean;
|
||||
rejectUnauthorized: boolean;
|
||||
nick: string;
|
||||
username: string;
|
||||
realname: string;
|
||||
join: string;
|
||||
leaveMessage: string;
|
||||
sasl: string;
|
||||
saslAccount: string;
|
||||
saslPassword: string;
|
||||
};
|
||||
export type LockedConfigNetDefaults = Pick<
|
||||
ConfigNetDefaults,
|
||||
"name" | "nick" | "username" | "password" | "realname" | "join"
|
||||
>;
|
||||
|
||||
export type LockedSharedConfiguration = SharedConfigurationBase & {
|
||||
lockNetwork: true;
|
||||
defaults: LockedConfigNetDefaults;
|
||||
};
|
||||
|
||||
export type SharedConfiguration = SharedConfigurationBase & {
|
||||
lockNetwork: false;
|
||||
defaults: ConfigNetDefaults;
|
||||
};
|
10
shared/types/mention.ts
Normal file
10
shared/types/mention.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import {MessageType, UserInMessage} from "./msg";
|
||||
|
||||
export type SharedMention = {
|
||||
chanId: number;
|
||||
msgId: number;
|
||||
type: MessageType;
|
||||
time: Date;
|
||||
text: string;
|
||||
from: UserInMessage;
|
||||
};
|
100
shared/types/msg.ts
Normal file
100
shared/types/msg.ts
Normal file
|
@ -0,0 +1,100 @@
|
|||
export enum MessageType {
|
||||
UNHANDLED = "unhandled",
|
||||
ACTION = "action",
|
||||
AWAY = "away",
|
||||
BACK = "back",
|
||||
ERROR = "error",
|
||||
INVITE = "invite",
|
||||
JOIN = "join",
|
||||
KICK = "kick",
|
||||
LOGIN = "login",
|
||||
LOGOUT = "logout",
|
||||
MESSAGE = "message",
|
||||
MODE = "mode",
|
||||
MODE_CHANNEL = "mode_channel",
|
||||
MODE_USER = "mode_user", // RPL_UMODEIS
|
||||
MONOSPACE_BLOCK = "monospace_block",
|
||||
NICK = "nick",
|
||||
NOTICE = "notice",
|
||||
PART = "part",
|
||||
QUIT = "quit",
|
||||
CTCP = "ctcp",
|
||||
CTCP_REQUEST = "ctcp_request",
|
||||
CHGHOST = "chghost",
|
||||
TOPIC = "topic",
|
||||
TOPIC_SET_BY = "topic_set_by",
|
||||
WHOIS = "whois",
|
||||
RAW = "raw",
|
||||
PLUGIN = "plugin",
|
||||
WALLOPS = "wallops",
|
||||
}
|
||||
|
||||
export type SharedUser = {
|
||||
modes: string[];
|
||||
// Users in the channel have only one mode assigned
|
||||
mode: string;
|
||||
away: string;
|
||||
nick: string;
|
||||
lastMessage: number;
|
||||
};
|
||||
|
||||
export type UserInMessage = Partial<SharedUser> & {
|
||||
mode: string;
|
||||
};
|
||||
|
||||
export type LinkPreview = {
|
||||
type: string;
|
||||
head: string;
|
||||
body: string;
|
||||
thumb: string;
|
||||
size: number;
|
||||
link: string; // Send original matched link to the client
|
||||
shown?: boolean | null;
|
||||
error?: string;
|
||||
message?: string;
|
||||
|
||||
media?: string;
|
||||
mediaType?: string;
|
||||
maxSize?: number;
|
||||
thumbActualUrl?: string;
|
||||
};
|
||||
|
||||
export type SharedMsg = {
|
||||
from?: UserInMessage;
|
||||
id: number;
|
||||
previews?: LinkPreview[];
|
||||
text?: string;
|
||||
type?: MessageType;
|
||||
self?: boolean;
|
||||
time: Date;
|
||||
hostmask?: string;
|
||||
target?: UserInMessage;
|
||||
// TODO: new_nick is only on MessageType.NICK,
|
||||
// we should probably make Msgs that extend this class and use those
|
||||
// throughout. I'll leave any similar fields below.
|
||||
new_nick?: string;
|
||||
highlight?: boolean;
|
||||
showInActive?: boolean;
|
||||
new_ident?: string;
|
||||
new_host?: string;
|
||||
ctcpMessage?: string;
|
||||
command?: string;
|
||||
invitedYou?: boolean;
|
||||
gecos?: string;
|
||||
account?: boolean;
|
||||
|
||||
// these are all just for error:
|
||||
error?: string;
|
||||
nick?: string;
|
||||
channel?: string;
|
||||
reason?: string;
|
||||
|
||||
raw_modes?: any;
|
||||
when?: Date;
|
||||
whois?: any;
|
||||
|
||||
users: string[];
|
||||
|
||||
statusmsgGroup?: string;
|
||||
params?: string[];
|
||||
};
|
36
shared/types/network.ts
Normal file
36
shared/types/network.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import {SharedChan} from "./chan";
|
||||
|
||||
export type SharedPrefixObject = {
|
||||
symbol: string;
|
||||
mode: string;
|
||||
};
|
||||
|
||||
export type SharedNetworkChan = SharedChan & {
|
||||
totalMessages: number;
|
||||
};
|
||||
|
||||
export type SharedPrefix = {
|
||||
prefix: SharedPrefixObject[];
|
||||
modeToSymbol: {[mode: string]: string};
|
||||
symbols: string[];
|
||||
};
|
||||
|
||||
export type SharedServerOptions = {
|
||||
CHANTYPES: string[];
|
||||
PREFIX: SharedPrefix;
|
||||
NETWORK: string;
|
||||
};
|
||||
|
||||
export type SharedNetworkStatus = {
|
||||
connected: boolean;
|
||||
secure: boolean;
|
||||
};
|
||||
|
||||
export type SharedNetwork = {
|
||||
uuid: string;
|
||||
name: string;
|
||||
nick: string;
|
||||
serverOptions: SharedServerOptions;
|
||||
status: SharedNetworkStatus;
|
||||
channels: SharedNetworkChan[];
|
||||
};
|
181
shared/types/socket-events.d.ts
vendored
Normal file
181
shared/types/socket-events.d.ts
vendored
Normal file
|
@ -0,0 +1,181 @@
|
|||
import {SharedMention} from "./mention";
|
||||
import {ChanState, SharedChan} from "./chan";
|
||||
import {SharedNetwork, SharedServerOptions} from "./network";
|
||||
import {SharedMsg, LinkPreview} from "./msg";
|
||||
import {SharedUser} from "./user";
|
||||
import {SharedChangelogData} from "./changelog";
|
||||
import {SharedConfiguration, LockedSharedConfiguration} from "./config";
|
||||
import {SearchResponse, SearchQuery} from "./storage";
|
||||
|
||||
type Session = {
|
||||
current: boolean;
|
||||
active: number;
|
||||
lastUse: number;
|
||||
ip: string;
|
||||
agent: string;
|
||||
token: string;
|
||||
};
|
||||
|
||||
type EventHandler<T> = (data: T) => void;
|
||||
type NoPayloadEventHandler = EventHandler<void>;
|
||||
|
||||
interface ServerToClientEvents {
|
||||
"auth:start": (serverHash: number) => void;
|
||||
"auth:failed": NoPayloadEventHandler;
|
||||
"auth:success": NoPayloadEventHandler;
|
||||
|
||||
"upload:auth": (token: string) => void;
|
||||
|
||||
changelog: EventHandler<SharedChangelogData>;
|
||||
"changelog:newversion": NoPayloadEventHandler;
|
||||
|
||||
"channel:state": EventHandler<{chan: number; state: ChanState}>;
|
||||
|
||||
"change-password": EventHandler<{success: boolean; error?: any}>;
|
||||
|
||||
commands: EventHandler<string[]>;
|
||||
|
||||
configuration: EventHandler<SharedConfiguration | LockedSharedConfiguration>;
|
||||
|
||||
"push:issubscribed": EventHandler<boolean>;
|
||||
"push:unregister": NoPayloadEventHandler;
|
||||
|
||||
"sessions:list": EventHandler<Session[]>;
|
||||
|
||||
"mentions:list": EventHandler<SharedMention[]>;
|
||||
|
||||
"setting:new": EventHandler<{name: string; value: any}>;
|
||||
"setting:all": EventHandler<{[key: string]: any}>;
|
||||
|
||||
"history:clear": EventHandler<{target: number}>;
|
||||
|
||||
"mute:changed": EventHandler<{target: number; status: boolean}>;
|
||||
|
||||
names: EventHandler<{id: number; users: SharedUser[]}>;
|
||||
|
||||
network: EventHandler<{network: SharedNetwork}>;
|
||||
"network:options": EventHandler<{network: string; serverOptions: SharedServerOptions}>;
|
||||
"network:status": EventHandler<{network: string; connected: boolean; secure: boolean}>;
|
||||
"network:info": EventHandler<{uuid: string}>;
|
||||
"network:name": EventHandler<{uuid: string; name: string}>;
|
||||
|
||||
nick: EventHandler<{network: string; nick: string}>;
|
||||
|
||||
open: (id: number) => void;
|
||||
|
||||
part: EventHandler<{chan: number}>;
|
||||
|
||||
"sign-out": NoPayloadEventHandler;
|
||||
|
||||
"sync_sort:networks": EventHandler<{order: SharedNetwork["uuid"][]}>;
|
||||
"sync_sort:channels": EventHandler<{
|
||||
network: SharedNetwork["uuid"];
|
||||
order: SharedChan["id"][];
|
||||
}>;
|
||||
|
||||
topic: EventHandler<{chan: number; topic: string}>;
|
||||
|
||||
users: EventHandler<{chan: number}>;
|
||||
|
||||
more: EventHandler<{chan: number; messages: SharedMsg[]; totalMessages: number}>;
|
||||
|
||||
"msg:preview": EventHandler<{id: number; chan: number; preview: LinkPreview}>;
|
||||
"msg:special": EventHandler<{chan: number; data?: Record<string, any>}>;
|
||||
msg: EventHandler<{msg: SharedMsg; chan: number; highlight?: number; unread?: number}>;
|
||||
|
||||
init: EventHandler<{active: number; networks: SharedNetwork[]; token?: string}>;
|
||||
|
||||
"search:results": (response: SearchResponse) => void;
|
||||
|
||||
quit: EventHandler<{network: string}>;
|
||||
|
||||
error: (error: any) => void;
|
||||
|
||||
connecting: NoPayloadEventHandler;
|
||||
|
||||
join: EventHandler<{
|
||||
shouldOpen: boolean;
|
||||
index: number;
|
||||
network: string;
|
||||
chan: SharedNetworkChan;
|
||||
}>;
|
||||
}
|
||||
|
||||
type AuthPerformData =
|
||||
| Record<string, never> // funny way of saying an empty object
|
||||
| {user: string; password: string}
|
||||
| {
|
||||
user: string;
|
||||
token: string;
|
||||
lastMessage: number;
|
||||
openChannel: number | null;
|
||||
hasConfig: boolean;
|
||||
};
|
||||
|
||||
interface ClientToServerEvents {
|
||||
"auth:perform": EventHandler<AuthPerformData>;
|
||||
|
||||
changelog: NoPayloadEventHandler;
|
||||
|
||||
"change-password": EventHandler<{
|
||||
old_password: string;
|
||||
new_password: string;
|
||||
verify_password: string;
|
||||
}>;
|
||||
|
||||
open: (channelId: number) => void;
|
||||
|
||||
names: EventHandler<{target: number}>;
|
||||
|
||||
input: EventHandler<{target: number; text: string}>;
|
||||
|
||||
"upload:auth": NoPayloadEventHandler;
|
||||
"upload:ping": (token: string) => void;
|
||||
|
||||
"mute:change": EventHandler<{target: number; setMutedTo: boolean}>;
|
||||
|
||||
"push:register": EventHandler<PushSubscriptionJSON>;
|
||||
"push:unregister": NoPayloadEventHandler;
|
||||
|
||||
"setting:get": NoPayloadEventHandler;
|
||||
"setting:set": EventHandler<{name: string; value: any}>;
|
||||
|
||||
"sessions:get": NoPayloadEventHandler;
|
||||
|
||||
"sort:networks": EventHandler<{order: SharedNetwork["uuid"][]}>;
|
||||
"sort:channels": EventHandler<{
|
||||
network: SharedNetwork["uuid"];
|
||||
order: SharedChan["id"][];
|
||||
}>;
|
||||
|
||||
"mentions:dismiss": (msgId: number) => void;
|
||||
"mentions:dismiss_all": NoPayloadEventHandler;
|
||||
"mentions:get": NoPayloadEventHandler;
|
||||
|
||||
more: EventHandler<{target: number; lastId: number; condensed: boolean}>;
|
||||
|
||||
"msg:preview:toggle": EventHandler<{
|
||||
target: number;
|
||||
messageIds?: number[];
|
||||
msgId?: number;
|
||||
shown?: boolean | null;
|
||||
link?: string;
|
||||
}>;
|
||||
|
||||
"network:get": (uuid: string) => void;
|
||||
// TODO typing
|
||||
"network:edit": (data: Record<string, any>) => void;
|
||||
"network:new": (data: Record<string, any>) => void;
|
||||
|
||||
"sign-out": (token?: string) => void;
|
||||
|
||||
"history:clear": EventHandler<{target: number}>;
|
||||
|
||||
search: EventHandler<SearchQuery>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface InterServerEvents {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface SocketData {}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue