├── .dockerignore
├── .github
├── FUNDING.yml
└── workflows
│ └── docker.yml
├── templates
├── message.handlebars
└── success.handlebars
├── Dockerfile
├── fly.toml
├── index.html
├── package.json
├── LICENSE
├── .gitignore
├── index.js
└── README.md
/.dockerignore:
--------------------------------------------------------------------------------
1 | .git
2 | node_modules
3 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | custom: https://paypal.me/ushakovhq
2 |
--------------------------------------------------------------------------------
/templates/message.handlebars:
--------------------------------------------------------------------------------
1 |
New submission
2 |
3 | {{#each fields}}
4 | {{capitalize @key}}: {{this}}
5 |
6 | {{/each}}
7 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:14
2 | WORKDIR /usr/src/app
3 | COPY package*.json ./
4 |
5 | RUN npm install
6 | COPY . .
7 |
8 | EXPOSE 8080
9 | CMD [ "npm", "start" ]
10 |
--------------------------------------------------------------------------------
/fly.toml:
--------------------------------------------------------------------------------
1 | app = "matrix-forms"
2 |
3 | [env]
4 | PORT = 8080
5 | HOMESERVER = "https://converser.eu"
6 | DEFAULT_ROOM = "!sAYXjSoGjJfOAUHaqp:matrix.org"
7 | CORS = "*"
8 |
9 | [[services]]
10 | internal_port = 8080
11 | protocol = "tcp"
12 |
13 | [[services.ports]]
14 | handlers = ["http"]
15 | port = "80"
16 |
17 | [[services.ports]]
18 | handlers = ["tls", "http"]
19 | port = "443"
20 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "matrix-forms",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "node index.js"
9 | },
10 | "author": "",
11 | "license": "MIT",
12 | "dependencies": {
13 | "cors": "^2.8.5",
14 | "express": "^4.17.1",
15 | "express-formidable-v2": "^2.2.1",
16 | "express-handlebars": "^6.0.1",
17 | "formidable": "^2.0.1",
18 | "matrix-js-sdk": "^15.1.1"
19 | },
20 | "devDependencies": {
21 | "dotenv": "^10.0.0"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/.github/workflows/docker.yml:
--------------------------------------------------------------------------------
1 | name: Docker publish
2 | on: [push]
3 | env:
4 | CONTAINER_IMAGE: ghcr.io/${{github.repository}}:${{github.sha}}
5 | CONTAINER_IMAGE_LATEST: ghcr.io/${{github.repository}}:latest
6 | jobs:
7 | publish:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@master
11 | - name: Build
12 | run: docker build -t $CONTAINER_IMAGE -t $CONTAINER_IMAGE_LATEST .
13 | - name: Login
14 | run: docker login -u $GITHUB_ACTOR -p ${{secrets.GITHUB_TOKEN}} ghcr.io
15 | - name: Publish
16 | run: |
17 | docker push $CONTAINER_IMAGE
18 | docker push $CONTAINER_IMAGE_LATEST
19 |
--------------------------------------------------------------------------------
/templates/success.handlebars:
--------------------------------------------------------------------------------
1 | Submission was successful
2 |
3 | Fields
4 |
5 |
6 |
7 | {{#each @fields}}
8 | | {{capitalize @key}} |
9 | {{/each}}
10 |
11 |
12 | {{#each @fields}}
13 | | {{this}} |
14 | {{/each}}
15 |
16 |
17 |
18 | Files
19 |
20 |
21 | {{#each @files}}
22 |
23 | | File |
24 | Size |
25 | Content URI |
26 |
27 |
28 | | {{this.name}} |
29 | {{this.size}} |
30 | {{this.content_uri}} |
31 |
32 | {{/each}}
33 |
34 |
35 |
36 | ← Return
37 | View in Matrix →
38 |
39 |
48 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2021 Mish Ushakov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Microbundle cache
58 | .rpt2_cache/
59 | .rts2_cache_cjs/
60 | .rts2_cache_es/
61 | .rts2_cache_umd/
62 |
63 | # Optional REPL history
64 | .node_repl_history
65 |
66 | # Output of 'npm pack'
67 | *.tgz
68 |
69 | # Yarn Integrity file
70 | .yarn-integrity
71 |
72 | # dotenv environment variables file
73 | .env
74 | .env.test
75 | .env.production
76 |
77 | # parcel-bundler cache (https://parceljs.org/)
78 | .cache
79 | .parcel-cache
80 |
81 | # Next.js build output
82 | .next
83 | out
84 |
85 | # Nuxt.js build / generate output
86 | .nuxt
87 | dist
88 |
89 | # Gatsby files
90 | .cache/
91 | # Comment in the public line in if your project uses Gatsby and not Next.js
92 | # https://nextjs.org/blog/next-9-1#public-directory-support
93 | # public
94 |
95 | # vuepress build output
96 | .vuepress/dist
97 |
98 | # Serverless directories
99 | .serverless/
100 |
101 | # FuseBox cache
102 | .fusebox/
103 |
104 | # DynamoDB Local files
105 | .dynamodb/
106 |
107 | # TernJS port file
108 | .tern-port
109 |
110 | # Stores VSCode versions used for testing VSCode extensions
111 | .vscode-test
112 |
113 | # yarn v2
114 | .yarn/cache
115 | .yarn/unplugged
116 | .yarn/build-state.yml
117 | .yarn/install-state.gz
118 | .pnp.*
119 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config()
2 | const express = require('express')
3 | const formidableMiddleware = require('express-formidable-v2')
4 | const { create } = require('express-handlebars')
5 | const cors = require('cors')
6 | const sdk = require('matrix-js-sdk')
7 | const fs = require('fs')
8 |
9 | const app = express()
10 | app.use(formidableMiddleware())
11 | app.use(cors({origin: process.env.CORS || '*'}))
12 |
13 | const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1)
14 | const hbs = create({
15 | helpers: {capitalize}
16 | })
17 |
18 | app.engine('handlebars', hbs.engine)
19 | app.set('view engine', 'handlebars')
20 | app.set('views', './templates')
21 |
22 | const client = sdk.createClient({
23 | baseUrl: process.env.HOMESERVER,
24 | accessToken: process.env.ACCESS_TOKEN,
25 | })
26 |
27 | app.post('/', async (req, res) => {
28 | const roomId = req.query.to || process.env.DEFAULT_ROOM
29 | if (!req.fields || !req.files) return res.sendStatus(400)
30 |
31 | const files = Object.values(req.files)
32 | const upload_queue = []
33 | files.forEach((file) => upload_queue.push(client.uploadContent(fs.createReadStream(file.path))))
34 |
35 | const upload_result = await Promise.all(upload_queue)
36 | upload_result.forEach((content_uri, index) => files[index].content_uri = JSON.parse(content_uri).content_uri)
37 |
38 | const message = {
39 | body: Object.keys(req.fields).map((field) => `${capitalize(field)}: ${req.fields[field]}\n`).join(""),
40 | formatted_body: await hbs.render('./templates/message.handlebars', {fields: req.fields}),
41 | format: 'org.matrix.custom.html',
42 | msgtype: 'm.text',
43 | 'matrix.forms': {
44 | fields: req.fields,
45 | files: files.map(file => {
46 | return {
47 | name: file.name,
48 | size: file.size,
49 | content_uri: file.content_uri
50 | }
51 | })
52 | }
53 | }
54 |
55 | try {
56 | await client.sendMessage(roomId, message)
57 | files.forEach((file) => {
58 | client.sendMessage(roomId, {
59 | msgtype: 'm.file',
60 | body: file.name,
61 | url: file.content_uri
62 | })
63 | })
64 | } catch (e) {
65 | return res.status(e.httpStatus).send(e.data.error)
66 | }
67 |
68 | if (Boolean(req.query.redirect)) return res.redirect(req.query.return)
69 | await res.render('success', {layout: false, data: {
70 | room: roomId,
71 | fields: req.fields,
72 | files,
73 | return: req.query.return
74 | }})
75 | })
76 |
77 | app.listen(process.env.PORT, () => {
78 | console.log(`Listening at http://localhost:${process.env.PORT}`)
79 | })
80 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Matrix Forms
2 |
3 | Connect your forms to [Matrix](https://matrix.org)
4 |
5 | **Example**
6 |
7 | ```html
8 |
26 | ```
27 |
28 | **In Matrix**
29 |
30 | 
31 |
32 | [Demo](https://mishushakov.github.io/matrix-forms)
33 |
34 | ## Features
35 |
36 | - Server-side, no additional JavaScript
37 | - Rich formatting
38 | - Many forms on same instance
39 | - File uploads
40 | - Templates
41 | - CORS
42 | - Access with bots
43 |
44 | Consider sponsoring me if you want to see new features
45 |
46 | ## Prerequisites
47 |
48 | 1. Create new account
49 | 2. In Element, go to "All settings" > "Help & About" and copy the "Access Token" from the "Advanced" section
50 | 3. For each form, create a new (unencrypted) room and then go to "Room Settings" > "Advanced" and copy the "Internal room ID"
51 | 4. Follow the installation proccess below
52 |
53 | ## Installation
54 |
55 | ### Using Docker
56 |
57 | ```sh
58 | docker run ghcr.io/mishushakov/matrix-forms:latest
59 | ```
60 |
61 | See the [configuration](#configuration) options below
62 |
63 | ### Bare-metal
64 |
65 | 1. Clone and enter the repository
66 |
67 | ```sh
68 | git clone https://github.com/mishushakov/matrix-forms && cd matrix-forms
69 | ```
70 |
71 | 2. Install the dependencies
72 |
73 | ```sh
74 | npm i
75 | ```
76 |
77 | 3. [Configure](#configuration) using environment variables
78 |
79 | 4. Start the server
80 |
81 | ```
82 | npm start
83 | ```
84 |
85 | ## Configuration
86 |
87 | ### Environment Variables
88 |
89 | `PORT` - Port to listen to
90 |
91 | `HOMESERVER` - Matrix Homeserver
92 |
93 | `ACCESS_TOKEN` - User access token
94 |
95 | `DEFAULT_ROOM` - Default room for submissions if no room is specified in query params
96 |
97 | `CORS` - CORS Origin
98 |
99 | ### Query Params
100 |
101 | `to` - Matrix room to submit the form
102 |
103 | `return` - Return URL (rendered in template)
104 |
105 | `redirect` - Redirect to `return` URL (Boolean)
106 |
107 | ### Templates
108 |
109 | `templates/message.handlebars` - Message template
110 |
111 | `templates/success.handlebars` - Success template
112 |
113 | ## Access forms with bots
114 |
115 | The messages should contain `matrix.forms` field to help you parse submission contents with bots
116 |
117 | ```json
118 | "matrix.forms": {
119 | "fields": {
120 | "link": "gfd",
121 | "name": "gfd"
122 | },
123 | "files": [
124 | {
125 | "content_uri": "mxc://matrix.org/gMFeqNWiVdMnBfmxTyxDgeMP",
126 | "name": "recording-3.ogg",
127 | "size": 12527
128 | }
129 | ]
130 | }
131 | ```
132 |
--------------------------------------------------------------------------------