├── .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 |
2 |
3 | 4 | 5 |
6 | 7 |
8 | 9 | 10 |
11 | 12 |
13 | 14 | 15 |
16 | 17 | 18 |
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 | 9 | {{/each}} 10 | 11 | 12 | {{#each @fields}} 13 | 14 | {{/each}} 15 | 16 |
{{capitalize @key}}
{{this}}
17 | 18 |

Files

19 | 20 | 21 | {{#each @files}} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {{/each}} 33 |
FileSizeContent URI
{{this.name}}{{this.size}}{{this.content_uri}}
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 |
9 |
10 | 11 | 12 |
13 | 14 |
15 | 16 | 17 |
18 | 19 |
20 | 21 | 22 |
23 | 24 | 25 |
26 | ``` 27 | 28 | **In Matrix** 29 | 30 | ![Screenshot](https://i.imgur.com/fSZuwNr.png) 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 | --------------------------------------------------------------------------------