├── .eslintignore
├── .eslintrc.json
├── .github
└── workflows
│ ├── containerize.yaml
│ ├── linting.yml
│ └── tests.yaml
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── CHANGELOG.md
├── Dockerfile
├── LICENSE.md
├── README.md
├── build-scripts
├── .eslintrc.json
├── build-client.js
├── do-client-build.js
├── vite.config.js
└── write-version-files.js
├── client
├── css
│ ├── room-directory.css
│ └── styles.css
├── img
│ ├── external-link-icon.svg
│ ├── favicon.ico
│ ├── favicon.svg
│ ├── matrix-lines-hero-sprite.svg
│ └── opengraph.png
└── js
│ ├── .eslintrc.json
│ ├── entry-client-hydrogen.js
│ ├── entry-client-room-alias-hash-redirect.js
│ ├── entry-client-room-directory.js
│ └── entry-client-timeout.js
├── config
├── config.default.json
└── config.test.json
├── docker-health-check.js
├── docs
├── faq.md
├── testing.md
└── tracing.md
├── package-lock.json
├── package.json
├── server
├── child-process-runner
│ ├── child-fork-script.js
│ └── run-in-child-process.js
├── hydrogen-render
│ ├── render-hydrogen-to-string-unsafe.js
│ ├── render-hydrogen-to-string.js
│ ├── render-hydrogen-vm-render-script-to-page-html.js
│ └── render-page-html.js
├── lib
│ ├── config.js
│ ├── errors
│ │ ├── extended-error.js
│ │ ├── rethrown-error.js
│ │ ├── route-timeout-abort-error.js
│ │ ├── status-error.js
│ │ └── user-closed-connection-abort-error.js
│ ├── express-async-handler.js
│ ├── fetch-endpoint.js
│ ├── get-asset-url.js
│ ├── get-dependencies-for-entry-point-name.js
│ ├── get-version-tags.js
│ ├── matrix-utils
│ │ ├── ensure-room-joined.js
│ │ ├── fetch-accessible-rooms.js
│ │ ├── fetch-events-from-timestamp-backwards.js
│ │ ├── fetch-room-data.js
│ │ ├── get-messages-response-from-event-id.js
│ │ ├── get-server-name-from-matrix-room-id-or-alias.js
│ │ └── timestamp-to-event.js
│ ├── parse-via-servers-from-user-input.js
│ ├── safe-json.js
│ ├── sanitize-html.js
│ ├── set-headers-for-date-temporal-context.js
│ └── set-headers-to-preload-assets.js
├── middleware
│ ├── content-security-policy-middleware.js
│ ├── identify-route-middleware.js
│ ├── prevent-clickjacking-middleware.js
│ ├── redirect-to-correct-room-url-if-bad-sigil-middleware.js
│ └── timeout-middleware.js
├── routes
│ ├── client-side-room-alias-hash-redirect-route.js
│ ├── install-routes.js
│ ├── room-directory-routes.js
│ └── room-routes.js
├── server.js
├── start-dev.js
└── tracing
│ ├── capture-span-processor.js
│ ├── serialize-span.js
│ ├── trace-utilities.js
│ ├── tracing-middleware.js
│ └── tracing.js
├── shared
├── .eslintrc.json
├── hydrogen-vm-render-script.js
├── lib
│ ├── assert.js
│ ├── check-text-for-nsfw.js
│ ├── custom-tile-utilities.js
│ ├── local-storage-keys.js
│ ├── matrix-viewer-history.js
│ ├── mxc-url-to-http.js
│ ├── redirect-if-room-alias-in-hash.js
│ ├── reference-values.js
│ ├── stub-powerlevels-observable.js
│ ├── supress-blank-anchors-reloading-the-page.js
│ ├── timestamp-utilities.js
│ └── url-creator.js
├── room-directory-vm-render-script.js
├── viewmodels
│ ├── AvatarViewModel.js
│ ├── CalendarViewModel.js
│ ├── DeveloperOptionsContentViewModel.js
│ ├── HomeserverSelectionModalContentViewModel.js
│ ├── JumpToNextActivitySummaryTileViewModel.js
│ ├── JumpToPreviousActivitySummaryTileViewModel.js
│ ├── ModalViewModel.js
│ ├── RoomCardViewModel.js
│ ├── RoomDirectoryViewModel.js
│ ├── RoomViewModel.js
│ └── TimeSelectorViewModel.js
└── views
│ ├── CalendarView.js
│ ├── DeveloperOptionsContentView.js
│ ├── HomeserverSelectionModalContentView.js
│ ├── JumpToNextActivitySummaryTileView.js
│ ├── JumpToPreviousActivitySummaryTileView.js
│ ├── MatrixLogoView.js
│ ├── ModalView.js
│ ├── RightPanelContentView.js
│ ├── RoomCardView.js
│ ├── RoomDirectoryView.js
│ ├── RoomView.js
│ └── TimeSelectorView.js
└── test
├── .eslintrc.json
├── docker-compose.yml
├── dockerfiles
├── Synapse.Dockerfile
├── keys
│ ├── README.md
│ ├── ca.crt
│ └── ca.key
└── synapse
│ ├── as_registration.yaml
│ ├── homeserver.yaml
│ ├── log_config.yaml
│ └── start.sh
├── e2e-tests.js
├── fixtures
└── friction_between_surfaces.jpg
├── server
└── matrix-utils
│ └── get-server-name-from-matrix-room-id-or-alias-tests.js
├── shared
└── lib
│ ├── check-text-for-nsfw-tests.js
│ └── timestamp-utilties-tests.js
└── test-utils
├── client-utils.js
├── parse-matrix-viewer-url-for-room.js
├── parse-room-day-message-structure.js
└── test-error.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "es6": true,
4 | "commonjs": true,
5 | "node": true
6 | },
7 | "parserOptions": {
8 | "ecmaVersion": 2022,
9 | "sourceType": "script"
10 | },
11 | "extends": ["eslint:recommended", "prettier", "plugin:n/recommended"],
12 | "rules": {
13 | "indent": "off",
14 | "comma-dangle": "off",
15 | "quotes": "off",
16 | "eqeqeq": ["warn", "allow-null"],
17 | "strict": ["error", "safe"],
18 | "no-unused-vars": ["error", { "destructuredArrayIgnorePattern": "^_" }],
19 | "no-extra-boolean-cast": ["warn"],
20 | "complexity": [
21 | "error",
22 | {
23 | "max": 12
24 | }
25 | ],
26 | "max-statements-per-line": [
27 | "error",
28 | {
29 | "max": 3
30 | }
31 | ],
32 | "no-debugger": "error",
33 | "no-dupe-keys": "error",
34 | "no-unsafe-finally": "error",
35 | "no-with": "error",
36 | "no-useless-call": "error",
37 | "no-spaced-func": "error",
38 | "no-useless-escape": "warn",
39 | "max-statements": ["warn", 30],
40 | "max-depth": ["error", 4],
41 | "no-throw-literal": ["error"],
42 | "no-sequences": "error",
43 | "radix": "error",
44 | "yoda": "error",
45 | "no-nested-ternary": "warn",
46 | "no-whitespace-before-property": "error",
47 | "no-trailing-spaces": ["error"],
48 | "space-in-parens": ["warn", "never"],
49 | "max-nested-callbacks": ["error", 6],
50 | "eol-last": "warn",
51 | "no-mixed-spaces-and-tabs": "error",
52 | "no-negated-condition": "warn",
53 | "no-unneeded-ternary": "error",
54 | "no-use-before-define": ["warn", { "variables": true, "functions": true, "classes": true }],
55 | "no-undef": "error",
56 | "no-param-reassign": "warn",
57 | "no-multi-spaces": [
58 | "warn",
59 | {
60 | "exceptions": {
61 | "Property": true
62 | }
63 | }
64 | ],
65 | "key-spacing": [
66 | "warn",
67 | {
68 | "singleLine": {
69 | "beforeColon": false,
70 | "afterColon": true
71 | },
72 | "multiLine": {
73 | "beforeColon": false,
74 | "afterColon": true,
75 | "mode": "minimum"
76 | }
77 | }
78 | ],
79 | "n/no-process-exit": "off",
80 | "n/no-unsupported-features/es-syntax": "error",
81 | "n/no-unsupported-features/es-builtins": "error"
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/.github/workflows/containerize.yaml:
--------------------------------------------------------------------------------
1 | name: Containerize
2 |
3 | on:
4 | push:
5 | branches: [main]
6 |
7 | env:
8 | REGISTRY: ghcr.io
9 | GHCR_NAMESPACE: matrix-org/matrix-viewer
10 | IMAGE_NAME: matrix-viewer
11 |
12 | jobs:
13 | # Create and publish a Docker image for matrix-viewer
14 | #
15 | # Based off of
16 | # https://docs.github.com/en/actions/publishing-packages/publishing-docker-images#publishing-images-to-github-packages
17 | build-image:
18 | runs-on: ubuntu-latest
19 | permissions:
20 | contents: read
21 | packages: write
22 |
23 | outputs:
24 | # Make the Docker image name available to use in other jobs as `${{
25 | # needs.build-image.outputs.docker_image_name }}`. Also see
26 | # the `save_var` step below for how this works.
27 | docker_image_name: ${{ steps.save_var.outputs.docker_image_name }}
28 |
29 | steps:
30 | - name: Checkout repository
31 | uses: actions/checkout@v3
32 |
33 | - name: Log in to the GitHub Container registry
34 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
35 | with:
36 | registry: ${{ env.REGISTRY }}
37 | username: ${{ github.actor }}
38 | password: ${{ secrets.GITHUB_TOKEN }}
39 |
40 | # Trick to get env variables available in `needs` context which is
41 | # available to use in almost everywhere
42 | # (https://docs.github.com/en/actions/learn-github-actions/contexts). This
43 | # is so that we can reference the same Docker image name in the `services` of
44 | # the next job.
45 | #
46 | # via https://github.community/t/how-to-use-env-with-container-image/17252/25
47 | - name: Save the Docker image name to a variable so we can share and re-use it in other jobs via `${{ needs.build-image.outputs.docker_image_name }}`
48 | id: save_var
49 | run: echo "::set-output name=docker_image_name::${{ env.REGISTRY }}/${{ env.GHCR_NAMESPACE }}/${{ env.IMAGE_NAME }}"
50 |
51 | - name: Extract metadata (tags, labels) for Docker
52 | id: meta
53 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
54 | with:
55 | images: ${{ steps.save_var.outputs.docker_image_name }}
56 | # Defaults (as indicated by https://github.com/docker/metadata-action/tree/97c170d70b5f8bfc77e6b23e68381f217cb64ded#tags-input).
57 | # Plus custom tags:
58 | # - Full length sha
59 | tags: |
60 | type=schedule
61 | type=ref,event=branch
62 | type=ref,event=tag
63 | type=ref,event=pr
64 | type=sha,format=long
65 |
66 | - name: Build and push Docker image
67 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
68 | with:
69 | push: true
70 | context: '.'
71 | file: 'Dockerfile'
72 | tags: ${{ steps.meta.outputs.tags }}
73 | labels: ${{ steps.meta.outputs.labels }}
74 | build-args: |
75 | GITHUB_SHA=${{ github.sha }}
76 | GITHUB_REF=${{ github.ref_name }}
77 |
78 | # Just make sure the container can start-up and responds to the health check
79 | test-image:
80 | needs: [build-image]
81 | runs-on: ubuntu-latest
82 |
83 | services:
84 | matrix-viewer:
85 | image: ${{ needs.build-image.outputs.docker_image_name }}:sha-${{ github.sha }}
86 | credentials:
87 | username: ${{ github.actor }}
88 | password: ${{ secrets.GITHUB_TOKEN }}
89 | ports:
90 | - 3050:3050
91 | env:
92 | matrixServerUrl: http://FAKE_SERVER/
93 | matrixAccessToken: FAKE_TOKEN
94 |
95 | steps:
96 | - name: See if the container will respond to a request
97 | run: curl http://localhost:3050/health-check
98 |
--------------------------------------------------------------------------------
/.github/workflows/linting.yml:
--------------------------------------------------------------------------------
1 | name: Linting
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | lint-eslint:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v3
14 | - name: Use Node.js ${{ matrix.node-version }}
15 | uses: actions/setup-node@v3
16 | with:
17 | node-version: ${{ matrix.node-version }}
18 | cache: 'npm'
19 |
20 | - name: Install dependencies
21 | run: npm ci
22 |
23 | - name: Run ESLint
24 | run: npm run eslint -- "**/*.js"
25 |
26 | lint-prettier:
27 | runs-on: ubuntu-latest
28 | steps:
29 | - uses: actions/checkout@v3
30 | - name: Use Node.js ${{ matrix.node-version }}
31 | uses: actions/setup-node@v3
32 | with:
33 | node-version: ${{ matrix.node-version }}
34 | cache: 'npm'
35 |
36 | - name: Install dependencies
37 | run: npm ci
38 |
39 | - name: Run Prettier
40 | run: npm run prettier -- --check "**/*.{js,css,md}"
41 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yaml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | push:
5 | branches: [main]
6 |
7 | env:
8 | REGISTRY: ghcr.io
9 | GHCR_NAMESPACE: matrix-org/matrix-viewer
10 | IMAGE_NAME: matrix-viewer-test-homeserver
11 |
12 | jobs:
13 | # Create and publish a Docker image for a Synapse test instance that can
14 | # federate with each other.
15 | #
16 | # Based off of
17 | # https://docs.github.com/en/actions/publishing-packages/publishing-docker-images#publishing-images-to-github-packages
18 | build-test-synapse-image:
19 | runs-on: ubuntu-latest
20 | permissions:
21 | contents: read
22 | packages: write
23 |
24 | outputs:
25 | # Make the Docker image name available to use in other jobs as `${{
26 | # needs.build-test-synapse-image.outputs.docker_image_name }}`. Also see
27 | # the `save_var` step below for how this works.
28 | docker_image_name: ${{ steps.save_var.outputs.docker_image_name }}
29 |
30 | steps:
31 | - name: Checkout repository
32 | uses: actions/checkout@v3
33 |
34 | - name: Log in to the GitHub Container registry
35 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
36 | with:
37 | registry: ${{ env.REGISTRY }}
38 | username: ${{ github.actor }}
39 | password: ${{ secrets.GITHUB_TOKEN }}
40 |
41 | # Trick to get env variables available in `needs` context which is
42 | # available to use in almost everywhere
43 | # (https://docs.github.com/en/actions/learn-github-actions/contexts). This
44 | # is so that we can reference the same Docker image name in the `services` of
45 | # the next job.
46 | #
47 | # via https://github.community/t/how-to-use-env-with-container-image/17252/25
48 | - name: Save the Docker image name to a variable so we can share and re-use it in other jobs via `${{ needs.build-test-synapse-image.outputs.docker_image_name }}`
49 | id: save_var
50 | run: echo "::set-output name=docker_image_name::${{ env.REGISTRY }}/${{ env.GHCR_NAMESPACE }}/${{ env.IMAGE_NAME }}"
51 |
52 | - name: Extract metadata (tags, labels) for Docker
53 | id: meta
54 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
55 | with:
56 | images: ${{ steps.save_var.outputs.docker_image_name }}
57 | # Defaults (as indicated by https://github.com/docker/metadata-action/tree/97c170d70b5f8bfc77e6b23e68381f217cb64ded#tags-input).
58 | # Plus custom tags:
59 | # - Full length sha
60 | tags: |
61 | type=schedule
62 | type=ref,event=branch
63 | type=ref,event=tag
64 | type=ref,event=pr
65 | type=sha,format=long
66 |
67 | - name: Build and push Docker image
68 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
69 | with:
70 | push: true
71 | context: test/dockerfiles/
72 | file: 'test/dockerfiles/Synapse.Dockerfile'
73 | tags: ${{ steps.meta.outputs.tags }}
74 | labels: ${{ steps.meta.outputs.labels }}
75 |
76 | tests:
77 | needs: [build-test-synapse-image]
78 | runs-on: ubuntu-latest
79 |
80 | strategy:
81 | matrix:
82 | node-version: [18.x]
83 |
84 | services:
85 | # We need two homeservers that federate with each other to test with
86 | hs1:
87 | image: ${{ needs.build-test-synapse-image.outputs.docker_image_name }}:sha-${{ github.sha }}
88 | credentials:
89 | username: ${{ github.actor }}
90 | password: ${{ secrets.GITHUB_TOKEN }}
91 | ports:
92 | - 11008:8008
93 | env:
94 | SERVER_NAME: hs1
95 | hs2:
96 | image: ${{ needs.build-test-synapse-image.outputs.docker_image_name }}:sha-${{ github.sha }}
97 | credentials:
98 | username: ${{ github.actor }}
99 | password: ${{ secrets.GITHUB_TOKEN }}
100 | ports:
101 | - 12008:8008
102 | env:
103 | SERVER_NAME: hs2
104 |
105 | steps:
106 | - uses: actions/checkout@v3
107 | - name: Use Node.js ${{ matrix.node-version }}
108 | uses: actions/setup-node@v3
109 | with:
110 | node-version: ${{ matrix.node-version }}
111 | cache: 'npm'
112 |
113 | - name: Install dependencies
114 | run: npm ci
115 |
116 | - name: Run build
117 | run: npm run build
118 |
119 | - name: Test!
120 | run: npm test
121 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | public
5 | *.local
6 |
7 | config.user-overrides.json
8 | config/config.dev.user-overrides.json
9 | config/config.beta.user-overrides.json
10 | config/config.prod.user-overrides.json
11 | config.json
12 | secrets.json
13 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "singleQuote": true
4 | }
5 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 0.2.0 - _upcoming_ - Matrix Viewer
2 |
3 | - Prevent Cloudflare from overriding our own 504 timeout page, https://github.com/matrix-org/matrix-viewer/pull/228
4 | - Catch NSFW rooms with underscores, https://github.com/matrix-org/matrix-viewer/pull/231
5 | - Fix `18+` false positives with NSFW check, https://github.com/matrix-org/matrix-viewer/pull/279
6 | - Fix room cards sorting in the wrong direction on Firefox, https://github.com/matrix-org/matrix-viewer/pull/261
7 | - Remove `libera.chat` as a default since their rooms are not accessible, https://github.com/matrix-org/matrix-viewer/pull/263
8 | - Add reason why the bot is joining the room, https://github.com/matrix-org/matrix-viewer/pull/262
9 | - Add `/faq` redirect, https://github.com/matrix-org/matrix-viewer/pull/265
10 | - Use `rel=canonical` link to de-duplicate event permalinks, https://github.com/matrix-org/matrix-viewer/pull/266, https://github.com/matrix-org/matrix-viewer/pull/269
11 | - Prevent join event spam with stable `reason`, https://github.com/matrix-org/matrix-viewer/pull/268
12 | - Don't allow previewing `shared` history rooms, https://github.com/matrix-org/matrix-viewer/pull/239
13 | - Contributed by [@tulir](https://github.com/tulir)
14 | - Update FAQ to explain `world_readable` only, https://github.com/matrix-org/matrix-viewer/pull/277
15 | - Indicate when the room was set to `world_readable` and by who, https://github.com/matrix-org/matrix-viewer/pull/278
16 | - Only show `world_readable` rooms in the room directory, https://github.com/matrix-org/matrix-viewer/pull/276
17 |
18 | Developer facing:
19 |
20 | - Fix eslint trying to look at `node_modules/`, https://github.com/matrix-org/matrix-viewer/pull/275
21 |
22 | # 0.1.0 - 2023-05-11
23 |
24 | - Initial public release with good enough functionality to be generally available including: room directory homepage, room archive view with calendar jump-to-date, drill-down with the time selector, following room upgrades (tombstone/predecessor), and more. Completed milestone: https://github.com/matrix-org/matrix-viewer/milestone/1
25 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # XXX: Before updating this, make sure the issues around `npm` silently exiting
2 | # with error 243 issues are solved:
3 | # - https://github.com/npm/cli/issues/4996
4 | # - https://github.com/npm/cli/issues/4769
5 | FROM node:18.16.0-buster-slim
6 |
7 | # Pass through some GitHub CI variables which we use in the build (for version
8 | # files/tags)
9 | ARG GITHUB_SHA
10 | ENV GITHUB_SHA=$GITHUB_SHA
11 | ARG GITHUB_REF
12 | ENV GITHUB_REF=$GITHUB_REF
13 |
14 | RUN mkdir -p /app
15 |
16 | WORKDIR /app
17 |
18 | # Copy the health-check script
19 | COPY docker-health-check.js /app/
20 |
21 | # Copy just what we need to install the dependencies so this layer can be cached
22 | # in the Docker build
23 | COPY package.json package-lock.json /app/
24 | RUN npm install
25 |
26 | # Copy what we need for the client-side build
27 | COPY config /app/config/
28 | COPY build-scripts /app/build-scripts/
29 | COPY client /app/client/
30 | COPY shared /app/shared/
31 | # Also copy the server stuff (we reference the config from the `build-client.js`)
32 | COPY server /app/server/
33 | # Build the client-side bundle
34 | RUN npm run build
35 |
36 | HEALTHCHECK CMD node docker-health-check.js
37 |
38 | ENTRYPOINT ["/bin/bash", "-c", "npm start"]
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Matrix Viewer
2 |
3 |
4 |
5 | > **Note**
6 | > The Matrix Public Archive has been renamed to Matrix Viewer to better reflect what it
7 | > actually does and doesn't do. It's a viewer for world-readable Matrix rooms and
8 | > doesn't actually archive anything.
9 |
10 | In the vein of [feature parity with
11 | Gitter](https://github.com/vector-im/roadmap/issues/26), the goal is to make an
12 | accessible public site for `world_readable` Matrix rooms like Gitter's archives
13 | which search engines can index and keep all of the content accessible/available.
14 |
15 |
16 | Room directory homepage | Room view
17 | --- | ---
18 |
| 
19 |
20 | ## Demo videos
21 |
22 | The demo's refer to this project as the "Matrix Public Archive" which has now been renamed to "Matrix Viewer".
23 |
24 | - [ May 2023](https://www.youtube.com/watch?v=4KlNILNItGQ&t=1046s): Introducing [archive.matrix.org](https://archive.matrix.org/), the shiny new public instance of the Matrix Public Archive that everyone can share and link to.
25 | - [ Aug 2022](https://www.youtube.com/watch?v=6KHQSeJTXm0&t=583s) ([blog post](https://matrix.org/blog/2022/08/05/this-week-in-matrix-2022-08-05#matrix-public-archive-website)): A quick intro of what the project looks like, the goals, what it accomplishes, and how it's a new portal into the Matrix ecosystem.
26 | - [ Oct 2022](https://www.youtube.com/watch?v=UT6KSEqDUf8&t=548s): Showing off the room directory landing page used to browse everything available in the archive.
27 |
28 | ## Technical overview
29 |
30 | We server-side render (SSR) the [Hydrogen](https://github.com/vector-im/hydrogen-web)
31 | Matrix client on a Node.js server (since both use JavaScript) and serve pages on the fly
32 | (with some Cloudflare caching on top) when someone requests
33 | `/r/matrixhq:matrix.org/${year}/${month}/${day}`. To fetch the events for a
34 | given day/time, we use [MSC3030](https://github.com/matrix-org/matrix-doc/pull/3030)'s
35 | `/timestamp_to_event` endpoint to jump to a given day in the timeline and fetch the
36 | messages from a Matrix homeserver.
37 |
38 | Re-using Hydrogen gets us pretty and native(to Element) looking styles and keeps
39 | the maintenance burden of supporting more event types in Hydrogen.
40 |
41 | ## FAQ
42 |
43 | See the [FAQ page](docs/faq.md).
44 |
45 | ## Setup
46 |
47 | ### Prerequisites
48 |
49 | - [Node.js](https://nodejs.org/) v18
50 | - We need v18 because it includes `fetch` by default. And [`node-fetch` doesn't
51 | support `abortSignal.reason`](https://github.com/node-fetch/node-fetch/issues/1462)
52 | yet.
53 | - We need v16 because it includes
54 | [`require('crypto').webcrypto.subtle`](https://nodejs.org/docs/latest-v16.x/api/webcrypto.html#cryptosubtle)
55 | for [Matrix encryption (olm) which can't be disabled in
56 | Hydrogen](https://github.com/vector-im/hydrogen-web/issues/579) yet. And
57 | [`abortSignal.reason` was introduced in
58 | v16.14.0](https://nodejs.org/dist/latest-v18.x/docs/api/globals.html#abortsignalreason) (although we use `node-fetch` for now).
59 | - A Matrix homeserver that supports [MSC3030's](https://github.com/matrix-org/matrix-spec-proposals/pull/3030) `/timestamp_to_event` endpoint
60 | - [Synapse](https://matrix.org/docs/projects/server/synapse) 1.73.0+
61 |
62 | ### Get the app running
63 |
64 | ```sh
65 | $ npm install
66 | $ npm run build
67 |
68 | # Edit `config/config.user-overrides.json` so that `matrixServerUrl` points to
69 | # your homeserver and has `matrixAccessToken` defined
70 | $ cp config/config.default.json config/config.user-overrides.json
71 |
72 | $ npm run start
73 | ```
74 |
75 | ## Development
76 |
77 | ```sh
78 | # Clone and install the `matrix-viewer` project
79 | $ git clone git@github.com:matrix-org/matrix-viewer.git
80 | $ cd matrix-viewer
81 | $ npm install
82 |
83 | # Edit `config/config.user-overrides.json` so that `matrixServerUrl` points to
84 | # your homeserver and has `matrixAccessToken` defined
85 | $ cp config/config.default.json config/config.user-overrides.json
86 |
87 | # This will watch for changes, rebuild bundles and restart the server
88 | $ npm run start-dev
89 | ```
90 |
91 | If you want to make changes to the underlying Hydrogen SDK as well, you can locally link
92 | it into this project with the following instructions:
93 |
94 | ```sh
95 | # We need to use a draft branch of Hydrogen to get the custom changes needed for
96 | # `matrix-viewer` to run. Hopefully soon, we can get all of the custom
97 | # changes mainlined so this isn't necessary.
98 | $ git clone git@github.com:vector-im/hydrogen-web.git
99 | $ cd hydrogen-web
100 | $ git checkout madlittlemods/matrix-public-archive-scratch-changes
101 | $ yarn install
102 | $ yarn build:sdk
103 | $ cd target/ && npm link && cd ..
104 | $ cd ..
105 |
106 | $ cd matrix-viewer
107 | $ npm link hydrogen-view-sdk
108 | ```
109 |
110 | ### Running tests
111 |
112 | See the [testing documentation](./docs/testing.md).
113 |
114 | ### Tracing
115 |
116 | See the [tracing documentation](./docs/tracing.md).
117 |
--------------------------------------------------------------------------------
/build-scripts/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "n/no-unpublished-require": "off"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/build-scripts/build-client.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const vite = require('vite');
4 | const mergeOptions = require('merge-options');
5 |
6 | // Require the config before the Vite config so `process.env.NODE_ENV` is set
7 | require('../server/lib/config');
8 |
9 | const writeVersionFiles = require('./write-version-files');
10 | const viteConfig = require('./vite.config');
11 |
12 | async function buildClient(extraConfig = {}) {
13 | await writeVersionFiles();
14 |
15 | const resultantViteConfig = mergeOptions(viteConfig, extraConfig);
16 | await vite.build(resultantViteConfig);
17 | }
18 |
19 | module.exports = buildClient;
20 |
--------------------------------------------------------------------------------
/build-scripts/do-client-build.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // This is just a callable from the commandline version of `build-client.js`. So
4 | // that we can run the build from an npm script.
5 |
6 | const buildClient = require('./build-client');
7 |
8 | buildClient();
9 |
--------------------------------------------------------------------------------
/build-scripts/vite.config.js:
--------------------------------------------------------------------------------
1 | // vite.config.js
2 | 'use strict';
3 |
4 | const path = require('path');
5 | const {
6 | defineConfig, //splitVendorChunkPlugin
7 | } = require('vite');
8 |
9 | module.exports = defineConfig({
10 | // We have to specify this otherwise Vite will override NODE_ENV as
11 | // `production` when we start the server and watch build in our `start-dev.js`.
12 | mode: process.env.NODE_ENV || 'dev',
13 |
14 | plugins: [
15 | // Alternatively, we can manually configure chunks via
16 | // `build.rollupOptions.output.manualChunks`.
17 | // Docs: https://vitejs.dev/guide/build.html#chunking-strategy
18 | //
19 | // This didn't seem to work for me though, so I've done the manual config way.
20 | // splitVendorChunkPlugin(),
21 | ],
22 |
23 | optimizeDeps: {
24 | include: [
25 | // This doesn't seem to be necessary for the this package to work (ref
26 | // https://vitejs.dev/guide/dep-pre-bundling.html#monorepos-and-linked-dependencies)
27 | //
28 | //'matrix-viewer-shared'
29 | ],
30 | },
31 | resolve: {
32 | alias: {
33 | // The `file:` packages don't seem resolve correctly so let's add an alias as well
34 | // See https://css-tricks.com/adding-vite-to-your-existing-web-app/#aa-aliases
35 | 'matrix-viewer-shared': path.resolve(__dirname, '../shared'),
36 | },
37 | // This will make sure Vite/Rollup matches the original file path (i.e. the path
38 | // without following symlinks) instead of the real file path (i.e. the path after
39 | // following symlinks). This is useful when symlinking `hydrogen-view-sdk`, so it
40 | // still matches our `/node_modules/` pattern in the `build.commonjsOptions.include`
41 | // config below and gets converted from CommonJS to ESM as expected.
42 | preserveSymlinks: true,
43 | },
44 | build: {
45 | outDir: './dist',
46 | rollupOptions: {
47 | // Overwrite default `index.html` entry
48 | // (https://vitejs.dev/guide/backend-integration.html#backend-integration)
49 | input: [
50 | path.resolve(__dirname, '../client/js/entry-client-hydrogen.js'),
51 | path.resolve(__dirname, '../client/js/entry-client-room-directory.js'),
52 | path.resolve(__dirname, '../client/js/entry-client-room-alias-hash-redirect.js'),
53 | path.resolve(__dirname, '../client/js/entry-client-timeout.js'),
54 | ],
55 | output: {
56 | assetFileNames: (chunkInfo) => {
57 | const { name } = path.parse(chunkInfo.name);
58 | // Some of the Hydrogen assets already have hashes in the name so let's remove
59 | // that in favor of our new hash.
60 | const nameWithoutHash = name.replace(/-[a-z0-9]+$/, '');
61 |
62 | return `assets/${nameWithoutHash}-[hash][extname]`;
63 | },
64 | },
65 | },
66 |
67 | // We want to know how the transformed source relates back to the original source
68 | // for easier debugging
69 | sourcemap: true,
70 |
71 | // Generate `dist/manifest.json` that we can use to map a given file to it's built
72 | // hashed file name and any dependencies it has.
73 | manifest: true,
74 | // We don't want to use the `ssrManifest` option. It's supposedly "for determining
75 | // style links and asset preload directives in production"
76 | // (https://vitejs.dev/config/build-options.html#build-ssrmanifest) (also see
77 | // https://vitejs.dev/guide/ssr.html#generating-preload-directives) but doesn't seem
78 | // very useful or what we want.
79 | //
80 | // ssrManifest: true,
81 |
82 | // Copy things like the version files from `public/` to `dist/`. Things in `public/`
83 | // are copied as-is with no transformations.
84 | copyPublicDir: true,
85 |
86 | commonjsOptions: {
87 | include: [
88 | // Fix `Error: 'default' is not exported by ...` when importing CommonJS files, see
89 | // https://github.com/vitejs/vite/issues/2679 and docs:
90 | // https://vitejs.dev/guide/dep-pre-bundling.html#monorepos-and-linked-dependencies
91 | /shared\//,
92 |
93 | // Make all of our `require()` CommonJS calls compatible in the ESM client build.
94 | // See https://vitejs.dev/guide/dep-pre-bundling.html#monorepos-and-linked-dependencies
95 | /node_modules/,
96 | ],
97 | },
98 | },
99 | });
100 |
--------------------------------------------------------------------------------
/build-scripts/write-version-files.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const { mkdir, writeFile } = require('fs').promises;
5 | const util = require('util');
6 | const exec = util.promisify(require('child_process').exec);
7 |
8 | async function mkdirp(path) {
9 | try {
10 | await mkdir(path, { recursive: true });
11 | } catch (err) {
12 | console.log('mkdirp err', err);
13 | // no-op, the directory is already created
14 | }
15 | }
16 |
17 | async function writeVersionFiles() {
18 | let commit;
19 | let branch;
20 | try {
21 | commit = (await exec(`git rev-parse HEAD`)).stdout;
22 | branch = (await exec(`git rev-parse --abbrev-ref HEAD`)).stdout;
23 | } catch (err) {
24 | console.log(
25 | `Failed to use \`git\` to find the commit and branch.` +
26 | ` Falling back to using environment variables assuming we're running in GitHub CI. The error encountered:`,
27 | err
28 | );
29 |
30 | // Pull these values from environment variables provided by GitHub CI
31 | commit = process.env.GITHUB_SHA;
32 | branch = process.env.GITHUB_REF;
33 | }
34 |
35 | if (!commit || !branch) {
36 | throw new Error(
37 | `Unable to get a suitable commit=${commit} or branch=${branch} while writing version files`
38 | );
39 | }
40 |
41 | await mkdirp(path.join(__dirname, '../public/'));
42 | await writeFile(path.join(__dirname, '../public/GIT_COMMIT'), commit);
43 | await writeFile(path.join(__dirname, '../public/VERSION'), branch);
44 | await writeFile(path.join(__dirname, '../public/VERSION_DATE'), new Date().toISOString());
45 | }
46 |
47 | module.exports = writeVersionFiles;
48 |
--------------------------------------------------------------------------------
/client/img/external-link-icon.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/client/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matrix-org/matrix-viewer/5c448f24c252ebc4068824f1e1e47cefe7527b5f/client/img/favicon.ico
--------------------------------------------------------------------------------
/client/img/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
17 |
--------------------------------------------------------------------------------
/client/img/opengraph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matrix-org/matrix-viewer/5c448f24c252ebc4068824f1e1e47cefe7527b5f/client/img/opengraph.png
--------------------------------------------------------------------------------
/client/js/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "commonjs": false,
5 | "node": false
6 | },
7 | "parserOptions": {
8 | "ecmaVersion": 2018,
9 | "sourceType": "module"
10 | },
11 | "rules": {
12 | "n/no-unsupported-features/es-syntax": "off",
13 | "n/no-unsupported-features/es-builtins": "off"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/client/js/entry-client-hydrogen.js:
--------------------------------------------------------------------------------
1 | import 'matrix-viewer-shared/hydrogen-vm-render-script.js';
2 |
3 | // Assets
4 | import 'hydrogen-view-sdk/assets/theme-element-light.css';
5 | import '../css/styles.css';
6 |
--------------------------------------------------------------------------------
/client/js/entry-client-room-alias-hash-redirect.js:
--------------------------------------------------------------------------------
1 | import assert from 'matrix-viewer-shared/lib/assert.js';
2 | import MatrixViewerURLCreator from 'matrix-viewer-shared/lib/url-creator.js';
3 | import redirectIfRoomAliasInHash from 'matrix-viewer-shared/lib/redirect-if-room-alias-in-hash.js';
4 |
5 | // Assets
6 | import 'hydrogen-view-sdk/assets/theme-element-light.css';
7 | import '../css/styles.css';
8 |
9 | const config = window.matrixViewerContext.config;
10 | assert(config);
11 | assert(config.basePath);
12 |
13 | const matrixViewerURLCreator = new MatrixViewerURLCreator(config.basePath);
14 |
15 | console.log(`Trying to redirect based on pageHash=${window.location.hash}`);
16 | const isRedirecting = redirectIfRoomAliasInHash(matrixViewerURLCreator);
17 |
18 | // Show the message while we're trying to redirect or if we found nothing, remove the
19 | // message
20 | document.querySelector('.js-try-redirect-message').style.display = isRedirecting
21 | ? 'inline'
22 | : 'none';
23 |
--------------------------------------------------------------------------------
/client/js/entry-client-room-directory.js:
--------------------------------------------------------------------------------
1 | import 'matrix-viewer-shared/room-directory-vm-render-script.js';
2 |
3 | // Assets
4 | import 'hydrogen-view-sdk/assets/theme-element-light.css';
5 | import '../css/styles.css';
6 | import '../css/room-directory.css';
7 | // Just need to reference the favicon in one of the entry points for it to be copied
8 | // over for all
9 | import '../img/favicon.ico';
10 | import '../img/favicon.svg';
11 | import '../img/opengraph.png';
12 |
--------------------------------------------------------------------------------
/client/js/entry-client-timeout.js:
--------------------------------------------------------------------------------
1 | // Assets
2 | import 'hydrogen-view-sdk/assets/theme-element-light.css';
3 | import '../css/styles.css';
4 |
--------------------------------------------------------------------------------
/config/config.default.json:
--------------------------------------------------------------------------------
1 | {
2 | "basePort": "3050",
3 | "basePath": "http://localhost:3050",
4 | // Requires homeserver with MSC3030 `/timestamp_to_event` endpoint (Synapse 1.73.0+)
5 | // (see readme for more details)
6 | "matrixServerUrl": "http://localhost:8008/",
7 | "matrixServerName": "localhost",
8 | // Set this to 100 since that is the max that Synapse will backfill even if you do a
9 | // `/messages?limit=1000` and we don't want to miss messages in between.
10 | "messageLimit": 100,
11 | "requestTimeoutMs": 25000,
12 | "logOutputFromChildProcesses": false,
13 | //"stopSearchEngineIndexing": true,
14 | "workaroundCloudflare504TimeoutErrors": false,
15 | // Tracing
16 | //"jaegerTracesEndpoint": "http://localhost:14268/api/traces",
17 |
18 | // Testing
19 | "testMatrixServerUrl1": "http://localhost:11008/",
20 | "testMatrixServerUrl2": "http://localhost:12008/"
21 |
22 | // Secrets
23 | //"matrixAccessToken": "xxx"
24 | }
25 |
--------------------------------------------------------------------------------
/config/config.test.json:
--------------------------------------------------------------------------------
1 | {
2 | "basePort": "3051",
3 | "basePath": "http://localhost:3051",
4 | "matrixServerUrl": "http://localhost:11008/",
5 | "matrixServerName": "hs1",
6 | "testMatrixServerUrl1": "http://localhost:11008/",
7 | "testMatrixServerUrl2": "http://localhost:12008/",
8 |
9 | // Secrets
10 | "matrixAccessToken": "as_token_matrix_viewer_foobarbaz"
11 | }
12 |
--------------------------------------------------------------------------------
/docker-health-check.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const assert = require('assert');
4 |
5 | const { fetchEndpointAsJson } = require('./server/lib/fetch-endpoint');
6 |
7 | const config = require('./server/lib/config');
8 | const basePort = config.get('basePort');
9 | assert(basePort);
10 |
11 | const healthCheckUrl = `http://localhost:${basePort}/health-check`;
12 |
13 | (async () => {
14 | try {
15 | await fetchEndpointAsJson(healthCheckUrl);
16 | process.exit(0);
17 | } catch (err) {
18 | // eslint-disable-next-line no-console
19 | console.log(`Health check error: ${healthCheckUrl}`, err);
20 | process.exit(1);
21 | }
22 | })();
23 |
--------------------------------------------------------------------------------
/docs/faq.md:
--------------------------------------------------------------------------------
1 | # FAQ
2 |
3 | ## Can I run my own instance?
4 |
5 | Yes! We host a public canonical version of the Matrix Viewer at
6 | _(incoming)_ that everyone can use but feel free to
7 | also run your own instance (setup instructions in the [readme](../README.md)).
8 |
9 | ## How is this different from the `matrix-static` project?
10 |
11 | [Matrix Static](https://github.com/matrix-org/matrix-static) already existed
12 | before the Matrix Viewer but there was some desire to make something with more
13 | Element-feeling polish and loading faster (avoid the slow 502's errors that are frequent
14 | on `view.matrix.org`).
15 |
16 | And with the introduction of the jump to date API via
17 | [MSC3030](https://github.com/matrix-org/matrix-spec-proposals/pull/3030), we could show
18 | messages from any given date and day-by-day navigation.
19 |
20 | The Matrix Viewer project has since replaced the `matrix-static` project on
21 | [`view.matrix.org`](https://view.matrix.org/).
22 |
23 | ## Why did the bot join my room?
24 |
25 | Only Matrix rooms with `world_readable` [history
26 | visibility](https://spec.matrix.org/latest/client-server-api/#room-history-visibility)
27 | are accessible in the Matrix Viewer and indexed by search engines.
28 |
29 | But the bot (`@view:matrix.org`) will join any public room because it doesn't
30 | know the history visibility without first joining. Any room that doesn't have
31 | `world_readable` history visibility will lead a `403 Forbidden`.
32 |
33 | The Matrix Viewer hold onto any data (it's
34 | stateless) and requests the messages from the homeserver every time. The
35 | [view.matrix.org](https://view.matrix.org/) instance has some caching in place, 5
36 | minutes for the current day, and 2 days for past content.
37 |
38 | See the [opt out
39 | section](#how-do-i-opt-out-and-keep-my-room-from-being-indexed-by-search-engines) below
40 | for more details.
41 |
42 | ## How do I opt out and keep my room from being indexed by search engines?
43 |
44 | Only Matrix rooms with `world_readable` [history
45 | visibility](https://spec.matrix.org/latest/client-server-api/#room-history-visibility)
46 | are accessible in the Matrix Viewer and indexed by search engines. One easy way
47 | to opt-out is to change your rooms history visibility to something else if you don't
48 | intend for your room be world readable.
49 |
50 | Dedicated opt-out controls are being tracked in
51 | [#47](https://github.com/matrix-org/matrix-viewer/issues/47).
52 |
53 | As a workaround for [view.matrix.org](https://view.matrix.org/), you can ban the
54 | `@view:matrix.org` user if you don't want your room content to be shown at all.
55 |
56 | ### Why does the bot user join rooms instead peeking in the room or using guests?
57 |
58 | Since Matrix Viewer only displays rooms with `world_readable` history visibility, we could
59 | peek into the rooms without joining. This is being explored in
60 | [#272](https://github.com/matrix-org/matrix-viewer/pull/272). But peeking
61 | doesn't work when the server doesn't know about the room already (this is commonly
62 | referred to as federated peeking) which is why we have to fallback to joining the room
63 | in any case. We could solve the federated peeking problem and avoid the join with
64 | [MSC3266 room summaries](https://github.com/matrix-org/matrix-spec-proposals/pull/3266)
65 | to check whether the room is `world_readable` even over federation.
66 |
67 | Guests are completely separate concept and controlled by the `m.room.guest_access` state
68 | event in the room. Guest access is also a much different ask than read-only access since
69 | guests can also send messages in the room which isn't always desirable. The bot
70 | is read-only and does not send messages.
71 |
72 | ## Technical details
73 |
74 | The main readme has a [technical overview](../README.md#technical-overview) of the
75 | project. Here are a few more details.
76 |
77 | ### How do I figure out what version of the Matrix Viewer is running?
78 |
79 | Just visit the `/health-check` endpoint which will return information like the following:
80 |
81 | ```
82 | {
83 | "ok": true,
84 | "commit": "954b22995a44bf11bfcd5850b62e206e46ee2db9",
85 | "version": "main",
86 | "versionDate": "2023-04-05T09:26:12.524Z",
87 | "packageVersion": "0.0.0"
88 | }
89 | ```
90 |
91 | ### How does the room URL relate to what is displayed on the page?
92 |
93 | We start the end of the date/time specified in the URL looking backward up to the limit.
94 |
95 | ### Why does the time selector only appear for some pages?
96 |
97 | The time selector only appears for pages that have a lot of messages on a given
98 | day/hour/minute/second (more than the configured `messageLimit`).
99 |
--------------------------------------------------------------------------------
/docs/testing.md:
--------------------------------------------------------------------------------
1 | # Testing
2 |
3 | ## Setup
4 |
5 | If you haven't setup `matrix-viewer` yet, see the [_Setup_ section in the root `README.md`](../README.md#setup)
6 |
7 | Then we need to setup the federation cluster of homeservers that we will test with.
8 | Sorry, this isn't automated yet when you run the tests 🙇
9 |
10 | ```sh
11 | # Build the test homeserver image that are pre-configured to federate with each other
12 | $ docker pull matrixdotorg/synapse:latest
13 | $ docker build -t matrix-viewer-test-homeserver -f test/dockerfiles/Synapse.Dockerfile test/dockerfiles/
14 |
15 | # Start the test homeservers
16 | $ docker-compose --project-name matrix_viewer_test -f test/docker-compose.yml up -d --no-recreate
17 | ```
18 |
19 | ## Running the tests
20 |
21 | ```sh
22 | $ npm run test
23 | ```
24 |
25 | Or if you want to keep the Matrix Viewer server running after the tests run and
26 | explore the UI from the interactive URL's printed on the screen to better debug, use:
27 |
28 | ```sh
29 | $ npm run test-interactive
30 | ```
31 |
32 | Caveat: You might not see the same result that a test is seeing when visiting the
33 | interactive URL. Some tests set config like the `messageLimit` which is reset
34 | after each test case unless you are using `npm run test-interactive` and visiting the
35 | interactive URL for a failed test. Otherwise, we reset config between each test case so
36 | they don't leak and contaminate each other.
37 |
38 | ### Developer utility
39 |
40 | Some copy-pasta to help you manage the Docker containers for the test homeservers:
41 |
42 | ```sh
43 | $ docker ps --all | grep test_hs
44 | $ docker logs -f --tail 10 matrix_viewer_test_hs1_1
45 | $ docker logs -f --tail 10 matrix_viewer_test_hs2_1
46 |
47 | $ docker stop matrix_viewer_test_hs1_1 matrix_viewer_test_hs2_1
48 | $ docker rm matrix_viewer_test_hs1_1 matrix_viewer_test_hs2_1
49 | ```
50 |
--------------------------------------------------------------------------------
/docs/tracing.md:
--------------------------------------------------------------------------------
1 | # Tracing
2 |
3 | Tracing allows you to see the flow of a request through the system and where time is
4 | taken up in functions. This is useful for debugging and performance analysis.
5 |
6 |
7 |
8 | ## Setup
9 |
10 | 1. Get the all-in-one Jaeger Docker container running (via https://www.jaegertracing.io/docs/1.35/getting-started/)
11 | ```
12 | docker run -d --name jaeger \
13 | -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \
14 | -e COLLECTOR_OTLP_ENABLED=true \
15 | -p 6831:6831/udp \
16 | -p 6832:6832/udp \
17 | -p 5778:5778 \
18 | -p 5775:5775/udp \
19 | -p 16686:16686 \
20 | -p 4317:4317 \
21 | -p 4318:4318 \
22 | -p 14250:14250 \
23 | -p 14268:14268 \
24 | -p 14269:14269 \
25 | -p 9411:9411 \
26 | jaegertracing/all-in-one:1.35
27 | ```
28 | 1. Add `jaegerTracesEndpoint` to your `config.json`:
29 | ```json5
30 | {
31 | // ...
32 | jaegerTracesEndpoint: 'http://localhost:14268/api/traces',
33 | }
34 | ```
35 |
36 | ## Run the app with the OpenTelemetry tracing enabled
37 |
38 | ```
39 | npm run start -- --tracing
40 | # or
41 | npm run start-dev -- --tracing
42 | ```
43 |
44 | Manually:
45 |
46 | ```
47 | node --require './server/tracing.js' server/server.js
48 | ```
49 |
50 | ## Viewing traces in Jaeger
51 |
52 | Once you have the all-in-one Jaeger Docker container running, just visit
53 | http://localhost:16686 to see a dashboard of the collected traces and dive in.
54 |
55 | Traces are made up of many spans. Each span defines a `traceId` which it is associated with.
56 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "matrix-viewer",
3 | "version": "0.1.0",
4 | "license": "Apache-2.0",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/matrix-org/matrix-viewer"
8 | },
9 | "scripts": {
10 | "lint": "eslint \"**/*.js\"",
11 | "build": "node ./build-scripts/do-client-build.js",
12 | "start": "node server/server.js",
13 | "start-dev": "node server/start-dev.js",
14 | "test": "npm run mocha -- test/**/*-tests.js --timeout 15000",
15 | "test-e2e-interactive": "npm run mocha -- test/e2e-tests.js --timeout 15000 --bail --interactive",
16 | "nodemon": "nodemon",
17 | "gulp": "gulp",
18 | "vite": "vite",
19 | "mocha": "mocha",
20 | "eslint": "eslint",
21 | "prettier": "prettier"
22 | },
23 | "engines": {
24 | "node": ">=16.0.0"
25 | },
26 | "devDependencies": {
27 | "chalk": "^4.1.2",
28 | "eslint": "^8.37.0",
29 | "eslint-config-prettier": "^8.8.0",
30 | "eslint-plugin-n": "^15.7.0",
31 | "eslint-plugin-prettier": "^4.2.1",
32 | "merge-options": "^3.0.4",
33 | "mocha": "^9.2.1",
34 | "nodemon": "^2.0.15",
35 | "prettier": "^2.8.7",
36 | "vite": "^4.3.9"
37 | },
38 | "dependencies": {
39 | "@opentelemetry/api": "^1.4.1",
40 | "@opentelemetry/auto-instrumentations-node": "^0.36.6",
41 | "@opentelemetry/context-async-hooks": "^1.12.0",
42 | "@opentelemetry/core": "^1.12.0",
43 | "@opentelemetry/exporter-jaeger": "^1.12.0",
44 | "@opentelemetry/instrumentation": "^0.38.0",
45 | "@opentelemetry/propagator-ot-trace": "^0.26.2",
46 | "@opentelemetry/resources": "^1.12.0",
47 | "@opentelemetry/sdk-trace-base": "^1.12.0",
48 | "@opentelemetry/semantic-conventions": "^1.12.0",
49 | "cors": "^2.8.5",
50 | "dompurify": "^2.3.9",
51 | "escape-string-regexp": "^4.0.0",
52 | "express": "^4.17.2",
53 | "hydrogen-view-sdk": "npm:@mlm/hydrogen-view-sdk@^0.28.0-scratch",
54 | "json5": "^2.2.1",
55 | "linkedom": "^0.14.17",
56 | "matrix-viewer-shared": "file:./shared/",
57 | "nconf": "^0.11.3",
58 | "opentelemetry-instrumentation-fetch-node": "^1.0.0",
59 | "url-join": "^4.0.1"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/server/child-process-runner/child-fork-script.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Called by `child_process` `fork` in `run-in-child-process.js` so we can
4 | // get the data and exit the process cleanly.
5 |
6 | const assert = require('assert');
7 |
8 | const RethrownError = require('../lib/errors/rethrown-error');
9 |
10 | // Serialize the error and send it back up to the parent process so we can
11 | // interact with it and know what happened when the process exits.
12 | async function serializeError(err) {
13 | await new Promise((resolve) => {
14 | process.send(
15 | {
16 | error: true,
17 | name: err.name,
18 | message: err.message,
19 | stack: err.stack,
20 | },
21 | (sendErr) => {
22 | if (sendErr) {
23 | // We just log here instead of rejecting because it's more important
24 | // to see the original error we are trying to send up. Let's just
25 | // throw the original error below.
26 | const sendErrWithDescription = new RethrownError(
27 | 'Failed to send error to the parent process',
28 | sendErr
29 | );
30 | console.error(sendErrWithDescription);
31 | // This will end up hitting the `unhandledRejection` handler and
32 | // serializing this error instead (worth a shot) 🤷♀️
33 | throw sendErrWithDescription;
34 | }
35 |
36 | resolve();
37 | }
38 | );
39 | });
40 | }
41 |
42 | // We don't exit the process after encountering one of these because maybe it
43 | // doesn't matter to the main-line process in the module.
44 | //
45 | // If we don't listen for these events, the child will exit with status code 1
46 | // (error) when they occur.
47 | process.on('uncaughtException', async (err /*, origin*/) => {
48 | await serializeError(new RethrownError('uncaughtException in child process', err));
49 | });
50 |
51 | process.on('unhandledRejection', async (reason /*, promise*/) => {
52 | await serializeError(new RethrownError('unhandledRejection in child process', reason));
53 | });
54 |
55 | // Only kick everything off once we receive the options. We pass in the options
56 | // this way instead of argv because we will run into `Error: spawn E2BIG` and
57 | // `Error: spawn ENAMETOOLONG` with argv.
58 | process.on('message', async (runArguments) => {
59 | try {
60 | assert(runArguments);
61 |
62 | // Require the module that we're supposed to run
63 | const modulePath = process.argv[2];
64 | assert(
65 | modulePath,
66 | 'Expected `modulePath` to be passed into `child-fork-script.js` via argv[2]'
67 | );
68 | const moduleToRun = require(modulePath);
69 |
70 | // Run the module
71 | const result = await moduleToRun(runArguments);
72 |
73 | assert(result, `No result returned from module we ran (${modulePath}).`);
74 |
75 | // Send back the data we need to the parent.
76 | await new Promise((resolve, reject) => {
77 | process.send(
78 | {
79 | data: result,
80 | },
81 | (err) => {
82 | if (err) {
83 | return reject(err);
84 | }
85 |
86 | // Exit once we know the data was sent out. We can't gurantee the
87 | // message was received but this should work pretty well.
88 | //
89 | // Related:
90 | // - https://stackoverflow.com/questions/34627546/process-send-is-sync-async-on-nix-windows
91 | // - https://github.com/nodejs/node/commit/56d9584a0ead78874ca9d4de2e55b41c4056e502
92 | // - https://github.com/nodejs/node/issues/6767
93 | process.exit(0);
94 | resolve();
95 | }
96 | );
97 | });
98 | } catch (err) {
99 | // We need to wait for the error to completely send to the parent
100 | // process before we exit the process.
101 | await serializeError(err);
102 |
103 | // Fail the process and exit
104 | process.exit(1);
105 | }
106 | });
107 |
--------------------------------------------------------------------------------
/server/hydrogen-render/render-hydrogen-to-string-unsafe.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Server-side render Hydrogen to a string using a browser-like context thanks
4 | // to `linkedom`. We use a VM so we can put all of the browser-like globals in
5 | // place.
6 | //
7 | // Note: This is marked as unsafe because the render script is run in a VM which
8 | // doesn't exit after we get the result (Hydrogen keeps running). There isn't a
9 | // way to stop, terminate, or kill a vm script or vm context so in order to be
10 | // safe, we need to run this inside of a child_process which we can kill after.
11 | // This is why we have the `1-render-hydrogen-to-string.js` layer to handle
12 | // this.
13 |
14 | const assert = require('assert');
15 | const vm = require('vm');
16 | const path = require('path');
17 | const { readFile } = require('fs').promises;
18 | const crypto = require('crypto');
19 | const { parseHTML } = require('linkedom');
20 |
21 | // Setup the DOM context with any necessary shims/polyfills and ensure the VM
22 | // context global has everything that a normal document does so Hydrogen can
23 | // render.
24 | function createDomAndSetupVmContext() {
25 | const dom = parseHTML(`
26 |
27 |
28 |