mirror of
https://github.com/thelounge/thelounge.git
synced 2024-06-08 08:42:17 +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:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
# EOL: April 2024
|
# EOL: April 2025
|
||||||
- os: ubuntu-latest
|
|
||||||
node_version: 16.x
|
|
||||||
- os: macOS-latest
|
- os: macOS-latest
|
||||||
node_version: 18.x
|
node_version: 18.x
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
node_version: 18.x
|
node_version: 18.x
|
||||||
# EOL: April 2025
|
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
node_version: 18.x
|
node_version: 18.x
|
||||||
# EOL: April 2026
|
# EOL: April 2026
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
node_version: 20.x
|
node_version: 20.x
|
||||||
|
# EOL: April June 2024
|
||||||
|
- os: ubuntu-latest
|
||||||
|
node_version: 21.x
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
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
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
|
node-version: "latest"
|
||||||
registry-url: "https://registry.npmjs.org/"
|
registry-url: "https://registry.npmjs.org/"
|
||||||
|
|
||||||
- name: Install
|
- name: Install
|
||||||
|
@ -33,9 +34,6 @@ jobs:
|
||||||
- name: Test
|
- name: Test
|
||||||
run: yarn test
|
run: yarn test
|
||||||
|
|
||||||
- name: Update npm
|
|
||||||
run: npm install -g npm
|
|
||||||
|
|
||||||
- name: Publish latest
|
- name: Publish latest
|
||||||
if: "!contains(github.ref, '-')"
|
if: "!contains(github.ref, '-')"
|
||||||
run: npm publish --tag latest --provenance
|
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 -->
|
<!-- 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
|
## v4.4.1 - 2023-06-13
|
||||||
|
|
||||||
Small bug fix release that addresses the bugs reported since v4.4.0
|
Small bug fix release that addresses the bugs reported since v4.4.0
|
||||||
|
|
|
@ -59,7 +59,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {filter as fuzzyFilter} from "fuzzy";
|
import {filter as fuzzyFilter} from "fuzzy";
|
||||||
import {computed, defineComponent, nextTick, PropType, ref} from "vue";
|
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 type {ClientChan, ClientUser} from "../js/types";
|
||||||
import Username from "./Username.vue";
|
import Username from "./Username.vue";
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ export default defineComponent({
|
||||||
const result = filteredUsers.value;
|
const result = filteredUsers.value;
|
||||||
|
|
||||||
for (const user of result) {
|
for (const user of result) {
|
||||||
const mode = user.original.modes[0] || "";
|
const mode: string = user.original.modes[0] || "";
|
||||||
|
|
||||||
if (!groups[mode]) {
|
if (!groups[mode]) {
|
||||||
groups[mode] = [];
|
groups[mode] = [];
|
||||||
|
|
|
@ -41,9 +41,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import {computed, defineComponent, ref, watch} from "vue";
|
import {computed, defineComponent, ref, watch} from "vue";
|
||||||
import {onBeforeRouteLeave, onBeforeRouteUpdate} from "vue-router";
|
|
||||||
import eventbus from "../js/eventbus";
|
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({
|
export default defineComponent({
|
||||||
name: "ImageViewer",
|
name: "ImageViewer",
|
||||||
|
@ -104,9 +104,9 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
const links = channel.value.messages
|
const links = channel.value.messages
|
||||||
.map((msg) => msg.previews)
|
.map((msg: SharedMsg) => msg.previews)
|
||||||
.flat()
|
.flat()
|
||||||
.filter((preview) => preview.thumb);
|
.filter((preview) => preview && preview.thumb);
|
||||||
|
|
||||||
const currentIndex = links.indexOf(link.value);
|
const currentIndex = links.indexOf(link.value);
|
||||||
|
|
||||||
|
|
|
@ -150,10 +150,14 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
|
|
||||||
const messageComponent = computed(() => {
|
const messageComponent = computed(() => {
|
||||||
return "message-" + props.message.type;
|
return "message-" + (props.message.type || "invalid"); // TODO: force existence of type in sharedmsg
|
||||||
});
|
});
|
||||||
|
|
||||||
const isAction = () => {
|
const isAction = () => {
|
||||||
|
if (!props.message.type) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return typeof MessageTypes["message-" + props.message.type] !== "undefined";
|
return typeof MessageTypes["message-" + props.message.type] !== "undefined";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -79,7 +79,7 @@ import {
|
||||||
} from "vue";
|
} from "vue";
|
||||||
import {useStore} from "../js/store";
|
import {useStore} from "../js/store";
|
||||||
import {ClientChan, ClientMessage, ClientNetwork, ClientLinkPreview} from "../js/types";
|
import {ClientChan, ClientMessage, ClientNetwork, ClientLinkPreview} from "../js/types";
|
||||||
import Msg from "../../server/models/msg";
|
import {SharedMsg} from "../../shared/types/msg";
|
||||||
|
|
||||||
type CondensedMessageContainer = {
|
type CondensedMessageContainer = {
|
||||||
type: "condensed";
|
type: "condensed";
|
||||||
|
@ -242,7 +242,7 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
|
|
||||||
const shouldDisplayDateMarker = (
|
const shouldDisplayDateMarker = (
|
||||||
message: Msg | ClientMessage | CondensedMessageContainer,
|
message: SharedMsg | ClientMessage | CondensedMessageContainer,
|
||||||
id: number
|
id: number
|
||||||
) => {
|
) => {
|
||||||
const previousMessage = condensedMessages.value[id - 1];
|
const previousMessage = condensedMessages.value[id - 1];
|
||||||
|
@ -270,7 +270,7 @@ export default defineComponent({
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isPreviousSource = (currentMessage: ClientMessage | Msg, id: number) => {
|
const isPreviousSource = (currentMessage: ClientMessage | SharedMsg, id: number) => {
|
||||||
const previousMessage = condensedMessages.value[id - 1];
|
const previousMessage = condensedMessages.value[id - 1];
|
||||||
return !!(
|
return !!(
|
||||||
previousMessage &&
|
previousMessage &&
|
||||||
|
|
|
@ -26,36 +26,43 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const errorMessage = computed(() => {
|
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) {
|
switch (props.message.error) {
|
||||||
case "bad_channel_key":
|
case "bad_channel_key":
|
||||||
return `Cannot join ${props.message.channel} - Bad channel key.`;
|
return `Cannot join ${chan} - Bad channel key.`;
|
||||||
case "banned_from_channel":
|
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":
|
case "cannot_send_to_channel":
|
||||||
return `Cannot send to channel ${props.message.channel}`;
|
return `Cannot send to channel ${chan}`;
|
||||||
case "channel_is_full":
|
case "channel_is_full":
|
||||||
return `Cannot join ${props.message.channel} - Channel is full.`;
|
return `Cannot join ${chan} - Channel is full.`;
|
||||||
case "chanop_privs_needed":
|
case "chanop_privs_needed":
|
||||||
return "Cannot perform action: You're not a channel operator.";
|
return "Cannot perform action: You're not a channel operator.";
|
||||||
case "invite_only_channel":
|
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":
|
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":
|
case "not_on_channel":
|
||||||
return "Cannot perform action: You're not on the channel.";
|
return "Cannot perform action: You're not on the channel.";
|
||||||
case "password_mismatch":
|
case "password_mismatch":
|
||||||
return "Password mismatch.";
|
return "Password mismatch.";
|
||||||
case "too_many_channels":
|
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":
|
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":
|
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":
|
case "user_on_channel":
|
||||||
return `User ${props.message.nick} is already on the channel.`;
|
return `User ${nick} is already on the channel.`;
|
||||||
default:
|
default:
|
||||||
if (props.message.reason) {
|
if (props.message.reason) {
|
||||||
return `${props.message.reason} (${props.message.error})`;
|
return `${props.message.reason} (${
|
||||||
|
props.message.error || "!UNDEFINED_ERR"
|
||||||
|
})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return props.message.error;
|
return props.message.error;
|
||||||
|
|
|
@ -498,6 +498,7 @@ export default defineComponent({
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
// eslint-disable-next-line
|
||||||
() => props.defaults?.commands,
|
() => props.defaults?.commands,
|
||||||
() => {
|
() => {
|
||||||
void nextTick(() => {
|
void nextTick(() => {
|
||||||
|
@ -507,6 +508,7 @@ export default defineComponent({
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
// eslint-disable-next-line
|
||||||
() => props.defaults?.tls,
|
() => props.defaults?.tls,
|
||||||
(isSecureChecked) => {
|
(isSecureChecked) => {
|
||||||
const ports = [6667, 6697];
|
const ports = [6667, 6697];
|
||||||
|
|
|
@ -309,8 +309,7 @@ export default defineComponent({
|
||||||
|
|
||||||
moveItemInArray(store.state.networks, oldIndex, newIndex);
|
moveItemInArray(store.state.networks, oldIndex, newIndex);
|
||||||
|
|
||||||
socket.emit("sort", {
|
socket.emit("sort:networks", {
|
||||||
type: "networks",
|
|
||||||
order: store.state.networks.map((n) => n.uuid),
|
order: store.state.networks.map((n) => n.uuid),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -341,9 +340,8 @@ export default defineComponent({
|
||||||
|
|
||||||
moveItemInArray(netChan.network.channels, oldIndex, newIndex);
|
moveItemInArray(netChan.network.channels, oldIndex, newIndex);
|
||||||
|
|
||||||
socket.emit("sort", {
|
socket.emit("sort:channel", {
|
||||||
type: "channels",
|
network: netChan.network.uuid,
|
||||||
target: netChan.network.uuid,
|
|
||||||
order: netChan.network.channels.map((c) => c.id),
|
order: netChan.network.channels.map((c) => c.id),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,10 +12,10 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {computed, defineComponent, PropType} from "vue";
|
import {computed, defineComponent, PropType} from "vue";
|
||||||
import {UserInMessage} from "../../server/models/msg";
|
import {UserInMessage} from "../../shared/types/msg";
|
||||||
import eventbus from "../js/eventbus";
|
import eventbus from "../js/eventbus";
|
||||||
import colorClass from "../js/helpers/colorClass";
|
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";
|
import {useStore} from "../js/store";
|
||||||
|
|
||||||
type UsernameUser = Partial<UserInMessage> & {
|
type UsernameUser = Partial<UserInMessage> & {
|
||||||
|
|
|
@ -179,6 +179,26 @@
|
||||||
</div>
|
</div>
|
||||||
</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="help-item">
|
||||||
<div class="subject">
|
<div class="subject">
|
||||||
<span v-if="!isApple"><kbd>Alt</kbd> <kbd>A</kbd></span>
|
<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 {useStore} from "../../js/store";
|
||||||
import {useRoute, useRouter} from "vue-router";
|
import {useRoute, useRouter} from "vue-router";
|
||||||
import {switchToChannel} from "../../js/router";
|
import {switchToChannel} from "../../js/router";
|
||||||
import {SearchQuery} from "../../../server/plugins/messageStorage/types";
|
import {SearchQuery} from "../../../shared/types/storage";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "SearchResults",
|
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) {
|
for (const message of store.state.activeChannel.channel.messages) {
|
||||||
let toggled = false;
|
let toggled = false;
|
||||||
|
|
||||||
for (const preview of message.previews) {
|
for (const preview of message.previews || []) {
|
||||||
if (preview.shown) {
|
if (preview.shown) {
|
||||||
preview.shown = false;
|
preview.shown = false;
|
||||||
toggled = true;
|
toggled = true;
|
||||||
|
|
|
@ -11,7 +11,7 @@ function input() {
|
||||||
for (const message of store.state.activeChannel.channel.messages) {
|
for (const message of store.state.activeChannel.channel.messages) {
|
||||||
let toggled = false;
|
let toggled = false;
|
||||||
|
|
||||||
for (const preview of message.previews) {
|
for (const preview of message.previews || []) {
|
||||||
if (!preview.shown) {
|
if (!preview.shown) {
|
||||||
preview.shown = true;
|
preview.shown = true;
|
||||||
toggled = true;
|
toggled = true;
|
||||||
|
|
|
@ -83,6 +83,35 @@ Mousetrap.bind(["alt+shift+up", "alt+shift+down"], function (e, keys) {
|
||||||
return false;
|
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
|
// Jump to the first window with a highlight in it, or the first with unread
|
||||||
// activity if there are none with highlights.
|
// activity if there are none with highlights.
|
||||||
Mousetrap.bind(["alt+a"], function (e) {
|
Mousetrap.bind(["alt+a"], function (e) {
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import {nextTick} from "vue";
|
|
||||||
import socket from "../socket";
|
import socket from "../socket";
|
||||||
import storage from "../localStorage";
|
import storage from "../localStorage";
|
||||||
|
import {toClientChan} from "../chan";
|
||||||
import {router, switchToChannel, navigate} from "../router";
|
import {router, switchToChannel, navigate} from "../router";
|
||||||
import {store} from "../store";
|
import {store} from "../store";
|
||||||
import parseIrcUri from "../helpers/parseIrcUri";
|
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) {
|
socket.on("init", async function (data) {
|
||||||
store.commit("networks", mergeNetworkData(data.networks));
|
store.commit("networks", mergeNetworkData(data.networks));
|
||||||
|
@ -31,54 +32,54 @@ socket.on("init", async function (data) {
|
||||||
window.g_TheLoungeRemoveLoading();
|
window.g_TheLoungeRemoveLoading();
|
||||||
}
|
}
|
||||||
|
|
||||||
const handledQuery = await handleQueryParams();
|
if (await handleQueryParams()) {
|
||||||
|
// If we handled query parameters like irc:// links or just general
|
||||||
|
// connect parameters in public mode, then nothing to do here
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// If we handled query parameters like irc:// links or just general
|
// If we are on an unknown route or still on SignIn component
|
||||||
// connect parameters in public mode, then nothing to do here
|
// then we can open last known channel on server, or Connect window if none
|
||||||
if (!handledQuery) {
|
if (!router.currentRoute?.value?.name || router.currentRoute?.value?.name === "SignIn") {
|
||||||
// If we are on an unknown route or still on SignIn component
|
const channel = store.getters.findChannel(data.active);
|
||||||
// then we can open last known channel on server, or Connect window if none
|
|
||||||
if (
|
|
||||||
!router.currentRoute?.value?.name ||
|
|
||||||
router.currentRoute?.value?.name === "SignIn"
|
|
||||||
) {
|
|
||||||
const channel = store.getters.findChannel(data.active);
|
|
||||||
|
|
||||||
if (channel) {
|
if (channel) {
|
||||||
switchToChannel(channel.channel);
|
switchToChannel(channel.channel);
|
||||||
} else if (store.state.networks.length > 0) {
|
} else if (store.state.networks.length > 0) {
|
||||||
// Server is telling us to open a channel that does not exist
|
// Server is telling us to open a channel that does not exist
|
||||||
// For example, it can be unset if you first open the page after server start
|
// For example, it can be unset if you first open the page after server start
|
||||||
switchToChannel(store.state.networks[0].channels[0]);
|
switchToChannel(store.state.networks[0].channels[0]);
|
||||||
} else {
|
} else {
|
||||||
await navigate("Connect");
|
await navigate("Connect");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function mergeNetworkData(newNetworks: ClientNetwork[]) {
|
function mergeNetworkData(newNetworks: SharedNetwork[]): ClientNetwork[] {
|
||||||
const stored = storage.get("thelounge.networks.collapsed");
|
const stored = storage.get("thelounge.networks.collapsed");
|
||||||
const collapsedNetworks = stored ? new Set(JSON.parse(stored)) : new Set();
|
const collapsedNetworks = stored ? new Set(JSON.parse(stored)) : new Set();
|
||||||
|
const result: ReturnType<typeof mergeNetworkData> = [];
|
||||||
|
|
||||||
for (let n = 0; n < newNetworks.length; n++) {
|
for (const sharedNet of newNetworks) {
|
||||||
const network = newNetworks[n];
|
const currentNetwork = store.getters.findNetwork(sharedNet.uuid);
|
||||||
const currentNetwork = store.getters.findNetwork(network.uuid);
|
|
||||||
|
|
||||||
// If this network is new, set some default variables and initalize channel variables
|
// If this network is new, set some default variables and initalize channel variables
|
||||||
if (!currentNetwork) {
|
if (!currentNetwork) {
|
||||||
network.isJoinChannelShown = false;
|
const newNet: ClientNetwork = {
|
||||||
network.isCollapsed = collapsedNetworks.has(network.uuid);
|
...sharedNet,
|
||||||
network.channels.forEach(store.getters.initChannel);
|
channels: sharedNet.channels.map(toClientChan),
|
||||||
|
isJoinChannelShown: false,
|
||||||
|
isCollapsed: collapsedNetworks.has(sharedNet.uuid),
|
||||||
|
};
|
||||||
|
result.push(newNet);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge received network object into existing network object on the client
|
// Merge received network object into existing network object on the client
|
||||||
// so the object reference stays the same (e.g. for currentChannel state)
|
// so the object reference stays the same (e.g. for currentChannel state)
|
||||||
for (const key in network) {
|
for (const key in sharedNet) {
|
||||||
if (!Object.prototype.hasOwnProperty.call(network, key)) {
|
if (!Object.prototype.hasOwnProperty.call(sharedNet, key)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,81 +87,82 @@ function mergeNetworkData(newNetworks: ClientNetwork[]) {
|
||||||
if (key === "channels") {
|
if (key === "channels") {
|
||||||
currentNetwork.channels = mergeChannelData(
|
currentNetwork.channels = mergeChannelData(
|
||||||
currentNetwork.channels,
|
currentNetwork.channels,
|
||||||
network.channels as InitClientChan[]
|
sharedNet.channels
|
||||||
);
|
);
|
||||||
} else {
|
} 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[]) {
|
function mergeChannelData(
|
||||||
for (let c = 0; c < newChannels.length; c++) {
|
oldChannels: ClientChan[],
|
||||||
const channel = newChannels[c];
|
newChannels: SharedNetworkChan[]
|
||||||
const currentChannel = oldChannels.find((chan) => chan.id === channel.id);
|
): 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) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge received channel object into existing currentChannel
|
// Merge received channel object into existing currentChannel
|
||||||
// so the object references are exactly the same (e.g. in store.state.activeChannel)
|
// 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
|
emitNamesOrMarkUsersOudated(currentChannel); // TODO: this should not carry logic like that
|
||||||
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;
|
// Reconnection only sends new messages, so merge it on the client
|
||||||
}
|
// Only concat if server sent us less than 100 messages so we don't introduce gaps
|
||||||
|
if (currentChannel.messages && newChannel.messages.length < 100) {
|
||||||
// Server sends total count of messages in memory, we compare it to amount of messages
|
currentChannel.messages = currentChannel.messages.concat(newChannel.messages);
|
||||||
// on the client, and decide whether theres more messages to load from server
|
} else {
|
||||||
if (key === "totalMessages") {
|
currentChannel.messages = newChannel.messages;
|
||||||
currentChannel.moreHistoryAvailable =
|
|
||||||
channel.totalMessages! > currentChannel.messages.length;
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reconnection only sends new messages, so merge it on the client
|
|
||||||
// Only concat if server sent us less than 100 messages so we don't introduce gaps
|
|
||||||
if (key === "messages" && currentChannel.messages && channel.messages.length < 100) {
|
|
||||||
currentChannel.messages = currentChannel.messages.concat(channel.messages);
|
|
||||||
} else {
|
|
||||||
currentChannel[key] = channel[key];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
newChannels[c] = currentChannel;
|
// TODO: this is copies more than what the compiler knows about
|
||||||
|
for (const key in newChannel) {
|
||||||
|
if (!Object.hasOwn(currentChannel, key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "messages") {
|
||||||
|
// already handled
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentChannel[key] = newChannel[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(currentChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
return newChannels;
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitNamesOrMarkUsersOudated(chan: ClientChan) {
|
||||||
|
if (store.state.activeChannel && store.state.activeChannel.channel === chan) {
|
||||||
|
// For currently open channel, request the user list straight away
|
||||||
|
socket.emit("names", {
|
||||||
|
target: chan.id,
|
||||||
|
});
|
||||||
|
chan.usersOutdated = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For all other channels, mark the user list as outdated
|
||||||
|
// so an update will be requested whenever user switches to these channels
|
||||||
|
chan.usersOutdated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleQueryParams() {
|
async function handleQueryParams() {
|
||||||
|
@ -170,30 +172,28 @@ async function handleQueryParams() {
|
||||||
|
|
||||||
const params = new URLSearchParams(document.location.search);
|
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")) {
|
if (params.has("uri")) {
|
||||||
// Set default connection settings from IRC protocol links
|
// Set default connection settings from IRC protocol links
|
||||||
const uri = params.get("uri");
|
const uri = params.get("uri");
|
||||||
const queryParams = parseIrcUri(String(uri));
|
const queryParams = parseIrcUri(String(uri));
|
||||||
|
removeQueryParams();
|
||||||
cleanParams();
|
|
||||||
await router.push({name: "Connect", query: queryParams});
|
await router.push({name: "Connect", query: queryParams});
|
||||||
|
|
||||||
return true;
|
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
|
// Set default connection settings from url params
|
||||||
const queryParams = Object.fromEntries(params.entries());
|
const queryParams = Object.fromEntries(params.entries());
|
||||||
|
removeQueryParams();
|
||||||
cleanParams();
|
|
||||||
await router.push({name: "Connect", query: queryParams});
|
await router.push({name: "Connect", query: queryParams});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
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 socket from "../socket";
|
||||||
import {store} from "../store";
|
import {store} from "../store";
|
||||||
import {switchToChannel} from "../router";
|
import {switchToChannel} from "../router";
|
||||||
|
import {ClientChan} from "../types";
|
||||||
|
import {toClientChan} from "../chan";
|
||||||
|
|
||||||
socket.on("join", function (data) {
|
socket.on("join", function (data) {
|
||||||
store.getters.initChannel(data.chan);
|
|
||||||
|
|
||||||
const network = store.getters.findNetwork(data.network);
|
const network = store.getters.findNetwork(data.network);
|
||||||
|
|
||||||
if (!network) {
|
if (!network) {
|
||||||
return;
|
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
|
// Queries do not automatically focus, unless the user did a whois
|
||||||
if (data.chan.type === "query" && !data.shouldOpen) {
|
if (data.chan.type === "query" && !data.shouldOpen) {
|
||||||
|
|
|
@ -1,7 +1,17 @@
|
||||||
import socket from "../socket";
|
import socket from "../socket";
|
||||||
import {store} from "../store";
|
import {store} from "../store";
|
||||||
import {ClientMention} from "../types";
|
import {ClientMention} from "../types";
|
||||||
|
import {SharedMention} from "../../../shared/types/mention";
|
||||||
|
|
||||||
socket.on("mentions:list", function (data) {
|
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 socket from "../socket";
|
||||||
import {store} from "../store";
|
import {store} from "../store";
|
||||||
import {ClientMessage} from "../types";
|
import type {ClientChan, ClientMessage} from "../types";
|
||||||
|
|
||||||
socket.on("more", async (data) => {
|
socket.on("more", async (data) => {
|
||||||
const channel = store.getters.findChannel(data.chan)?.channel;
|
const channel = store.getters.findChannel(data.chan)?.channel;
|
||||||
|
@ -14,13 +14,15 @@ socket.on("more", async (data) => {
|
||||||
channel.inputHistory = channel.inputHistory.concat(
|
channel.inputHistory = channel.inputHistory.concat(
|
||||||
data.messages
|
data.messages
|
||||||
.filter((m) => m.self && m.text && m.type === "message")
|
.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()
|
.reverse()
|
||||||
.slice(0, 100 - channel.inputHistory.length)
|
.slice(0, 100 - channel.inputHistory.length)
|
||||||
);
|
);
|
||||||
channel.moreHistoryAvailable =
|
channel.moreHistoryAvailable =
|
||||||
data.totalMessages > channel.messages.length + data.messages.length;
|
data.totalMessages > channel.messages.length + data.messages.length;
|
||||||
channel.messages.unshift(...(data.messages as ClientMessage[]));
|
channel.messages.unshift(...data.messages);
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
channel.historyLoading = false;
|
channel.historyLoading = false;
|
||||||
|
|
|
@ -3,7 +3,8 @@ import socket from "../socket";
|
||||||
import {cleanIrcMessage} from "../../../shared/irc";
|
import {cleanIrcMessage} from "../../../shared/irc";
|
||||||
import {store} from "../store";
|
import {store} from "../store";
|
||||||
import {switchToChannel} from "../router";
|
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;
|
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(
|
function notifyMessage(
|
||||||
targetId: number,
|
targetId: number,
|
||||||
channel: ClientChan,
|
channel: ClientChan,
|
||||||
|
@ -122,12 +131,14 @@ function notifyMessage(
|
||||||
) {
|
) {
|
||||||
let title: string;
|
let title: string;
|
||||||
let body: string;
|
let body: string;
|
||||||
|
// TODO: fix msg type and get rid of that conditional
|
||||||
|
const nick = msg.from && msg.from.nick ? msg.from.nick : "unkonown";
|
||||||
|
|
||||||
if (msg.type === "invite") {
|
if (msg.type === "invite") {
|
||||||
title = "New channel invite:";
|
title = "New channel invite:";
|
||||||
body = msg.from.nick + " invited you to " + msg.channel;
|
body = nick + " invited you to " + msg.channel;
|
||||||
} else {
|
} else {
|
||||||
title = String(msg.from.nick);
|
title = nick;
|
||||||
|
|
||||||
if (channel.type !== "query") {
|
if (channel.type !== "query") {
|
||||||
title += ` (${channel.name})`;
|
title += ` (${channel.name})`;
|
||||||
|
@ -137,7 +148,8 @@ function notifyMessage(
|
||||||
title += " says:";
|
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));
|
const timestamp = Date.parse(String(msg.time));
|
||||||
|
@ -184,24 +196,40 @@ function notifyMessage(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateUserList(channel, msg) {
|
function updateUserList(channel: ClientChan, msg: SharedMsg) {
|
||||||
if (msg.type === "message" || msg.type === "action") {
|
switch (msg.type) {
|
||||||
const user = channel.users.find((u) => u.nick === msg.from.nick);
|
case "message": // fallthrough
|
||||||
|
|
||||||
if (user) {
|
case "action": {
|
||||||
user.lastMessage = new Date(msg.time).getTime() || Date.now();
|
const user = channel.users.find((u) => u.nick === msg.from?.nick);
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
user.lastMessage = new Date(msg.time).getTime() || Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} else if (msg.type === "quit" || msg.type === "part") {
|
|
||||||
const idx = channel.users.findIndex((u) => u.nick === msg.from.nick);
|
|
||||||
|
|
||||||
if (idx > -1) {
|
case "quit": // fallthrough
|
||||||
channel.users.splice(idx, 1);
|
|
||||||
|
case "part": {
|
||||||
|
const idx = channel.users.findIndex((u) => u.nick === msg.from?.nick);
|
||||||
|
|
||||||
|
if (idx > -1) {
|
||||||
|
channel.users.splice(idx, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} else if (msg.type === "kick") {
|
|
||||||
const idx = channel.users.findIndex((u) => u.nick === msg.target.nick);
|
|
||||||
|
|
||||||
if (idx > -1) {
|
case "kick": {
|
||||||
channel.users.splice(idx, 1);
|
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 netChan = store.getters.findChannel(data.chan);
|
||||||
const message = netChan?.channel.messages.find((m) => m.id === data.id);
|
const message = netChan?.channel.messages.find((m) => m.id === data.id);
|
||||||
|
|
||||||
if (!message) {
|
if (!message || !message.previews) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
import socket from "../socket";
|
import socket from "../socket";
|
||||||
import {store} from "../store";
|
import {store} from "../store";
|
||||||
import {switchToChannel} from "../router";
|
import {switchToChannel} from "../router";
|
||||||
|
import {toClientChan} from "../chan";
|
||||||
|
import {ClientNetwork} from "../types";
|
||||||
|
import {ChanState} from "../../../shared/types/chan";
|
||||||
|
|
||||||
socket.on("network", function (data) {
|
socket.on("network", function (data) {
|
||||||
const network = data.networks[0];
|
const network: ClientNetwork = {
|
||||||
|
...data.network,
|
||||||
network.isJoinChannelShown = false;
|
channels: data.network.channels.map(toClientChan),
|
||||||
network.isCollapsed = false;
|
isJoinChannelShown: false,
|
||||||
network.channels.forEach(store.getters.initChannel);
|
isCollapsed: false,
|
||||||
|
};
|
||||||
|
|
||||||
store.commit("networks", [...store.state.networks, network]);
|
store.commit("networks", [...store.state.networks, network]);
|
||||||
|
|
||||||
|
@ -19,7 +23,7 @@ socket.on("network:options", function (data) {
|
||||||
const network = store.getters.findNetwork(data.network);
|
const network = store.getters.findNetwork(data.network);
|
||||||
|
|
||||||
if (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) {
|
if (!data.connected) {
|
||||||
network.channels.forEach((channel) => {
|
network.channels.forEach((channel) => {
|
||||||
channel.users = [];
|
channel.users = []; // TODO: untangle this
|
||||||
channel.state = 0;
|
channel.state = ChanState.PARTED;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,30 +1,16 @@
|
||||||
import socket from "../socket";
|
import socket from "../socket";
|
||||||
import {store} from "../store";
|
import {store} from "../store";
|
||||||
|
|
||||||
socket.on("sync_sort", function (data) {
|
socket.on("sync_sort:networks", function (data) {
|
||||||
const order = data.order;
|
store.commit("sortNetworks", (a, b) => data.order.indexOf(a.uuid) - data.order.indexOf(b.uuid));
|
||||||
|
});
|
||||||
switch (data.type) {
|
|
||||||
case "networks":
|
socket.on("sync_sort:channels", function (data) {
|
||||||
store.commit(
|
const network = store.getters.findNetwork(data.network);
|
||||||
"sortNetworks",
|
|
||||||
(a, b) => (order as string[]).indexOf(a.uuid) - (order as string[]).indexOf(b.uuid)
|
if (!network) {
|
||||||
);
|
return;
|
||||||
|
}
|
||||||
break;
|
|
||||||
|
network.channels.sort((a, b) => data.order.indexOf(a.id) - data.order.indexOf(b.id));
|
||||||
case "channels": {
|
|
||||||
const network = store.getters.findNetwork(data.target);
|
|
||||||
|
|
||||||
if (!network) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
network.channels.sort(
|
|
||||||
(a, b) => (order as number[]).indexOf(a.id) - (order as number[]).indexOf(b.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import io, {Socket} from "socket.io-client";
|
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({
|
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io({
|
||||||
transports: JSON.parse(document.body.dataset.transports || "['polling', 'websocket']"),
|
transports: JSON.parse(document.body.dataset.transports || "['polling', 'websocket']"),
|
||||||
|
|
|
@ -3,19 +3,12 @@
|
||||||
import {ActionContext, createStore, Store, useStore as baseUseStore} from "vuex";
|
import {ActionContext, createStore, Store, useStore as baseUseStore} from "vuex";
|
||||||
import {createSettingsStore} from "./store-settings";
|
import {createSettingsStore} from "./store-settings";
|
||||||
import storage from "./localStorage";
|
import storage from "./localStorage";
|
||||||
import type {
|
import type {ClientChan, ClientNetwork, NetChan, ClientMention, ClientMessage} from "./types";
|
||||||
ClientChan,
|
|
||||||
ClientConfiguration,
|
|
||||||
ClientNetwork,
|
|
||||||
InitClientChan,
|
|
||||||
NetChan,
|
|
||||||
ClientMessage,
|
|
||||||
ClientMention,
|
|
||||||
} from "./types";
|
|
||||||
import type {InjectionKey} from "vue";
|
import type {InjectionKey} from "vue";
|
||||||
|
|
||||||
import {SettingsState} from "./settings";
|
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;
|
const appName = document.title;
|
||||||
|
|
||||||
|
@ -59,7 +52,7 @@ export type State = {
|
||||||
mentions: ClientMention[];
|
mentions: ClientMention[];
|
||||||
hasServiceWorker: boolean;
|
hasServiceWorker: boolean;
|
||||||
pushNotificationState: string;
|
pushNotificationState: string;
|
||||||
serverConfiguration: ClientConfiguration | null;
|
serverConfiguration: SharedConfiguration | LockedSharedConfiguration | null;
|
||||||
sessions: ClientSession[];
|
sessions: ClientSession[];
|
||||||
sidebarOpen: boolean;
|
sidebarOpen: boolean;
|
||||||
sidebarDragging: boolean;
|
sidebarDragging: boolean;
|
||||||
|
@ -131,7 +124,6 @@ type Getters = {
|
||||||
findNetwork: (state: State) => (uuid: string) => ClientNetwork | null;
|
findNetwork: (state: State) => (uuid: string) => ClientNetwork | null;
|
||||||
highlightCount(state: State): number;
|
highlightCount(state: State): number;
|
||||||
title(state: State, getters: Omit<Getters, "title">): string;
|
title(state: State, getters: Omit<Getters, "title">): string;
|
||||||
initChannel: () => (channel: InitClientChan) => ClientChan;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// getters without the state argument
|
// getters without the state argument
|
||||||
|
@ -202,31 +194,6 @@ const getters: Getters = {
|
||||||
|
|
||||||
return alertEventCount + channelname + appName;
|
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 = {
|
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 {defineComponent} from "vue";
|
||||||
|
|
||||||
import Chan from "../../server/models/chan";
|
import {SharedChan} from "../../shared/types/chan";
|
||||||
import Network from "../../server/models/network";
|
import {SharedNetwork} from "../../shared/types/network";
|
||||||
import User from "../../server/models/user";
|
import {SharedUser} from "../../shared/types/user";
|
||||||
import Message from "../../server/models/msg";
|
import {SharedMention} from "../../shared/types/mention";
|
||||||
import {Mention} from "../../server/client";
|
import {SharedConfiguration, LockedSharedConfiguration} from "../../shared/types/config";
|
||||||
import {ClientConfiguration} from "../../server/server";
|
import {LinkPreview, SharedMsg} from "../../shared/types/msg";
|
||||||
import {LinkPreview} from "../../server/plugins/irc-events/link";
|
|
||||||
|
|
||||||
interface LoungeWindow extends Window {
|
interface LoungeWindow extends Window {
|
||||||
g_TheLoungeRemoveLoading?: () => void;
|
g_TheLoungeRemoveLoading?: () => void;
|
||||||
|
@ -16,19 +15,15 @@ interface LoungeWindow extends Window {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientUser = User & {
|
type ClientUser = SharedUser;
|
||||||
//
|
|
||||||
};
|
|
||||||
|
|
||||||
type ClientMessage = Omit<Message, "users"> & {
|
// we will eventually need to put client specific fields here
|
||||||
time: number;
|
// which are not shared with the server
|
||||||
users: string[];
|
export type ClientMessage = SharedMsg;
|
||||||
};
|
|
||||||
|
|
||||||
type ClientChan = Omit<Chan, "users" | "messages"> & {
|
type ClientChan = Omit<SharedChan, "messages"> & {
|
||||||
moreHistoryAvailable: boolean;
|
moreHistoryAvailable: boolean;
|
||||||
editTopic: boolean;
|
editTopic: boolean;
|
||||||
users: ClientUser[];
|
|
||||||
messages: ClientMessage[];
|
messages: ClientMessage[];
|
||||||
|
|
||||||
// these are added in store/initChannel
|
// these are added in store/initChannel
|
||||||
|
@ -38,6 +33,8 @@ type ClientChan = Omit<Chan, "users" | "messages"> & {
|
||||||
historyLoading: boolean;
|
historyLoading: boolean;
|
||||||
scrolledToBottom: boolean;
|
scrolledToBottom: boolean;
|
||||||
usersOutdated: boolean;
|
usersOutdated: boolean;
|
||||||
|
|
||||||
|
users: ClientUser[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type InitClientChan = ClientChan & {
|
type InitClientChan = ClientChan & {
|
||||||
|
@ -46,7 +43,7 @@ type InitClientChan = ClientChan & {
|
||||||
};
|
};
|
||||||
|
|
||||||
// We omit channels so we can use ClientChan[] instead of Chan[]
|
// We omit channels so we can use ClientChan[] instead of Chan[]
|
||||||
type ClientNetwork = Omit<Network, "channels"> & {
|
type ClientNetwork = Omit<SharedNetwork, "channels"> & {
|
||||||
isJoinChannelShown: boolean;
|
isJoinChannelShown: boolean;
|
||||||
isCollapsed: boolean;
|
isCollapsed: boolean;
|
||||||
channels: ClientChan[];
|
channels: ClientChan[];
|
||||||
|
@ -57,9 +54,8 @@ type NetChan = {
|
||||||
network: ClientNetwork;
|
network: ClientNetwork;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ClientConfiguration = ClientConfiguration;
|
type ClientMention = SharedMention & {
|
||||||
type ClientMention = Mention & {
|
localetime: string; // TODO: this needs to go the way of the dodo, nothing but a single component uses it
|
||||||
localetime: string;
|
|
||||||
channel: NetChan | null;
|
channel: NetChan | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,9 @@ import App from "../components/App.vue";
|
||||||
import storage from "./localStorage";
|
import storage from "./localStorage";
|
||||||
import {router} from "./router";
|
import {router} from "./router";
|
||||||
import socket from "./socket";
|
import socket from "./socket";
|
||||||
|
import "./socket-events"; // this sets up all socket event listeners, do not remove
|
||||||
import eventbus from "./eventbus";
|
import eventbus from "./eventbus";
|
||||||
|
|
||||||
import "./socket-events";
|
|
||||||
import "./webpush";
|
import "./webpush";
|
||||||
import "./keybinds";
|
import "./keybinds";
|
||||||
import {LoungeWindow} from "./types";
|
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. */,
|
] /* 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": [
|
"files": [
|
||||||
"../package.json",
|
"../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/fullnamemap.json",
|
||||||
"./js/helpers/simplemap.json",
|
"./js/helpers/simplemap.json"
|
||||||
"../webpack.config.ts",
|
|
||||||
"../babel.config.cjs"
|
|
||||||
] /* 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. */,
|
] /* 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": [],
|
// "exclude": [],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
|
22
package.json
22
package.json
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "thelounge",
|
"name": "thelounge",
|
||||||
"description": "The self-hosted Web IRC client",
|
"description": "The self-hosted Web IRC client",
|
||||||
"version": "4.4.1",
|
"version": "4.4.3",
|
||||||
"preferGlobal": true,
|
"preferGlobal": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"thelounge": "index.js"
|
"thelounge": "index.js"
|
||||||
|
@ -43,7 +43,7 @@
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"./.thelounge_home",
|
"./.thelounge_home",
|
||||||
|
@ -61,7 +61,7 @@
|
||||||
"cheerio": "1.0.0-rc.12",
|
"cheerio": "1.0.0-rc.12",
|
||||||
"commander": "9.0.0",
|
"commander": "9.0.0",
|
||||||
"content-disposition": "0.5.4",
|
"content-disposition": "0.5.4",
|
||||||
"express": "4.17.3",
|
"express": "4.19.2",
|
||||||
"file-type": "16.5.4",
|
"file-type": "16.5.4",
|
||||||
"filenamify": "4.3.0",
|
"filenamify": "4.3.0",
|
||||||
"got": "11.8.5",
|
"got": "11.8.5",
|
||||||
|
@ -98,20 +98,20 @@
|
||||||
"@types/chai": "4.3.5",
|
"@types/chai": "4.3.5",
|
||||||
"@types/cheerio": "0.22.35",
|
"@types/cheerio": "0.22.35",
|
||||||
"@types/content-disposition": "0.5.8",
|
"@types/content-disposition": "0.5.8",
|
||||||
"@types/express": "4.17.13",
|
"@types/express": "4.17.21",
|
||||||
"@types/is-utf8": "0.2.3",
|
"@types/is-utf8": "0.2.3",
|
||||||
"@types/ldapjs": "2.2.2",
|
"@types/ldapjs": "2.2.2",
|
||||||
"@types/linkify-it": "3.0.5",
|
"@types/linkify-it": "3.0.5",
|
||||||
"@types/lodash": "4.14.200",
|
"@types/lodash": "4.14.202",
|
||||||
"@types/mime-types": "2.1.1",
|
"@types/mime-types": "2.1.1",
|
||||||
"@types/mocha": "9.1.1",
|
"@types/mocha": "9.1.1",
|
||||||
"@types/mousetrap": "1.6.13",
|
"@types/mousetrap": "1.6.15",
|
||||||
"@types/node": "17.0.31",
|
"@types/node": "17.0.45",
|
||||||
"@types/read": "0.0.29",
|
"@types/read": "0.0.32",
|
||||||
"@types/semver": "7.3.9",
|
"@types/semver": "7.3.9",
|
||||||
"@types/sortablejs": "1.13.0",
|
"@types/sortablejs": "1.13.0",
|
||||||
"@types/sqlite3": "3.1.8",
|
"@types/sqlite3": "3.1.11",
|
||||||
"@types/ua-parser-js": "0.7.36",
|
"@types/ua-parser-js": "0.7.39",
|
||||||
"@types/uuid": "8.3.4",
|
"@types/uuid": "8.3.4",
|
||||||
"@types/web-push": "3.3.2",
|
"@types/web-push": "3.3.2",
|
||||||
"@types/webpack-env": "1.16.4",
|
"@types/webpack-env": "1.16.4",
|
||||||
|
@ -167,7 +167,7 @@
|
||||||
"vuex": "4.0.2",
|
"vuex": "4.0.2",
|
||||||
"webpack": "5.76.0",
|
"webpack": "5.76.0",
|
||||||
"webpack-cli": "4.9.2",
|
"webpack-cli": "4.9.2",
|
||||||
"webpack-dev-middleware": "5.3.3",
|
"webpack-dev-middleware": "5.3.4",
|
||||||
"webpack-hot-middleware": "2.25.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`
|
// Builds a Markdown list item for a commit made directly in `master`
|
||||||
function printCommit(commit) {
|
function printCommit(commit) {
|
||||||
return `- ${commit.messageHeadline} (${printEntryLink(commit)} ${printAuthorLink(
|
const author_link = commit.author.user ? printAuthorLink(commit.author.user) : "unknown author";
|
||||||
commit.author.user
|
return `- ${commit.messageHeadline} (${printEntryLink(commit)} ${author_link})`;
|
||||||
)})`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Builds a Markdown list of all given items
|
// 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.
|
// (with format `@username`) of everyone who contributed to this version.
|
||||||
function extractContributors(entries) {
|
function extractContributors(entries) {
|
||||||
const set = Object.values(entries).reduce((memo, {__typename, author}) => {
|
const set = Object.values(entries).reduce((memo, {__typename, author}) => {
|
||||||
if (__typename === "PullRequest" && author.__typename !== "Bot") {
|
if (!author) {
|
||||||
memo.add("@" + author.login);
|
// author can be null if GH doesn't recognize them
|
||||||
// Commit authors are *always* of type "User", so have to discriminate some
|
return memo;
|
||||||
// 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;
|
switch (__typename) {
|
||||||
|
case "PullRequest":
|
||||||
|
if (author.__typename !== "Bot") {
|
||||||
|
memo.add("@" + author.login);
|
||||||
|
}
|
||||||
|
|
||||||
|
return memo;
|
||||||
|
case "Commit":
|
||||||
|
// Commit authors are *always* of type "User", so have to discriminate some
|
||||||
|
// other way. Making the assumption of a suffix for now, see how that goes.
|
||||||
|
// author.user can be nil if GH doesn't recognize the email
|
||||||
|
if (!author.user || author.user.login.endsWith("-bot")) {
|
||||||
|
return memo;
|
||||||
|
}
|
||||||
|
|
||||||
|
memo.add("@" + author.user.login);
|
||||||
|
return memo;
|
||||||
|
default:
|
||||||
|
throw new Error(`got an unexpected type for extractContributors: ${__typename}`);
|
||||||
|
}
|
||||||
}, new Set());
|
}, new Set());
|
||||||
|
|
||||||
return Array.from(set).sort((a, b) => a.localeCompare(b, "en", {sensitivity: "base"}));
|
return Array.from(set).sort((a, b) => a.localeCompare(b, "en", {sensitivity: "base"}));
|
||||||
|
|
|
@ -10,10 +10,35 @@
|
||||||
|
|
||||||
const {readFileSync, writeFileSync} = require("fs");
|
const {readFileSync, writeFileSync} = require("fs");
|
||||||
const colors = require("chalk");
|
const colors = require("chalk");
|
||||||
const log = require("../server/log").default;
|
|
||||||
const {join} = require("path");
|
const {join} = require("path");
|
||||||
const {spawnSync} = require("child_process");
|
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() {
|
function getGitUsername() {
|
||||||
return spawnSync("git", ["config", "user.name"], {encoding: "utf8"}).stdout.trim();
|
return spawnSync("git", ["config", "user.name"], {encoding: "utf8"}).stdout.trim();
|
||||||
}
|
}
|
||||||
|
|
115
server/client.ts
115
server/client.ts
|
@ -6,10 +6,12 @@ import crypto from "crypto";
|
||||||
import colors from "chalk";
|
import colors from "chalk";
|
||||||
|
|
||||||
import log from "./log";
|
import log from "./log";
|
||||||
import Chan, {ChanConfig, Channel, ChanType} from "./models/chan";
|
import Chan, {ChanConfig} from "./models/chan";
|
||||||
import Msg, {MessageType, UserInMessage} from "./models/msg";
|
import Msg from "./models/msg";
|
||||||
import Config from "./config";
|
import Config from "./config";
|
||||||
import {condensedTypes} from "../shared/irc";
|
import {condensedTypes} from "../shared/irc";
|
||||||
|
import {MessageType} from "../shared/types/msg";
|
||||||
|
import {SharedMention} from "../shared/types/mention";
|
||||||
|
|
||||||
import inputs from "./plugins/inputs";
|
import inputs from "./plugins/inputs";
|
||||||
import PublicClient from "./plugins/packages/publicClient";
|
import PublicClient from "./plugins/packages/publicClient";
|
||||||
|
@ -17,11 +19,12 @@ import SqliteMessageStorage from "./plugins/messageStorage/sqlite";
|
||||||
import TextFileMessageStorage from "./plugins/messageStorage/text";
|
import TextFileMessageStorage from "./plugins/messageStorage/text";
|
||||||
import Network, {IgnoreListItem, NetworkConfig, NetworkWithIrcFramework} from "./models/network";
|
import Network, {IgnoreListItem, NetworkConfig, NetworkWithIrcFramework} from "./models/network";
|
||||||
import ClientManager from "./clientManager";
|
import ClientManager from "./clientManager";
|
||||||
import {MessageStorage, SearchQuery, SearchResponse} from "./plugins/messageStorage/types";
|
import {MessageStorage} from "./plugins/messageStorage/types";
|
||||||
import {StorageCleaner} from "./storageCleaner";
|
import {StorageCleaner} from "./storageCleaner";
|
||||||
|
import {SearchQuery, SearchResponse} from "../shared/types/storage";
|
||||||
type OrderItem = Chan["id"] | Network["uuid"];
|
import {SharedChan, ChanType} from "../shared/types/chan";
|
||||||
type Order = OrderItem[];
|
import {SharedNetwork} from "../shared/types/network";
|
||||||
|
import {ServerToClientEvents} from "../shared/types/socket-events";
|
||||||
|
|
||||||
const events = [
|
const events = [
|
||||||
"away",
|
"away",
|
||||||
|
@ -82,15 +85,6 @@ export type UserConfig = {
|
||||||
networks?: NetworkConfig[];
|
networks?: NetworkConfig[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Mention = {
|
|
||||||
chanId: number;
|
|
||||||
msgId: number;
|
|
||||||
type: MessageType;
|
|
||||||
time: Date;
|
|
||||||
text: string;
|
|
||||||
from: UserInMessage;
|
|
||||||
};
|
|
||||||
|
|
||||||
class Client {
|
class Client {
|
||||||
awayMessage!: string;
|
awayMessage!: string;
|
||||||
lastActiveChannel!: number;
|
lastActiveChannel!: number;
|
||||||
|
@ -98,12 +92,12 @@ class Client {
|
||||||
[socketId: string]: {token: string; openChannel: number};
|
[socketId: string]: {token: string; openChannel: number};
|
||||||
};
|
};
|
||||||
config!: UserConfig;
|
config!: UserConfig;
|
||||||
id!: number;
|
id: string;
|
||||||
idMsg!: number;
|
idMsg!: number;
|
||||||
idChan!: number;
|
idChan!: number;
|
||||||
name!: string;
|
name!: string;
|
||||||
networks!: Network[];
|
networks!: Network[];
|
||||||
mentions!: Mention[];
|
mentions!: SharedMention[];
|
||||||
manager!: ClientManager;
|
manager!: ClientManager;
|
||||||
messageStorage!: MessageStorage[];
|
messageStorage!: MessageStorage[];
|
||||||
highlightRegex!: RegExp | null;
|
highlightRegex!: RegExp | null;
|
||||||
|
@ -113,12 +107,12 @@ class Client {
|
||||||
fileHash!: string;
|
fileHash!: string;
|
||||||
|
|
||||||
constructor(manager: ClientManager, name?: string, config = {} as UserConfig) {
|
constructor(manager: ClientManager, name?: string, config = {} as UserConfig) {
|
||||||
|
this.id = uuidv4();
|
||||||
_.merge(this, {
|
_.merge(this, {
|
||||||
awayMessage: "",
|
awayMessage: "",
|
||||||
lastActiveChannel: -1,
|
lastActiveChannel: -1,
|
||||||
attachedClients: {},
|
attachedClients: {},
|
||||||
config: config,
|
config: config,
|
||||||
id: uuidv4(),
|
|
||||||
idChan: 1,
|
idChan: 1,
|
||||||
idMsg: 1,
|
idMsg: 1,
|
||||||
name: name,
|
name: name,
|
||||||
|
@ -229,9 +223,12 @@ class Client {
|
||||||
return chan;
|
return chan;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(event: string, data?: any) {
|
emit<Ev extends keyof ServerToClientEvents>(
|
||||||
|
event: Ev,
|
||||||
|
...args: Parameters<ServerToClientEvents[Ev]>
|
||||||
|
) {
|
||||||
if (this.manager !== null) {
|
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.networks.push(network);
|
||||||
client.emit("network", {
|
client.emit("network", {
|
||||||
networks: [network.getFilteredClone(this.lastActiveChannel, -1)],
|
network: network.getFilteredClone(this.lastActiveChannel, -1),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!network.validate(client)) {
|
if (!network.validate(client)) {
|
||||||
|
@ -697,56 +694,39 @@ class Client {
|
||||||
this.emit("open", targetNetChan.chan.id);
|
this.emit("open", targetNetChan.chan.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
sort(data: {order: Order; type: "networks" | "channels"; target: string}) {
|
sortChannels(netid: SharedNetwork["uuid"], order: SharedChan["id"][]) {
|
||||||
const order = data.order;
|
const network = _.find(this.networks, {uuid: netid});
|
||||||
|
|
||||||
if (!_.isArray(order)) {
|
if (!network) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (data.type) {
|
network.channels.sort((a, b) => {
|
||||||
case "networks":
|
// Always sort lobby to the top regardless of what the client has sent
|
||||||
this.networks.sort((a, b) => order.indexOf(a.uuid) - order.indexOf(b.uuid));
|
// Because there's a lot of code that presumes channels[0] is the lobby
|
||||||
|
if (a.type === ChanType.LOBBY) {
|
||||||
// Sync order to connected clients
|
return -1;
|
||||||
this.emit("sync_sort", {
|
} else if (b.type === ChanType.LOBBY) {
|
||||||
order: this.networks.map((obj) => obj.uuid),
|
return 1;
|
||||||
type: data.type,
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "channels": {
|
|
||||||
const network = _.find(this.networks, {uuid: data.target});
|
|
||||||
|
|
||||||
if (!network) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
network.channels.sort((a, b) => {
|
|
||||||
// Always sort lobby to the top regardless of what the client has sent
|
|
||||||
// Because there's a lot of code that presumes channels[0] is the lobby
|
|
||||||
if (a.type === ChanType.LOBBY) {
|
|
||||||
return -1;
|
|
||||||
} else if (b.type === ChanType.LOBBY) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return order.indexOf(a.id) - order.indexOf(b.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sync order to connected clients
|
|
||||||
this.emit("sync_sort", {
|
|
||||||
order: network.channels.map((obj) => obj.id),
|
|
||||||
type: data.type,
|
|
||||||
target: network.uuid,
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
return order.indexOf(a.id) - order.indexOf(b.id);
|
||||||
|
});
|
||||||
this.save();
|
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}) {
|
names(data: {target: number}) {
|
||||||
|
@ -776,7 +756,7 @@ class Client {
|
||||||
|
|
||||||
quit(signOut?: boolean) {
|
quit(signOut?: boolean) {
|
||||||
const sockets = this.manager.sockets.sockets;
|
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) {
|
if (room) {
|
||||||
for (const user of room) {
|
for (const user of room) {
|
||||||
|
@ -836,12 +816,13 @@ class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: type session to this.attachedClients
|
// TODO: type session to this.attachedClients
|
||||||
registerPushSubscription(session: any, subscription: ClientPushSubscription, noSave = false) {
|
registerPushSubscription(session: any, subscription: PushSubscriptionJSON, noSave = false) {
|
||||||
if (
|
if (
|
||||||
!_.isPlainObject(subscription) ||
|
!_.isPlainObject(subscription) ||
|
||||||
!_.isPlainObject(subscription.keys) ||
|
|
||||||
typeof subscription.endpoint !== "string" ||
|
typeof subscription.endpoint !== "string" ||
|
||||||
!/^https?:\/\//.test(subscription.endpoint) ||
|
!/^https?:\/\//.test(subscription.endpoint) ||
|
||||||
|
!_.isPlainObject(subscription.keys) ||
|
||||||
|
!subscription.keys || // TS compiler doesn't understand isPlainObject
|
||||||
typeof subscription.keys.p256dh !== "string" ||
|
typeof subscription.keys.p256dh !== "string" ||
|
||||||
typeof subscription.keys.auth !== "string"
|
typeof subscription.keys.auth !== "string"
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import Config from "./config";
|
||||||
import {NetworkConfig} from "./models/network";
|
import {NetworkConfig} from "./models/network";
|
||||||
import WebPush from "./plugins/webpush";
|
import WebPush from "./plugins/webpush";
|
||||||
import log from "./log";
|
import log from "./log";
|
||||||
import {Server} from "socket.io";
|
import {Server} from "./server";
|
||||||
|
|
||||||
class ClientManager {
|
class ClientManager {
|
||||||
clients: Client[];
|
clients: Client[];
|
||||||
|
|
|
@ -4,6 +4,7 @@ import fs, {Stats} from "fs";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import colors from "chalk";
|
import colors from "chalk";
|
||||||
|
import {SearchOptions} from "ldapjs";
|
||||||
|
|
||||||
import log from "./log";
|
import log from "./log";
|
||||||
import Helper from "./helper";
|
import Helper from "./helper";
|
||||||
|
@ -44,7 +45,7 @@ export type Defaults = Pick<
|
||||||
| "saslAccount"
|
| "saslAccount"
|
||||||
| "saslPassword"
|
| "saslPassword"
|
||||||
> & {
|
> & {
|
||||||
join?: string;
|
join: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Identd = {
|
type Identd = {
|
||||||
|
@ -57,7 +58,7 @@ type SearchDN = {
|
||||||
rootPassword: string;
|
rootPassword: string;
|
||||||
filter: string;
|
filter: string;
|
||||||
base: string;
|
base: string;
|
||||||
scope: string;
|
scope: SearchOptions["scope"];
|
||||||
};
|
};
|
||||||
|
|
||||||
type Ldap = {
|
type Ldap = {
|
||||||
|
|
|
@ -2,36 +2,14 @@ import _ from "lodash";
|
||||||
import log from "../log";
|
import log from "../log";
|
||||||
import Config from "../config";
|
import Config from "../config";
|
||||||
import User from "./user";
|
import User from "./user";
|
||||||
import Msg, {MessageType} from "./msg";
|
import Msg from "./msg";
|
||||||
import storage from "../plugins/storage";
|
import storage from "../plugins/storage";
|
||||||
import Client from "../client";
|
import Client from "../client";
|
||||||
import Network from "./network";
|
import Network from "./network";
|
||||||
import Prefix from "./prefix";
|
import Prefix from "./prefix";
|
||||||
|
import {MessageType, SharedMsg} from "../../shared/types/msg";
|
||||||
export enum ChanType {
|
import {ChanType, SpecialChanType, ChanState} from "../../shared/types/chan";
|
||||||
CHANNEL = "channel",
|
import {SharedNetworkChan} from "../../shared/types/network";
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ChanConfig = {
|
export type ChanConfig = {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -60,7 +38,6 @@ class Chan {
|
||||||
data?: any;
|
data?: any;
|
||||||
closed?: boolean;
|
closed?: boolean;
|
||||||
num_users?: number;
|
num_users?: number;
|
||||||
static optionalProperties = ["userAway", "special", "data", "closed", "num_users"];
|
|
||||||
|
|
||||||
constructor(attr?: Partial<Chan>) {
|
constructor(attr?: Partial<Chan>) {
|
||||||
_.defaults(this, attr, {
|
_.defaults(this, attr, {
|
||||||
|
@ -84,18 +61,11 @@ class Chan {
|
||||||
}
|
}
|
||||||
|
|
||||||
pushMessage(client: Client, msg: Msg, increasesUnread = false) {
|
pushMessage(client: Client, msg: Msg, increasesUnread = false) {
|
||||||
const chan = this.id;
|
const chanId = this.id;
|
||||||
const obj = {chan, msg} as {
|
|
||||||
chan: number;
|
|
||||||
msg: Msg;
|
|
||||||
unread?: number;
|
|
||||||
highlight?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
msg.id = client.idMsg++;
|
msg.id = client.idMsg++;
|
||||||
|
|
||||||
// If this channel is open in any of the clients, do not increase unread counter
|
// 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) {
|
if (msg.self) {
|
||||||
// reset counters/markers when receiving self-/echo-message
|
// reset counters/markers when receiving self-/echo-message
|
||||||
|
@ -108,15 +78,15 @@ class Chan {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (increasesUnread || msg.highlight) {
|
if (increasesUnread || msg.highlight) {
|
||||||
obj.unread = ++this.unread;
|
this.unread++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.highlight) {
|
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
|
// Never store messages in public mode as the session
|
||||||
// is completely destroyed when the page gets closed
|
// 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) {
|
if (!Config.values.prefetch || !Config.values.prefetchStorage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -160,6 +131,7 @@ class Chan {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getSortedUsers(irc?: Network["irc"]) {
|
getSortedUsers(irc?: Network["irc"]) {
|
||||||
const users = Array.from(this.users.values());
|
const users = Array.from(this.users.values());
|
||||||
|
|
||||||
|
@ -182,21 +154,27 @@ class Chan {
|
||||||
return userModeSortPriority[a.mode] - userModeSortPriority[b.mode];
|
return userModeSortPriority[a.mode] - userModeSortPriority[b.mode];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
findMessage(msgId: number) {
|
findMessage(msgId: number) {
|
||||||
return this.messages.find((message) => message.id === msgId);
|
return this.messages.find((message) => message.id === msgId);
|
||||||
}
|
}
|
||||||
|
|
||||||
findUser(nick: string) {
|
findUser(nick: string) {
|
||||||
return this.users.get(nick.toLowerCase());
|
return this.users.get(nick.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
getUser(nick: string) {
|
getUser(nick: string) {
|
||||||
return this.findUser(nick) || new User({nick}, new Prefix([]));
|
return this.findUser(nick) || new User({nick}, new Prefix([]));
|
||||||
}
|
}
|
||||||
|
|
||||||
setUser(user: User) {
|
setUser(user: User) {
|
||||||
this.users.set(user.nick.toLowerCase(), user);
|
this.users.set(user.nick.toLowerCase(), user);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeUser(user: User) {
|
removeUser(user: User) {
|
||||||
this.users.delete(user.nick.toLowerCase());
|
this.users.delete(user.nick.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a clean clone of this channel that will be sent to the client.
|
* Get a clean clone of this channel that will be sent to the client.
|
||||||
* This function performs manual cloning of channel object for
|
* This function performs manual cloning of channel object for
|
||||||
|
@ -206,38 +184,54 @@ class Chan {
|
||||||
* If true, channel is assumed active.
|
* If true, channel is assumed active.
|
||||||
* @param {int} lastMessage - Last message id seen by active client to avoid sending duplicates.
|
* @param {int} lastMessage - Last message id seen by active client to avoid sending duplicates.
|
||||||
*/
|
*/
|
||||||
getFilteredClone(lastActiveChannel?: number | boolean, lastMessage?: number): FilteredChannel {
|
getFilteredClone(
|
||||||
return Object.keys(this).reduce((newChannel, prop) => {
|
lastActiveChannel?: number | boolean,
|
||||||
if (Chan.optionalProperties.includes(prop)) {
|
lastMessage?: number
|
||||||
if (this[prop] !== undefined || (Array.isArray(this[prop]) && this[prop].length)) {
|
): SharedNetworkChan {
|
||||||
newChannel[prop] = this[prop];
|
let msgs: SharedMsg[];
|
||||||
}
|
|
||||||
} else if (prop === "users") {
|
|
||||||
// Do not send users, client requests updated user list whenever needed
|
|
||||||
newChannel[prop] = [];
|
|
||||||
} else if (prop === "messages") {
|
|
||||||
// If client is reconnecting, only send new messages that client has not seen yet
|
|
||||||
if (lastMessage && lastMessage > -1) {
|
|
||||||
// When reconnecting, always send up to 100 messages to prevent message gaps on the client
|
|
||||||
// See https://github.com/thelounge/thelounge/issues/1883
|
|
||||||
newChannel[prop] = this[prop].filter((m) => m.id > lastMessage).slice(-100);
|
|
||||||
} else {
|
|
||||||
// If channel is active, send up to 100 last messages, for all others send just 1
|
|
||||||
// Client will automatically load more messages whenever needed based on last seen messages
|
|
||||||
const messagesToSend =
|
|
||||||
lastActiveChannel === true || this.id === lastActiveChannel ? 100 : 1;
|
|
||||||
|
|
||||||
newChannel[prop] = this[prop].slice(-messagesToSend);
|
// If client is reconnecting, only send new messages that client has not seen yet
|
||||||
}
|
if (lastMessage && lastMessage > -1) {
|
||||||
|
// When reconnecting, always send up to 100 messages to prevent message gaps on the client
|
||||||
|
// See https://github.com/thelounge/thelounge/issues/1883
|
||||||
|
msgs = this.messages.filter((m) => m.id > lastMessage).slice(-100);
|
||||||
|
} else {
|
||||||
|
// If channel is active, send up to 100 last messages, for all others send just 1
|
||||||
|
// Client will automatically load more messages whenever needed based on last seen messages
|
||||||
|
const messagesToSend =
|
||||||
|
lastActiveChannel === true || this.id === lastActiveChannel ? 100 : 1;
|
||||||
|
msgs = this.messages.slice(-messagesToSend);
|
||||||
|
}
|
||||||
|
|
||||||
(newChannel as FilteredChannel).totalMessages = this[prop].length;
|
return {
|
||||||
} else {
|
id: this.id,
|
||||||
newChannel[prop] = this[prop];
|
messages: msgs,
|
||||||
}
|
totalMessages: this.messages.length,
|
||||||
|
name: this.name,
|
||||||
|
key: this.key,
|
||||||
|
topic: this.topic,
|
||||||
|
firstUnread: this.firstUnread,
|
||||||
|
unread: this.unread,
|
||||||
|
highlight: this.highlight,
|
||||||
|
muted: this.muted,
|
||||||
|
type: this.type,
|
||||||
|
state: this.state,
|
||||||
|
|
||||||
return newChannel;
|
special: this.special,
|
||||||
}, {}) as FilteredChannel;
|
data: this.data,
|
||||||
|
closed: this.closed,
|
||||||
|
num_users: this.num_users,
|
||||||
|
};
|
||||||
|
// TODO: funny array mutation below might need to be reproduced
|
||||||
|
// static optionalProperties = ["userAway", "special", "data", "closed", "num_users"];
|
||||||
|
// return Object.keys(this).reduce((newChannel, prop) => {
|
||||||
|
// if (Chan.optionalProperties.includes(prop)) {
|
||||||
|
// if (this[prop] !== undefined || (Array.isArray(this[prop]) && this[prop].length)) {
|
||||||
|
// newChannel[prop] = this[prop];
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
writeUserLog(client: Client, msg: Msg) {
|
writeUserLog(client: Client, msg: Msg) {
|
||||||
this.messages.push(msg);
|
this.messages.push(msg);
|
||||||
|
|
||||||
|
@ -270,6 +264,7 @@ class Chan {
|
||||||
messageStorage.index(target.network, targetChannel, msg).catch((e) => log.error(e));
|
messageStorage.index(target.network, targetChannel, msg).catch((e) => log.error(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMessages(client: Client, network: Network) {
|
loadMessages(client: Client, network: Network) {
|
||||||
if (!this.isLoggable()) {
|
if (!this.isLoggable()) {
|
||||||
return;
|
return;
|
||||||
|
@ -326,15 +321,23 @@ class Chan {
|
||||||
log.error(`Failed to load messages for ${client.name}: ${err.toString()}`)
|
log.error(`Failed to load messages for ${client.name}: ${err.toString()}`)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoggable() {
|
isLoggable() {
|
||||||
return this.type === ChanType.CHANNEL || this.type === ChanType.QUERY;
|
return this.type === ChanType.CHANNEL || this.type === ChanType.QUERY;
|
||||||
}
|
}
|
||||||
|
|
||||||
setMuteStatus(muted: boolean) {
|
setMuteStatus(muted: boolean) {
|
||||||
this.muted = !!muted;
|
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());
|
network.irc.raw("ZNC", "*playback", "PLAY", channel.name, from.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,41 +1,5 @@
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import {LinkPreview} from "../plugins/irc-events/link";
|
import {MessageType, LinkPreview, UserInMessage} from "../../shared/types/msg";
|
||||||
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",
|
|
||||||
}
|
|
||||||
|
|
||||||
class Msg {
|
class Msg {
|
||||||
from!: UserInMessage;
|
from!: UserInMessage;
|
||||||
|
@ -70,7 +34,7 @@ class Msg {
|
||||||
raw_modes!: any;
|
raw_modes!: any;
|
||||||
when!: Date;
|
when!: Date;
|
||||||
whois!: any;
|
whois!: any;
|
||||||
users!: UserInMessage[] | string[];
|
users!: string[];
|
||||||
statusmsgGroup!: string;
|
statusmsgGroup!: string;
|
||||||
params!: string[];
|
params!: string[];
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,17 @@
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import {v4 as uuidv4} from "uuid";
|
import {v4 as uuidv4} from "uuid";
|
||||||
import IrcFramework, {Client as IRCClient} from "irc-framework";
|
import IrcFramework, {Client as IRCClient} from "irc-framework";
|
||||||
import Chan, {ChanConfig, Channel, ChanType} from "./chan";
|
import Chan, {ChanConfig, Channel} from "./chan";
|
||||||
import Msg, {MessageType} from "./msg";
|
import Msg from "./msg";
|
||||||
import Prefix from "./prefix";
|
import Prefix from "./prefix";
|
||||||
import Helper, {Hostmask} from "../helper";
|
import Helper, {Hostmask} from "../helper";
|
||||||
import Config, {WebIRC} from "../config";
|
import Config, {WebIRC} from "../config";
|
||||||
import STSPolicies from "../plugins/sts";
|
import STSPolicies from "../plugins/sts";
|
||||||
import ClientCertificate, {ClientCertificateType} from "../plugins/clientCertificate";
|
import ClientCertificate, {ClientCertificateType} from "../plugins/clientCertificate";
|
||||||
import Client from "../client";
|
import Client from "../client";
|
||||||
|
import {MessageType} from "../../shared/types/msg";
|
||||||
/**
|
import {ChanType} from "../../shared/types/chan";
|
||||||
* List of keys which should be sent to the client by default.
|
import {SharedNetwork} from "../../shared/types/network";
|
||||||
*/
|
|
||||||
const fieldsForClient = {
|
|
||||||
uuid: true,
|
|
||||||
name: true,
|
|
||||||
nick: true,
|
|
||||||
serverOptions: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
type NetworkIrcOptions = {
|
type NetworkIrcOptions = {
|
||||||
host: string;
|
host: string;
|
||||||
|
@ -52,7 +45,7 @@ type NetworkStatus = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IgnoreListItem = Hostmask & {
|
export type IgnoreListItem = Hostmask & {
|
||||||
when?: number;
|
when: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type IgnoreList = IgnoreListItem[];
|
type IgnoreList = IgnoreListItem[];
|
||||||
|
@ -505,24 +498,17 @@ class Network {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getFilteredClone(lastActiveChannel?: number, lastMessage?: number) {
|
getFilteredClone(lastActiveChannel?: number, lastMessage?: number): SharedNetwork {
|
||||||
const filteredNetwork = Object.keys(this).reduce((newNetwork, prop) => {
|
return {
|
||||||
if (prop === "channels") {
|
uuid: this.uuid,
|
||||||
// Channels objects perform their own cloning
|
name: this.name,
|
||||||
newNetwork[prop] = this[prop].map((channel) =>
|
nick: this.nick,
|
||||||
channel.getFilteredClone(lastActiveChannel, lastMessage)
|
serverOptions: this.serverOptions,
|
||||||
);
|
status: this.getNetworkStatus(),
|
||||||
} else if (fieldsForClient[prop]) {
|
channels: this.channels.map((channel) =>
|
||||||
// Some properties that are not useful for the client are skipped
|
channel.getFilteredClone(lastActiveChannel, lastMessage)
|
||||||
newNetwork[prop] = this[prop];
|
),
|
||||||
}
|
};
|
||||||
|
|
||||||
return newNetwork;
|
|
||||||
}, {}) as Network;
|
|
||||||
|
|
||||||
filteredNetwork.status = this.getNetworkStatus();
|
|
||||||
|
|
||||||
return filteredNetwork;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getNetworkStatus() {
|
getNetworkStatus() {
|
||||||
|
|
|
@ -67,11 +67,11 @@ function advancedLdapAuth(user: string, password: string, callback: (success: bo
|
||||||
});
|
});
|
||||||
|
|
||||||
const base = config.ldap.searchDN.base;
|
const base = config.ldap.searchDN.base;
|
||||||
const searchOptions = {
|
const searchOptions: SearchOptions = {
|
||||||
scope: config.ldap.searchDN.scope,
|
scope: config.ldap.searchDN.scope,
|
||||||
filter: `(&(${config.ldap.primaryKey}=${userDN})${config.ldap.searchDN.filter})`,
|
filter: `(&(${config.ldap.primaryKey}=${userDN})${config.ldap.searchDN.filter})`,
|
||||||
attributes: ["dn"],
|
attributes: ["dn"],
|
||||||
} as SearchOptions;
|
};
|
||||||
|
|
||||||
ldapclient.on("error", function (err: Error) {
|
ldapclient.on("error", function (err: Error) {
|
||||||
log.error(`Unable to connect to LDAP server: ${err.toString()}`);
|
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 remainingUsers = new Set(users);
|
||||||
|
|
||||||
const searchOptions = {
|
const searchOptions: SearchOptions = {
|
||||||
scope: config.ldap.searchDN.scope,
|
scope: config.ldap.searchDN.scope,
|
||||||
filter: `${config.ldap.searchDN.filter}`,
|
filter: `${config.ldap.searchDN.filter}`,
|
||||||
attributes: [config.ldap.primaryKey],
|
attributes: [config.ldap.primaryKey],
|
||||||
paged: true,
|
paged: true,
|
||||||
} as SearchOptions;
|
};
|
||||||
|
|
||||||
ldapclient.search(base, searchOptions, function (err2, res) {
|
ldapclient.search(base, searchOptions, function (err2, res) {
|
||||||
if (err2) {
|
if (err2) {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import log from "../log";
|
||||||
import pkg from "../../package.json";
|
import pkg from "../../package.json";
|
||||||
import ClientManager from "../clientManager";
|
import ClientManager from "../clientManager";
|
||||||
import Config from "../config";
|
import Config from "../config";
|
||||||
|
import {SharedChangelogData} from "../../shared/types/changelog";
|
||||||
|
|
||||||
const TIME_TO_LIVE = 15 * 60 * 1000; // 15 minutes, in milliseconds
|
const TIME_TO_LIVE = 15 * 60 * 1000; // 15 minutes, in milliseconds
|
||||||
|
|
||||||
|
@ -12,31 +13,17 @@ export default {
|
||||||
fetch,
|
fetch,
|
||||||
checkForUpdates,
|
checkForUpdates,
|
||||||
};
|
};
|
||||||
export type ChangelogData = {
|
const versions: SharedChangelogData = {
|
||||||
current: {
|
|
||||||
prerelease: boolean;
|
|
||||||
version: string;
|
|
||||||
changelog?: string;
|
|
||||||
url: string;
|
|
||||||
};
|
|
||||||
expiresAt: number;
|
|
||||||
latest?: {
|
|
||||||
prerelease: boolean;
|
|
||||||
version: string;
|
|
||||||
url: string;
|
|
||||||
};
|
|
||||||
packages?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const versions = {
|
|
||||||
current: {
|
current: {
|
||||||
|
prerelease: false,
|
||||||
version: `v${pkg.version}`,
|
version: `v${pkg.version}`,
|
||||||
changelog: undefined,
|
changelog: undefined,
|
||||||
|
url: "", // TODO: properly init
|
||||||
},
|
},
|
||||||
expiresAt: -1,
|
expiresAt: -1,
|
||||||
latest: undefined,
|
latest: undefined,
|
||||||
packages: undefined,
|
packages: undefined,
|
||||||
} as ChangelogData;
|
};
|
||||||
|
|
||||||
async function fetch() {
|
async function fetch() {
|
||||||
const time = Date.now();
|
const time = Date.now();
|
||||||
|
|
|
@ -31,7 +31,7 @@ function get(uuid: string): ClientCertificateType | null {
|
||||||
return {
|
return {
|
||||||
private_key: fs.readFileSync(paths.privateKeyPath, "utf-8"),
|
private_key: fs.readFileSync(paths.privateKeyPath, "utf-8"),
|
||||||
certificate: fs.readFileSync(paths.certificatePath, "utf-8"),
|
certificate: fs.readFileSync(paths.certificatePath, "utf-8"),
|
||||||
} as ClientCertificateType;
|
};
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
log.error("Unable to get certificate", e);
|
log.error("Unable to get certificate", e);
|
||||||
}
|
}
|
||||||
|
@ -122,10 +122,10 @@ function generate() {
|
||||||
// Sign this certificate with a SHA256 signature
|
// Sign this certificate with a SHA256 signature
|
||||||
cert.sign(keys.privateKey, md.sha256.create());
|
cert.sign(keys.privateKey, md.sha256.create());
|
||||||
|
|
||||||
const pem = {
|
const pem: ClientCertificateType = {
|
||||||
private_key: pki.privateKeyToPem(keys.privateKey),
|
private_key: pki.privateKeyToPem(keys.privateKey),
|
||||||
certificate: pki.certificateToPem(cert),
|
certificate: pki.certificateToPem(cert),
|
||||||
} as ClientCertificateType;
|
};
|
||||||
|
|
||||||
return pem;
|
return pem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {PluginInputHandler} from "./index";
|
import {PluginInputHandler} from "./index";
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import {ChanType} from "../../models/chan";
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
import {ChanType} from "../../../shared/types/chan";
|
||||||
|
|
||||||
const commands = ["slap", "me"];
|
const commands = ["slap", "me"];
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {ChanType} from "../../models/chan";
|
import Msg from "../../models/msg";
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
|
||||||
import {PluginInputHandler} from "./index";
|
import {PluginInputHandler} from "./index";
|
||||||
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
import {ChanType} from "../../../shared/types/chan";
|
||||||
|
|
||||||
const commands = ["ban", "unban", "banlist", "kickban"];
|
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 {PluginInputHandler} from "./index";
|
||||||
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
|
||||||
const commands = ["connect", "server"];
|
const commands = ["connect", "server"];
|
||||||
const allowDisconnected = true;
|
const allowDisconnected = true;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import {PluginInputHandler} from "./index";
|
import {PluginInputHandler} from "./index";
|
||||||
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
|
||||||
const commands = ["ctcp"];
|
const commands = ["ctcp"];
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,14 @@
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import Helper from "../../helper";
|
import Helper from "../../helper";
|
||||||
import {PluginInputHandler} from "./index";
|
import {PluginInputHandler} from "./index";
|
||||||
import {IgnoreListItem} from "../../models/network";
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
import {ChanType, SpecialChanType} from "../../models/chan";
|
|
||||||
|
|
||||||
const commands = ["ignore", "unignore", "ignorelist"];
|
const commands = ["ignore", "unignore"];
|
||||||
|
|
||||||
const input: PluginInputHandler = function (network, chan, cmd, args) {
|
const input: PluginInputHandler = function (network, chan, cmd, args) {
|
||||||
const client = this;
|
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(
|
chan.pushMessage(
|
||||||
client,
|
client,
|
||||||
new Msg({
|
new Msg({
|
||||||
|
@ -24,16 +20,13 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmd !== "ignorelist") {
|
const target = args[0].trim();
|
||||||
// Trim to remove any spaces from the hostmask
|
const hostmask = Helper.parseHostmask(target);
|
||||||
target = args[0].trim();
|
|
||||||
hostmask = Helper.parseHostmask(target) as IgnoreListItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (cmd) {
|
switch (cmd) {
|
||||||
case "ignore": {
|
case "ignore": {
|
||||||
// IRC nicks are case insensitive
|
// IRC nicks are case insensitive
|
||||||
if (hostmask!.nick.toLowerCase() === network.nick.toLowerCase()) {
|
if (hostmask.nick.toLowerCase() === network.nick.toLowerCase()) {
|
||||||
chan.pushMessage(
|
chan.pushMessage(
|
||||||
client,
|
client,
|
||||||
new Msg({
|
new Msg({
|
||||||
|
@ -41,25 +34,14 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
||||||
text: "You can't ignore yourself",
|
text: "You can't ignore yourself",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else if (
|
return;
|
||||||
!network.ignoreList.some(function (entry) {
|
}
|
||||||
return Helper.compareHostmask(entry, hostmask!);
|
|
||||||
|
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(
|
chan.pushMessage(
|
||||||
client,
|
client,
|
||||||
new Msg({
|
new Msg({
|
||||||
|
@ -67,32 +49,31 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
||||||
text: "The specified user/hostmask is already ignored",
|
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": {
|
case "unignore": {
|
||||||
const idx = network.ignoreList.findIndex(function (entry) {
|
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
|
if (idx === -1) {
|
||||||
// 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 {
|
|
||||||
chan.pushMessage(
|
chan.pushMessage(
|
||||||
client,
|
client,
|
||||||
new Msg({
|
new Msg({
|
||||||
|
@ -100,52 +81,20 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
||||||
text: "The specified user/hostmask is not ignored",
|
text: "The specified user/hostmask is not ignored",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
network.ignoreList.splice(idx, 1);
|
||||||
|
client.save();
|
||||||
|
|
||||||
|
chan.pushMessage(
|
||||||
|
client,
|
||||||
|
new Msg({
|
||||||
|
type: MessageType.ERROR, // TODO: Successfully removed via type.Error 🤔 ?
|
||||||
|
text: `Successfully removed \u0002${hostmask.nick}!${hostmask.ident}@${hostmask.hostname}\u000f from ignorelist`,
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
case "ignorelist":
|
|
||||||
if (network.ignoreList.length === 0) {
|
|
||||||
chan.pushMessage(
|
|
||||||
client,
|
|
||||||
new Msg({
|
|
||||||
type: MessageType.ERROR,
|
|
||||||
text: "Ignorelist is empty",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const chanName = "Ignored users";
|
|
||||||
const ignored = network.ignoreList.map((data) => ({
|
|
||||||
hostmask: `${data.nick}!${data.ident}@${data.hostname}`,
|
|
||||||
when: data.when,
|
|
||||||
}));
|
|
||||||
let newChan = network.getChannel(chanName);
|
|
||||||
|
|
||||||
if (typeof newChan === "undefined") {
|
|
||||||
newChan = client.createChannel({
|
|
||||||
type: ChanType.SPECIAL,
|
|
||||||
special: SpecialChanType.IGNORELIST,
|
|
||||||
name: chanName,
|
|
||||||
data: ignored,
|
|
||||||
});
|
|
||||||
client.emit("join", {
|
|
||||||
network: network.uuid,
|
|
||||||
chan: newChan.getFilteredClone(true),
|
|
||||||
index: network.addChannel(newChan),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// TODO: add type for this chan/event
|
|
||||||
newChan.data = ignored;
|
|
||||||
|
|
||||||
client.emit("msg:special", {
|
|
||||||
chan: newChan.id,
|
|
||||||
data: ignored,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
57
server/plugins/inputs/ignorelist.ts
Normal file
57
server/plugins/inputs/ignorelist.ts
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import {PluginInputHandler} from "./index";
|
||||||
|
import Msg from "../../models/msg";
|
||||||
|
import {ChanType, SpecialChanType} from "../../../shared/types/chan";
|
||||||
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
|
||||||
|
const commands = ["ignorelist"];
|
||||||
|
|
||||||
|
const input: PluginInputHandler = function (network, chan, _cmd, _args) {
|
||||||
|
const client = this;
|
||||||
|
|
||||||
|
if (network.ignoreList.length === 0) {
|
||||||
|
chan.pushMessage(
|
||||||
|
client,
|
||||||
|
new Msg({
|
||||||
|
type: MessageType.ERROR,
|
||||||
|
text: "Ignorelist is empty",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chanName = "Ignored users";
|
||||||
|
const ignored = network.ignoreList.map((data) => ({
|
||||||
|
hostmask: `${data.nick}!${data.ident}@${data.hostname}`,
|
||||||
|
when: data.when,
|
||||||
|
}));
|
||||||
|
let newChan = network.getChannel(chanName);
|
||||||
|
|
||||||
|
if (typeof newChan === "undefined") {
|
||||||
|
newChan = client.createChannel({
|
||||||
|
type: ChanType.SPECIAL,
|
||||||
|
special: SpecialChanType.IGNORELIST,
|
||||||
|
name: chanName,
|
||||||
|
data: ignored,
|
||||||
|
});
|
||||||
|
client.emit("join", {
|
||||||
|
network: network.uuid,
|
||||||
|
chan: newChan.getFilteredClone(true),
|
||||||
|
shouldOpen: false,
|
||||||
|
index: network.addChannel(newChan),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add type for this chan/event
|
||||||
|
newChan.data = ignored;
|
||||||
|
|
||||||
|
client.emit("msg:special", {
|
||||||
|
chan: newChan.id,
|
||||||
|
data: ignored,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
commands,
|
||||||
|
input,
|
||||||
|
};
|
|
@ -54,6 +54,7 @@ const builtInInputs = [
|
||||||
"ctcp",
|
"ctcp",
|
||||||
"disconnect",
|
"disconnect",
|
||||||
"ignore",
|
"ignore",
|
||||||
|
"ignorelist",
|
||||||
"invite",
|
"invite",
|
||||||
"kick",
|
"kick",
|
||||||
"kill",
|
"kill",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {PluginInputHandler} from "./index";
|
import {PluginInputHandler} from "./index";
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import {ChanType} from "../../models/chan";
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
import {ChanType} from "../../../shared/types/chan";
|
||||||
|
|
||||||
const commands = ["invite", "invitelist"];
|
const commands = ["invite", "invitelist"];
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {PluginInputHandler} from "./index";
|
import {PluginInputHandler} from "./index";
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import {ChanType} from "../../models/chan";
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
import {ChanType} from "../../../shared/types/chan";
|
||||||
|
|
||||||
const commands = ["kick"];
|
const commands = ["kick"];
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import {PluginInputHandler} from "./index";
|
import {PluginInputHandler} from "./index";
|
||||||
|
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import {ChanType} from "../../models/chan";
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
import {ChanType} from "../../../shared/types/chan";
|
||||||
|
|
||||||
const commands = ["mode", "umode", "op", "deop", "hop", "dehop", "voice", "devoice"];
|
const commands = ["mode", "umode", "op", "deop", "hop", "dehop", "voice", "devoice"];
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import {PluginInputHandler} from "./index";
|
import {PluginInputHandler} from "./index";
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import Chan, {ChanType} from "../../models/chan";
|
import Chan from "../../models/chan";
|
||||||
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
import {ChanType} from "../../../shared/types/chan";
|
||||||
|
|
||||||
const commands = ["query", "msg", "say"];
|
const commands = ["query", "msg", "say"];
|
||||||
|
|
||||||
|
@ -97,10 +99,10 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
||||||
// being sent back to us.
|
// being sent back to us.
|
||||||
if (!network.irc.network.cap.isEnabled("echo-message")) {
|
if (!network.irc.network.cap.isEnabled("echo-message")) {
|
||||||
const parsedTarget = network.irc.network.extractTargetGroup(targetName);
|
const parsedTarget = network.irc.network.extractTargetGroup(targetName);
|
||||||
let targetGroup;
|
let targetGroup: string | undefined = undefined;
|
||||||
|
|
||||||
if (parsedTarget) {
|
if (parsedTarget) {
|
||||||
targetName = parsedTarget.target as string;
|
targetName = parsedTarget.target;
|
||||||
targetGroup = parsedTarget.target_group;
|
targetGroup = parsedTarget.target_group;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,10 @@ import Chan from "../../models/chan";
|
||||||
import Network from "../../models/network";
|
import Network from "../../models/network";
|
||||||
import {PluginInputHandler} from "./index";
|
import {PluginInputHandler} from "./index";
|
||||||
|
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
|
|
||||||
import Client from "../../client";
|
import Client from "../../client";
|
||||||
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
|
||||||
const commands = ["mute", "unmute"];
|
const commands = ["mute", "unmute"];
|
||||||
const allowDisconnected = true;
|
const allowDisconnected = true;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {PluginInputHandler} from "./index";
|
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 commands = ["nick"];
|
||||||
const allowDisconnected = true;
|
const allowDisconnected = true;
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import {PluginInputHandler} from "./index";
|
import {PluginInputHandler} from "./index";
|
||||||
|
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import Config from "../../config";
|
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 commands = ["close", "leave", "part"];
|
||||||
const allowDisconnected = true;
|
const allowDisconnected = true;
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import {PluginInputHandler} from "./index";
|
import {PluginInputHandler} from "./index";
|
||||||
|
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import {ChanType} from "../../models/chan";
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
import {ChanType} from "../../../shared/types/chan";
|
||||||
|
|
||||||
const commands = ["cycle", "rejoin"];
|
const commands = ["cycle", "rejoin"];
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import {PluginInputHandler} from "./index";
|
import {PluginInputHandler} from "./index";
|
||||||
|
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import {ChanType} from "../../models/chan";
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
import {ChanType} from "../../../shared/types/chan";
|
||||||
|
|
||||||
const commands = ["topic"];
|
const commands = ["topic"];
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
import {IrcEventHandler} from "../../client";
|
||||||
import {ChanType} from "../../models/chan";
|
import Msg from "../../models/msg";
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
import {ChanType} from "../../../shared/types/chan";
|
||||||
|
|
||||||
export default <IrcEventHandler>function (irc, network) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -14,7 +14,7 @@ export default <IrcEventHandler>function (irc, network) {
|
||||||
handleSTS(data, false);
|
handleSTS(data, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleSTS(data, shouldReconnect) {
|
function handleSTS(data, shouldReconnect: boolean) {
|
||||||
if (!Object.prototype.hasOwnProperty.call(data.capabilities, "sts")) {
|
if (!Object.prototype.hasOwnProperty.call(data.capabilities, "sts")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -3,10 +3,11 @@ import _ from "lodash";
|
||||||
import {IrcEventHandler} from "../../client";
|
import {IrcEventHandler} from "../../client";
|
||||||
|
|
||||||
import log from "../../log";
|
import log from "../../log";
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import Helper from "../../helper";
|
import Helper from "../../helper";
|
||||||
import Config from "../../config";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import {IrcEventHandler} from "../../client";
|
import {IrcEventHandler} from "../../client";
|
||||||
import Helper from "../../helper";
|
import Helper from "../../helper";
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import User from "../../models/user";
|
import User from "../../models/user";
|
||||||
import pkg from "../../../package.json";
|
import pkg from "../../../package.json";
|
||||||
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
|
||||||
const ctcpResponses = {
|
const ctcpResponses = {
|
||||||
CLIENTINFO: () =>
|
CLIENTINFO: () =>
|
||||||
|
@ -12,7 +13,7 @@ const ctcpResponses = {
|
||||||
.join(" "),
|
.join(" "),
|
||||||
PING: ({message}: {message: string}) => message.substring(5),
|
PING: ({message}: {message: string}) => message.substring(5),
|
||||||
SOURCE: () => pkg.repository.url,
|
SOURCE: () => pkg.repository.url,
|
||||||
VERSION: () => pkg.name + " " + Helper.getVersion() + " -- " + pkg.homepage,
|
VERSION: () => pkg.name + " -- " + pkg.homepage,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default <IrcEventHandler>function (irc, network) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
import {IrcEventHandler} from "../../client";
|
||||||
|
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import Config from "../../config";
|
import Config from "../../config";
|
||||||
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
|
||||||
export default <IrcEventHandler>function (irc, network) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import {IrcEventHandler} from "../../client";
|
import {IrcEventHandler} from "../../client";
|
||||||
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
|
||||||
export default <IrcEventHandler>function (irc, network) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import {IrcEventHandler} from "../../client";
|
import {IrcEventHandler} from "../../client";
|
||||||
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
|
||||||
export default <IrcEventHandler>function (irc, network) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import User from "../../models/user";
|
import User from "../../models/user";
|
||||||
import type {IrcEventHandler} from "../../client";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
@ -18,6 +19,7 @@ export default <IrcEventHandler>function (irc, network) {
|
||||||
client.emit("join", {
|
client.emit("join", {
|
||||||
network: network.uuid,
|
network: network.uuid,
|
||||||
chan: chan.getFilteredClone(true),
|
chan: chan.getFilteredClone(true),
|
||||||
|
shouldOpen: false,
|
||||||
index: network.addChannel(chan),
|
index: network.addChannel(chan),
|
||||||
});
|
});
|
||||||
client.save();
|
client.save();
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
import {IrcEventHandler} from "../../client";
|
||||||
import {ChanState} from "../../models/chan";
|
|
||||||
|
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import User from "../../models/user";
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
import {ChanState} from "../../../shared/types/chan";
|
||||||
|
|
||||||
export default <IrcEventHandler>function (irc, network) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
@ -14,11 +14,12 @@ export default <IrcEventHandler>function (irc, network) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const user = chan.getUser(data.kicked!);
|
||||||
const msg = new Msg({
|
const msg = new Msg({
|
||||||
type: MessageType.KICK,
|
type: MessageType.KICK,
|
||||||
time: data.time,
|
time: data.time,
|
||||||
from: chan.getUser(data.nick),
|
from: chan.getUser(data.nick),
|
||||||
target: chan.getUser(data.kicked!),
|
target: user,
|
||||||
text: data.message || "",
|
text: data.message || "",
|
||||||
highlight: data.kicked === irc.user.nick,
|
highlight: data.kicked === irc.user.nick,
|
||||||
self: data.nick === irc.user.nick,
|
self: data.nick === irc.user.nick,
|
||||||
|
@ -34,7 +35,7 @@ export default <IrcEventHandler>function (irc, network) {
|
||||||
state: chan.state,
|
state: chan.state,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
chan.removeUser(msg.target as User);
|
chan.removeUser(user);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,6 +6,7 @@ import mime from "mime-types";
|
||||||
import log from "../../log";
|
import log from "../../log";
|
||||||
import Config from "../../config";
|
import Config from "../../config";
|
||||||
import {findLinksWithSchema} from "../../../shared/linkify";
|
import {findLinksWithSchema} from "../../../shared/linkify";
|
||||||
|
import {LinkPreview} from "../../../shared/types/msg";
|
||||||
import storage from "../storage";
|
import storage from "../storage";
|
||||||
import Client from "../../client";
|
import Client from "../../client";
|
||||||
import Chan from "../../models/chan";
|
import Chan from "../../models/chan";
|
||||||
|
@ -20,23 +21,6 @@ const currentFetchPromises = new Map<string, Promise<FetchRequest>>();
|
||||||
const imageTypeRegex = /^image\/.+/;
|
const imageTypeRegex = /^image\/.+/;
|
||||||
const mediaTypeRegex = /^(audio|video)\/.+/;
|
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) {
|
export default function (client: Client, chan: Chan, msg: Msg, cleanText: string) {
|
||||||
if (!Config.values.prefetch) {
|
if (!Config.values.prefetch) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
@ -50,6 +51,7 @@ export default <IrcEventHandler>function (irc, network) {
|
||||||
client.emit("join", {
|
client.emit("join", {
|
||||||
network: network.uuid,
|
network: network.uuid,
|
||||||
chan: chan.getFilteredClone(true),
|
chan: chan.getFilteredClone(true),
|
||||||
|
shouldOpen: false,
|
||||||
index: network.addChannel(chan),
|
index: network.addChannel(chan),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,24 +1,38 @@
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg from "../../models/msg";
|
||||||
import LinkPrefetch from "./link";
|
import LinkPrefetch from "./link";
|
||||||
import {cleanIrcMessage} from "../../../shared/irc";
|
import {cleanIrcMessage} from "../../../shared/irc";
|
||||||
import Helper from "../../helper";
|
import Helper from "../../helper";
|
||||||
import {IrcEventHandler} from "../../client";
|
import {IrcEventHandler} from "../../client";
|
||||||
import Chan, {ChanType} from "../../models/chan";
|
import Chan from "../../models/chan";
|
||||||
import User from "../../models/user";
|
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;
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
||||||
irc.on("notice", function (data) {
|
irc.on("notice", function (data) {
|
||||||
data.type = MessageType.NOTICE;
|
handleMessage(convertForHandle(MessageType.NOTICE, data));
|
||||||
|
|
||||||
type ModifiedData = typeof data & {
|
|
||||||
type: MessageType.NOTICE;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMessage(data as ModifiedData);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
irc.on("action", function (data) {
|
irc.on("action", function (data) {
|
||||||
|
@ -37,18 +51,7 @@ export default <IrcEventHandler>function (irc, network) {
|
||||||
handleMessage(data);
|
handleMessage(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleMessage(data: {
|
function handleMessage(data: HandleInput) {
|
||||||
nick: string;
|
|
||||||
hostname: string;
|
|
||||||
ident: string;
|
|
||||||
target: string;
|
|
||||||
type: MessageType;
|
|
||||||
time: number;
|
|
||||||
text?: string;
|
|
||||||
from_server?: boolean;
|
|
||||||
message: string;
|
|
||||||
group?: string;
|
|
||||||
}) {
|
|
||||||
let chan: Chan | undefined;
|
let chan: Chan | undefined;
|
||||||
let from: User;
|
let from: User;
|
||||||
let highlight = false;
|
let highlight = false;
|
||||||
|
@ -105,6 +108,7 @@ export default <IrcEventHandler>function (irc, network) {
|
||||||
client.emit("join", {
|
client.emit("join", {
|
||||||
network: network.uuid,
|
network: network.uuid,
|
||||||
chan: chan.getFilteredClone(true),
|
chan: chan.getFilteredClone(true),
|
||||||
|
shouldOpen: false,
|
||||||
index: network.addChannel(chan),
|
index: network.addChannel(chan),
|
||||||
});
|
});
|
||||||
client.save();
|
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
|
// msg is constructed down here because `from` is being copied in the constructor
|
||||||
const msg = new Msg({
|
const msg = new Msg({
|
||||||
type: data.type,
|
type: data.type,
|
||||||
time: data.time as any,
|
time: new Date(data.time),
|
||||||
text: data.message,
|
text: data.message,
|
||||||
self: self,
|
self: self,
|
||||||
from: from,
|
from: from,
|
||||||
|
@ -164,7 +168,6 @@ export default <IrcEventHandler>function (irc, network) {
|
||||||
|
|
||||||
while ((match = nickRegExp.exec(data.message))) {
|
while ((match = nickRegExp.exec(data.message))) {
|
||||||
if (chan.findUser(match[1])) {
|
if (chan.findUser(match[1])) {
|
||||||
// @ts-expect-error Type 'string' is not assignable to type '{ mode: string; }'.ts(2345)
|
|
||||||
msg.users.push(match[1]);
|
msg.users.push(match[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import {IrcEventHandler} from "../../client";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
@ -68,6 +69,7 @@ export default <IrcEventHandler>function (irc, network) {
|
||||||
client.emit("join", {
|
client.emit("join", {
|
||||||
network: network.uuid,
|
network: network.uuid,
|
||||||
chan: chan.getFilteredClone(true),
|
chan: chan.getFilteredClone(true),
|
||||||
|
shouldOpen: false,
|
||||||
index: network.addChannel(chan),
|
index: network.addChannel(chan),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import {IrcEventHandler} from "../../client";
|
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) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
@ -28,9 +29,9 @@ export default <IrcEventHandler>function (irc, network) {
|
||||||
});
|
});
|
||||||
|
|
||||||
client.emit("join", {
|
client.emit("join", {
|
||||||
shouldOpen: true,
|
|
||||||
network: network.uuid,
|
network: network.uuid,
|
||||||
chan: chan.getFilteredClone(true),
|
chan: chan.getFilteredClone(true),
|
||||||
|
shouldOpen: true,
|
||||||
index: network.addChannel(chan),
|
index: network.addChannel(chan),
|
||||||
});
|
});
|
||||||
chan.loadMessages(client, network);
|
chan.loadMessages(client, network);
|
||||||
|
|
|
@ -7,8 +7,9 @@ import Config from "../../config";
|
||||||
import Msg, {Message} from "../../models/msg";
|
import Msg, {Message} from "../../models/msg";
|
||||||
import Chan, {Channel} from "../../models/chan";
|
import Chan, {Channel} from "../../models/chan";
|
||||||
import Helper from "../../helper";
|
import Helper from "../../helper";
|
||||||
import type {SearchResponse, SearchQuery, SearchableMessageStorage, DeletionRequest} from "./types";
|
import type {SearchableMessageStorage, DeletionRequest} from "./types";
|
||||||
import Network from "../../models/network";
|
import Network from "../../models/network";
|
||||||
|
import {SearchQuery, SearchResponse} from "../../../shared/types/storage";
|
||||||
|
|
||||||
// TODO; type
|
// TODO; type
|
||||||
let sqlite3: any;
|
let sqlite3: any;
|
||||||
|
|
|
@ -6,8 +6,9 @@ import filenamify from "filenamify";
|
||||||
import Config from "../../config";
|
import Config from "../../config";
|
||||||
import {MessageStorage} from "./types";
|
import {MessageStorage} from "./types";
|
||||||
import Channel from "../../models/chan";
|
import Channel from "../../models/chan";
|
||||||
import {Message, MessageType} from "../../models/msg";
|
import {Message} from "../../models/msg";
|
||||||
import Network from "../../models/network";
|
import Network from "../../models/network";
|
||||||
|
import {MessageType} from "../../../shared/types/msg";
|
||||||
|
|
||||||
class TextFileMessageStorage implements MessageStorage {
|
class TextFileMessageStorage implements MessageStorage {
|
||||||
isEnabled: boolean;
|
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 {Message} from "../../models/message";
|
||||||
import {Network} from "../../models/network";
|
import {Network} from "../../models/network";
|
||||||
import Client from "../../client";
|
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 = {
|
export type DeletionRequest = {
|
||||||
olderThanDays: number;
|
olderThanDays: number;
|
||||||
|
@ -28,17 +29,6 @@ interface MessageStorage {
|
||||||
canProvideMessages(): boolean;
|
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>;
|
type SearchFunction = (query: SearchQuery) => Promise<SearchResponse>;
|
||||||
|
|
||||||
export interface SearchableMessageStorage extends MessageStorage {
|
export interface SearchableMessageStorage extends MessageStorage {
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import {PackageInfo} from "./index";
|
import {PackageInfo} from "./index";
|
||||||
import Client from "../../client";
|
import Client from "../../client";
|
||||||
import Chan from "../../models/chan";
|
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 {
|
export default class PublicClient {
|
||||||
private client: Client;
|
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 {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
|
* @param {Object} data - Body of the event, can be anything, but will need to be properly interpreted by the client
|
||||||
*/
|
*/
|
||||||
sendToBrowser(event: string, data) {
|
// FIXME: this is utterly bonkers
|
||||||
this.client.emit(event, data);
|
// 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,
|
text: text,
|
||||||
from: {
|
from: {
|
||||||
nick: this.packageInfo.name || this.packageInfo.packageName,
|
nick: this.packageInfo.name || this.packageInfo.packageName,
|
||||||
} as UserInMessage,
|
mode: "",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
286
server/server.ts
286
server/server.ts
|
@ -3,7 +3,7 @@ import {Server as wsServer} from "ws";
|
||||||
import express, {NextFunction, Request, Response} from "express";
|
import express, {NextFunction, Request, Response} from "express";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
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 dns from "dns";
|
||||||
import colors from "chalk";
|
import colors from "chalk";
|
||||||
import net from "net";
|
import net from "net";
|
||||||
|
@ -13,25 +13,32 @@ import Client from "./client";
|
||||||
import ClientManager from "./clientManager";
|
import ClientManager from "./clientManager";
|
||||||
import Uploader from "./plugins/uploader";
|
import Uploader from "./plugins/uploader";
|
||||||
import Helper from "./helper";
|
import Helper from "./helper";
|
||||||
import Config, {ConfigType, Defaults} from "./config";
|
import Config, {ConfigType} from "./config";
|
||||||
import Identification from "./identification";
|
import Identification from "./identification";
|
||||||
import changelog from "./plugins/changelog";
|
import changelog from "./plugins/changelog";
|
||||||
import inputs from "./plugins/inputs";
|
import inputs from "./plugins/inputs";
|
||||||
import Auth from "./plugins/auth";
|
import Auth from "./plugins/auth";
|
||||||
|
|
||||||
import themes, {ThemeForClient} from "./plugins/packages/themes";
|
import themes from "./plugins/packages/themes";
|
||||||
themes.loadLocalThemes();
|
themes.loadLocalThemes();
|
||||||
|
|
||||||
import packages from "./plugins/packages/index";
|
import packages from "./plugins/packages/index";
|
||||||
import {NetworkWithIrcFramework} from "./models/network";
|
import {NetworkWithIrcFramework} from "./models/network";
|
||||||
import {ChanType} from "./models/chan";
|
|
||||||
import Utils from "./command-line/utils";
|
import Utils from "./command-line/utils";
|
||||||
import type {
|
import type {
|
||||||
ClientToServerEvents,
|
ClientToServerEvents,
|
||||||
ServerToClientEvents,
|
ServerToClientEvents,
|
||||||
InterServerEvents,
|
InterServerEvents,
|
||||||
SocketData,
|
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 = {
|
type ServerOptions = {
|
||||||
dev: boolean;
|
dev: boolean;
|
||||||
|
@ -45,21 +52,13 @@ type IndexTemplateConfiguration = ServerConfiguration & {
|
||||||
cacheBust: string;
|
cacheBust: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ClientConfiguration = Pick<
|
type Socket = ioSocket<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>;
|
||||||
ConfigType,
|
export type Server = ioServer<
|
||||||
"public" | "lockNetwork" | "useHexIp" | "prefetch" | "defaults"
|
ClientToServerEvents,
|
||||||
> & {
|
ServerToClientEvents,
|
||||||
fileUpload: boolean;
|
InterServerEvents,
|
||||||
ldapEnabled: boolean;
|
SocketData
|
||||||
isUpdateAvailable: boolean;
|
>;
|
||||||
applicationServerKey: string;
|
|
||||||
version: string;
|
|
||||||
gitCommit: string | null;
|
|
||||||
defaultTheme: string;
|
|
||||||
themes: ThemeForClient[];
|
|
||||||
defaults: Defaults;
|
|
||||||
fileUploadMaxFileSize?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
// A random number that will force clients to reload the page if it differs
|
// A random number that will force clients to reload the page if it differs
|
||||||
const serverHash = Math.floor(Date.now() * Math.random());
|
const serverHash = Math.floor(Date.now() * Math.random());
|
||||||
|
@ -219,12 +218,7 @@ export default async function (
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sockets = new Server<
|
const sockets: Server = new ioServer(server, {
|
||||||
ClientToServerEvents,
|
|
||||||
ServerToClientEvents,
|
|
||||||
InterServerEvents,
|
|
||||||
SocketData
|
|
||||||
>(server, {
|
|
||||||
wsEngine: wsServer,
|
wsEngine: wsServer,
|
||||||
cookie: false,
|
cookie: false,
|
||||||
serveClient: false,
|
serveClient: false,
|
||||||
|
@ -330,7 +324,7 @@ export default async function (
|
||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getClientLanguage(socket: Socket): string | null {
|
function getClientLanguage(socket: Socket): string | undefined {
|
||||||
const acceptLanguage = socket.handshake.headers["accept-language"];
|
const acceptLanguage = socket.handshake.headers["accept-language"];
|
||||||
|
|
||||||
if (typeof acceptLanguage === "string" && /^[\x00-\x7F]{1,50}$/.test(acceptLanguage)) {
|
if (typeof acceptLanguage === "string" && /^[\x00-\x7F]{1,50}$/.test(acceptLanguage)) {
|
||||||
|
@ -338,10 +332,10 @@ function getClientLanguage(socket: Socket): string | null {
|
||||||
return acceptLanguage;
|
return acceptLanguage;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getClientIp(socket: Socket) {
|
function getClientIp(socket: Socket): string {
|
||||||
let ip = socket.handshake.address || "127.0.0.1";
|
let ip = socket.handshake.address || "127.0.0.1";
|
||||||
|
|
||||||
if (Config.values.reverseProxy) {
|
if (Config.values.reverseProxy) {
|
||||||
|
@ -367,12 +361,12 @@ function getClientSecure(socket: Socket) {
|
||||||
return secure;
|
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");
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
function addSecurityHeaders(req: Request, res: Response, next: NextFunction) {
|
function addSecurityHeaders(_req: Request, res: Response, next: NextFunction) {
|
||||||
const policies = [
|
const policies = [
|
||||||
"default-src 'none'", // default to nothing
|
"default-src 'none'", // default to nothing
|
||||||
"base-uri 'none'", // disallow <base>, has no fallback to default-src
|
"base-uri 'none'", // disallow <base>, has no fallback to default-src
|
||||||
|
@ -402,32 +396,30 @@ function addSecurityHeaders(req: Request, res: Response, next: NextFunction) {
|
||||||
return next();
|
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,
|
// Intermittent proxies must not cache the following requests,
|
||||||
// browsers must fetch the latest version of these files (service worker, source maps)
|
// browsers must fetch the latest version of these files (service worker, source maps)
|
||||||
res.setHeader("Cache-Control", "no-cache, no-transform");
|
res.setHeader("Cache-Control", "no-cache, no-transform");
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
function indexRequest(req: Request, res: Response) {
|
function indexRequest(_req: Request, res: Response) {
|
||||||
res.setHeader("Content-Type", "text/html");
|
res.setHeader("Content-Type", "text/html");
|
||||||
|
|
||||||
return fs.readFile(
|
fs.readFile(Utils.getFileFromRelativeToRoot("client/index.html.tpl"), "utf-8", (err, file) => {
|
||||||
Utils.getFileFromRelativeToRoot("client/index.html.tpl"),
|
if (err) {
|
||||||
"utf-8",
|
log.error(`failed to server index request: ${err.name}, ${err.message}`);
|
||||||
(err, file) => {
|
res.sendStatus(500);
|
||||||
if (err) {
|
return;
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
const config: IndexTemplateConfiguration = {
|
|
||||||
...getServerConfiguration(),
|
|
||||||
...{cacheBust: Helper.getVersionCacheBust()},
|
|
||||||
};
|
|
||||||
|
|
||||||
res.send(_.template(file)(config));
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
const config: IndexTemplateConfiguration = {
|
||||||
|
...getServerConfiguration(),
|
||||||
|
...{cacheBust: Helper.getVersionCacheBust()},
|
||||||
|
};
|
||||||
|
|
||||||
|
res.send(_.template(file)(config));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeClient(
|
function initializeClient(
|
||||||
|
@ -552,18 +544,10 @@ function initializeClient(
|
||||||
const hash = Helper.password.hash(p1);
|
const hash = Helper.password.hash(p1);
|
||||||
|
|
||||||
client.setPassword(hash, (success: boolean) => {
|
client.setPassword(hash, (success: boolean) => {
|
||||||
const obj = {success: false, error: undefined} as {
|
socket.emit("change-password", {
|
||||||
success: boolean;
|
success: success,
|
||||||
error: string | undefined;
|
error: success ? undefined : "update_failed",
|
||||||
};
|
});
|
||||||
|
|
||||||
if (success) {
|
|
||||||
obj.success = true;
|
|
||||||
} else {
|
|
||||||
obj.error = "update_failed";
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.emit("change-password", obj);
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((error: Error) => {
|
.catch((error: Error) => {
|
||||||
|
@ -577,10 +561,28 @@ function initializeClient(
|
||||||
client.open(socket.id, data);
|
client.open(socket.id, data);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("sort", (data) => {
|
socket.on("sort:networks", (data) => {
|
||||||
if (_.isPlainObject(data)) {
|
if (!_.isPlainObject(data)) {
|
||||||
client.sort(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) => {
|
socket.on("names", (data) => {
|
||||||
|
@ -630,13 +632,13 @@ function initializeClient(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = networkAndChan.chan.findMessage(data.msgId);
|
const message = data.msgId ? networkAndChan.chan.findMessage(data.msgId) : null;
|
||||||
|
|
||||||
if (!message) {
|
if (!message) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const preview = message.findPreview(data.link);
|
const preview = data.link ? message.findPreview(data.link) : null;
|
||||||
|
|
||||||
if (preview) {
|
if (preview) {
|
||||||
preview.shown = newState;
|
preview.shown = newState;
|
||||||
|
@ -828,9 +830,9 @@ function initializeClient(
|
||||||
});
|
});
|
||||||
|
|
||||||
// socket.join is a promise depending on the adapter.
|
// 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", {
|
socket.emit("init", {
|
||||||
active: openChannel,
|
active: openChannel,
|
||||||
networks: client.networks.map((network) =>
|
networks: client.networks.map((network) =>
|
||||||
|
@ -842,7 +844,7 @@ function initializeClient(
|
||||||
};
|
};
|
||||||
|
|
||||||
if (Config.values.public) {
|
if (Config.values.public) {
|
||||||
sendInitEvent(null);
|
sendInitEvent();
|
||||||
} else if (!token) {
|
} else if (!token) {
|
||||||
client.generateToken((newToken) => {
|
client.generateToken((newToken) => {
|
||||||
token = client.calculateTokenHash(newToken);
|
token = client.calculateTokenHash(newToken);
|
||||||
|
@ -853,73 +855,108 @@ function initializeClient(
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
client.updateSession(token, getClientIp(socket), socket.request);
|
client.updateSession(token, getClientIp(socket), socket.request);
|
||||||
sendInitEvent(null);
|
sendInitEvent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getClientConfiguration(): ClientConfiguration {
|
function getClientConfiguration(): SharedConfiguration | LockedSharedConfiguration {
|
||||||
const config = _.pick(Config.values, [
|
const common = {
|
||||||
"public",
|
fileUpload: Config.values.fileUpload.enable,
|
||||||
"lockNetwork",
|
ldapEnabled: Config.values.ldap.enable,
|
||||||
"useHexIp",
|
isUpdateAvailable: changelog.isUpdateAvailable,
|
||||||
"prefetch",
|
applicationServerKey: manager!.webPush.vapidKeys!.publicKey,
|
||||||
]) as ClientConfiguration;
|
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;
|
const defaultsOverride = {
|
||||||
config.ldapEnabled = Config.values.ldap.enable;
|
nick: Config.getDefaultNick(), // expand the number part
|
||||||
|
|
||||||
if (!config.lockNetwork) {
|
// TODO: this doesn't seem right, if the client needs this as a buffer
|
||||||
config.defaults = _.clone(Config.values.defaults);
|
// the client ought to add it on its own
|
||||||
} else {
|
sasl: "",
|
||||||
// Only send defaults that are visible on the client
|
saslAccount: "",
|
||||||
config.defaults = _.pick(Config.values.defaults, [
|
saslPassword: "",
|
||||||
"name",
|
};
|
||||||
"nick",
|
|
||||||
"username",
|
if (!Config.values.lockNetwork) {
|
||||||
"password",
|
const defaults: ConfigNetDefaults = {
|
||||||
"realname",
|
..._.clone(Config.values.defaults),
|
||||||
"join",
|
...defaultsOverride,
|
||||||
]) as Defaults;
|
};
|
||||||
|
const result: SharedConfiguration = {
|
||||||
|
...common,
|
||||||
|
defaults: defaults,
|
||||||
|
lockNetwork: Config.values.lockNetwork,
|
||||||
|
};
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
config.isUpdateAvailable = changelog.isUpdateAvailable;
|
// Only send defaults that are visible on the client
|
||||||
config.applicationServerKey = manager!.webPush.vapidKeys!.publicKey;
|
const defaults: LockedConfigNetDefaults = {
|
||||||
config.version = Helper.getVersionNumber();
|
..._.pick(Config.values.defaults, ["name", "username", "password", "realname", "join"]),
|
||||||
config.gitCommit = Helper.getGitCommit();
|
...defaultsOverride,
|
||||||
config.themes = themes.getAll();
|
};
|
||||||
config.defaultTheme = Config.values.theme;
|
|
||||||
config.defaults.nick = Config.getDefaultNick();
|
|
||||||
config.defaults.sasl = "";
|
|
||||||
config.defaults.saslAccount = "";
|
|
||||||
config.defaults.saslPassword = "";
|
|
||||||
|
|
||||||
if (Uploader) {
|
const result: LockedSharedConfiguration = {
|
||||||
config.fileUploadMaxFileSize = Uploader.getMaxFileSize();
|
...common,
|
||||||
}
|
lockNetwork: Config.values.lockNetwork,
|
||||||
|
defaults: defaults,
|
||||||
|
};
|
||||||
|
|
||||||
return config;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getServerConfiguration(): ServerConfiguration {
|
function getServerConfiguration(): ServerConfiguration {
|
||||||
return {...Config.values, ...{stylesheets: packages.getStylesheets()}};
|
return {...Config.values, ...{stylesheets: packages.getStylesheets()}};
|
||||||
}
|
}
|
||||||
|
|
||||||
function performAuthentication(this: Socket, data) {
|
function performAuthentication(this: Socket, data: AuthPerformData) {
|
||||||
if (!_.isPlainObject(data)) {
|
if (!_.isPlainObject(data)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const socket = this;
|
const socket = this;
|
||||||
let client;
|
let client: Client | undefined;
|
||||||
let token: string;
|
let token: string;
|
||||||
|
|
||||||
const finalInit = () =>
|
const finalInit = () => {
|
||||||
initializeClient(socket, client, token, data.lastMessage || -1, data.openChannel);
|
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 = () => {
|
const initClient = () => {
|
||||||
|
if (!client) {
|
||||||
|
throw new Error("initClient called with undefined client");
|
||||||
|
}
|
||||||
|
|
||||||
// Configuration does not change during runtime of TL,
|
// Configuration does not change during runtime of TL,
|
||||||
// and the client listens to this event only once
|
// 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("configuration", getClientConfiguration());
|
||||||
|
|
||||||
socket.emit(
|
socket.emit(
|
||||||
|
@ -928,8 +965,10 @@ function performAuthentication(this: Socket, data) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clientIP = getClientIp(socket);
|
||||||
|
|
||||||
client.config.browser = {
|
client.config.browser = {
|
||||||
ip: getClientIp(socket),
|
ip: clientIP,
|
||||||
isSecure: getClientSecure(socket),
|
isSecure: getClientSecure(socket),
|
||||||
language: getClientLanguage(socket),
|
language: getClientLanguage(socket),
|
||||||
};
|
};
|
||||||
|
@ -939,8 +978,9 @@ function performAuthentication(this: Socket, data) {
|
||||||
return finalInit();
|
return finalInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
reverseDnsLookup(client.config.browser?.ip, (hostname) => {
|
const cb_client = client; // ensure that TS figures out that client can't be nil
|
||||||
client.config.browser!.hostname = hostname;
|
reverseDnsLookup(clientIP, (hostname) => {
|
||||||
|
cb_client.config.browser!.hostname = hostname;
|
||||||
|
|
||||||
finalInit();
|
finalInit();
|
||||||
});
|
});
|
||||||
|
@ -951,9 +991,10 @@ function performAuthentication(this: Socket, data) {
|
||||||
client.connect();
|
client.connect();
|
||||||
manager!.clients.push(client);
|
manager!.clients.push(client);
|
||||||
|
|
||||||
|
const cb_client = client; // ensure TS can see we never have a nil client
|
||||||
socket.on("disconnect", function () {
|
socket.on("disconnect", function () {
|
||||||
manager!.clients = _.without(manager!.clients, client);
|
manager!.clients = _.without(manager!.clients, cb_client);
|
||||||
client.quit();
|
cb_client.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
initClient();
|
initClient();
|
||||||
|
@ -965,7 +1006,7 @@ function performAuthentication(this: Socket, data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const authCallback = (success) => {
|
const authCallback = (success: boolean) => {
|
||||||
// Authorization failed
|
// Authorization failed
|
||||||
if (!success) {
|
if (!success) {
|
||||||
if (!client) {
|
if (!client) {
|
||||||
|
@ -990,6 +1031,10 @@ function performAuthentication(this: Socket, data) {
|
||||||
// load it and find the user again (this happens with LDAP)
|
// load it and find the user again (this happens with LDAP)
|
||||||
if (!client) {
|
if (!client) {
|
||||||
client = manager!.loadUser(data.user);
|
client = manager!.loadUser(data.user);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
throw new Error(`authCallback: ${data.user} not found after second lookup`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initClient();
|
initClient();
|
||||||
|
@ -998,16 +1043,23 @@ function performAuthentication(this: Socket, data) {
|
||||||
client = manager!.findClient(data.user);
|
client = manager!.findClient(data.user);
|
||||||
|
|
||||||
// We have found an existing user and client has provided a token
|
// 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);
|
const providedToken = client.calculateTokenHash(data.token);
|
||||||
|
|
||||||
if (Object.prototype.hasOwnProperty.call(client.config.sessions, providedToken)) {
|
if (Object.prototype.hasOwnProperty.call(client.config.sessions, providedToken)) {
|
||||||
token = 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(() => {
|
Auth.initialize().then(() => {
|
||||||
// Perform password checking
|
// Perform password checking
|
||||||
Auth.auth(manager, client, data.user, data.password, authCallback);
|
Auth.auth(manager, client, data.user, data.password, authCallback);
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import SqliteMessageStorage from "./plugins/messageStorage/sqlite";
|
import SqliteMessageStorage from "./plugins/messageStorage/sqlite";
|
||||||
import {MessageType} from "./models/msg";
|
|
||||||
import Config from "./config";
|
import Config from "./config";
|
||||||
import {DeletionRequest} from "./plugins/messageStorage/types";
|
import {DeletionRequest} from "./plugins/messageStorage/types";
|
||||||
import log from "./log";
|
import log from "./log";
|
||||||
|
import {MessageType} from "../shared/types/msg";
|
||||||
|
|
||||||
const status_types = [
|
const status_types = [
|
||||||
MessageType.AWAY,
|
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. */,
|
"extends": "../tsconfig.base.json" /* Path to base configuration file to inherit from. Requires TypeScript version 2.1 or later. */,
|
||||||
"include": [
|
"include": [
|
||||||
"**/*",
|
".",
|
||||||
"../shared/"
|
"../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. */,
|
] /* 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": [
|
"files": [
|
||||||
|
|
1
server/types/index.d.ts
vendored
1
server/types/index.d.ts
vendored
|
@ -1,2 +1 @@
|
||||||
import "./modules";
|
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;
|
reply: (message: string) => void;
|
||||||
tags: {[key: string]: string};
|
tags: {[key: string]: string};
|
||||||
target: string;
|
target: string;
|
||||||
time?: any;
|
time?: number;
|
||||||
type: "privmsg" | "action" | "notice" | "wallops";
|
|
||||||
}
|
}
|
||||||
export interface JoinEventArgs {
|
export interface JoinEventArgs {
|
||||||
account: boolean;
|
account: boolean;
|
||||||
|
@ -117,7 +116,11 @@ declare module "irc-framework" {
|
||||||
isEnabled: (cap: string) => boolean;
|
isEnabled: (cap: string) => boolean;
|
||||||
enabled: string[];
|
enabled: string[];
|
||||||
};
|
};
|
||||||
extractTargetGroup: (target: string) => any;
|
extractTargetGroup: (target: string) => {
|
||||||
|
target: string;
|
||||||
|
target_group: string;
|
||||||
|
};
|
||||||
|
|
||||||
supports(feature: "MODES"): string;
|
supports(feature: "MODES"): string;
|
||||||
supports(feature: string): boolean;
|
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 LinkifyIt, {Match} from "linkify-it";
|
||||||
import tlds from "tlds";
|
import tlds from "tlds";
|
||||||
|
|
||||||
export type NoSchemaMatch = Match & {
|
|
||||||
noschema: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type LinkPart = {
|
export type LinkPart = {
|
||||||
start: number;
|
start: number;
|
||||||
end: number;
|
end: number;
|
||||||
link: string;
|
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);
|
const linkify = LinkifyIt().tlds(tlds).tlds("onion", true);
|
||||||
|
|
||||||
// Known schemes to detect in text
|
// Known schemes to detect in text
|
||||||
|
@ -73,12 +49,25 @@ linkify.add("web+", {
|
||||||
},
|
},
|
||||||
normalize(match) {
|
normalize(match) {
|
||||||
match.schema = match.text.slice(0, match.text.indexOf(":") + 1);
|
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) {
|
export function findLinks(text: string) {
|
||||||
const matches = linkify.match(text) as NoSchemaMatch[];
|
const matches = linkify.match(text);
|
||||||
|
|
||||||
if (!matches) {
|
if (!matches) {
|
||||||
return [];
|
return [];
|
||||||
|
@ -88,16 +77,16 @@ export function findLinks(text: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findLinksWithSchema(text: string) {
|
export function findLinksWithSchema(text: string) {
|
||||||
const matches = linkify.match(text) as NoSchemaMatch[];
|
const matches = linkify.match(text);
|
||||||
|
|
||||||
if (!matches) {
|
if (!matches) {
|
||||||
return [];
|
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 {
|
return {
|
||||||
start: url.index,
|
start: url.index,
|
||||||
end: url.lastIndex,
|
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